Plain C API design, the real world Kobayashi Maru test
nibblestew.blogspot.com
nibblestew.blogspot.com
I start with the basics and implement everything as explicitly (read: type safe) as possible:
struct bob_params {...}
struct line_params {...}
pdf_page_cmd_bob(bob_params *params) { ... }
pdf_page_cmd_line(line_params *params) { ... }
Then, when that works and I want to add patterns, which are a superset, I'd add something that is literally a superset of those functions. enum pattern_type {
patterntype_Bob,
patterntype_Line,
}
struct pattern {
pattern_type type;
union {
bob_params Bob;
line_params Line;
}
}
pdf_pattern_cmd(pattern *pattern) {
switch (pattern->type) {
case patterntype_Bob: /* do bob drawing a bunch of times, or whatever */
}
}
// The pattern struct has a lot of different names (sum type, discriminated union, algebraic datatype, tagged union .. probably more), but the idea is the 'type' tag tells you which one of the union values to use.
I've tried everything and, as far as I can tell, this gets you the best of all worlds. It's pretty much as type-safe as things get in C. It's extremely flexible; you just mix the fundamentals together as you like when adding higher-order functions. It's fast; the compiler can see exactly what's going on, so you pay minimal runtime cost.Yes, it's a bunch of typing to get the functions all spelled out, but it's really not that bad considering how easy/obvious the code actually is.
I use this pattern so much I actually wrote a little metaprogramming language that is capable of generating a lot of the boilerplate for you. Link in my bio, if anyone's interested in looking at it.
For the next time I do a pattern like this, I'll be using `Generic` keyword to make the dispatch a compile-time match, not check at runtime.
#define cbrt(x) _Generic((x), long double: cbrtl, \
default: cbrt, \
float: cbrtf)(x)
In code you'll write `cbrt(foo)` and the correct function will be called for the type of foo. As I understand it, the `_Generic` selection is performed at compile time.Furthermore, I could only find this feature in the clang-compiler and references to it outside of the online-docs in gcc.
/* foo.h */
struct foo_object;
struct foo_object* foo_create(int flags, …);
/* foo.c */
#include “foo.h”
struct foo_object {
int flags,
…
};Yeah, that usually kills that plan for me. I also try to avoid heap allocation wherever possible (which is usually pretty easy), and that solution ends up with an everything-is-heap-allocated nest of pointers, which I really try to avoid.
I typically do a request-response thing where the user code asks how big something is, then they do the allocation, and pass the allocation into the library. Windows does this a lot and, while Microsoft products are generally a giant garbage fire these days, I think this is one pattern they really got right.
It's much simpler to just mark fields as private with some convention. If callers want to muck with your struct, they will. Opaque types is a big stick, I rarely reach for it. Occasionally it's useful, but I struggle to recall when a simpler, less hostile approach was worse.
>> It's much simpler to just mark fields as private with some convention. If callers want to muck with your struct, they will.
Agreed, and people mucking with my struct is usually the least of my worries. If a user wants to do that, I say go to town.
The "ask for the size first" approach has one major advantage compared to shipping the full struct:
1) You can hide all the random-ass internal structs you use, which can greatly reduce the amount of header code you have to ship. I like shipping a single header w/ a static lib and this can be a huge win for that use case. Note that I don't want to hide the internal stuff because I think users are incompetent and will ruin both our days by doing something dumb. I want to hide noise that they would literally never care about, and don't need definitions for.
That all said, packaging an API is pretty tricky to get right and depends a lot on the actual functionality. Maybe we work on different subject matter, and therefore have different views.
Ultimately I want to be able to step through the full source, and from my perspective it's easiest if the library could just be inserted into my source tree regardless of whether it actually is or not.
I'm not saying I particular like when libraries grow to support a custom allocator either, but I suppose there's some inevitable law that holds, preceding the lisp interpreter being added. At that point the Microsoft approach seems strictly more laborious with less clear value, at least to me.
Your language [0] is an interesting idea. I've done a lot of similar stuff (not at work, I'm very rarely asked to write C code professionally, but I like mucking around with it in my personal time). My own approach to C metaprogramming involves obtuse preprocessor hacks (e.g [1]) and shell scripts that use grep/awk/sed to find macro invocations and spit out new header files as a result. Maybe your way is better.
A word of warning though – do you realise your name for your language is an anti-gay slur in several dialects of English? You might end up unintentionally upsetting some people with it.
[0] https://github.com/scallyw4g/poof
[1] https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,...
I considered doing this, albeit briefly, but decided against it for two reasons:
1) I hate the preprocessor. 2) I found a few projects that did that already.
Ultimately, writing a bespoke C parser and scripting language that sits on top of it was kind of an experimental accident. I just started doing it, and it's ended up working out so well that I've kept pushing on it to see where the natural conclusion is.
>> do you realise your name for your language is an anti-gay slur in several dialects of English?
I actually didn't realize that, thanks for pointing that out. I'll be changing the name.
So is puff. Am I to give up actual puff the magic dragon and sugar puffs? Being English I like the occasional puff on a fag aswell, there's a double entendre for every one there.
And then there's the pouffe that I rest my legs on...
I never told him he had to change it. I was simply checking if he was aware it was an anti-gay slur (in some countries), in case he wasn't – and indeed he said he wasn't. Now he is aware that it is, it is his decision how to respond. He's said he's going to change the name. If he'd decided the opposite, to keep the name – it wouldn't be my place to criticise his decision. If someone else felt it was theirs, that would be between him and them, not me.
And, personally, I've never heard ''puff'' been used as an anti-gay slur, but I've heard ''poof'' been used that way many times. But sure, when you tell me it has been, I believe you.
If someone insisted that ''Puff the Magic Dragon's'' name be changed because ''puff'' is a slur, I'd think that was silly. But there's a big difference between a 50 year old song, which exists in many people's childhood memories (it does in mine), and a pre-alpha open source project which very few have heard of, where if there'd ever be a good time to change its name, it would be now.
The problem is that the author doesn't really know the cultural nuances, simply saying it's a possible issue isn't helpful because they don't know the relative usages of certain words. I wouldn't be particularly worried about using poof because there already homophones that are innocent so it isn't like fuck. Plus at this point Im not even sure if it's derogatory or just slang for gay.
But anyway, I'm firmly in the intent camp when it comes to words. If the word as used is obviously not meant to offend, offense shouldn't be taken. The world's got enough problems without extracting insults from open source projects.
Fwiw puff the magic dragon to me is synonymous with smoking weed.
At high school in Sydney, Australia in the 1990s, I remember hearing poof as an anti-gay slur a lot - to the point that if I hear the word, that's the first thing that pops into my head. And poof is a clipping of poofter, which I heard a lot too, and the two words get mentally merged together. Whereas puff, to me that's a completely separate and unrelated word, which is pronounced differently, I never thought of connecting the two. From what you are saying, in some dialects in England, the two words are pronounced the same – didn't know that before
> Plus at this point Im not even sure if it's derogatory or just slang for gay.
It is derogatory, but it also has "re-claiming" uses. Few will be offended by a gay person using it, but someone who isn't gay calling gay people that is going to offend a lot of people. Similar to the situation with Black people calling themselves the n-word versus non-Black people doing it
In Australia, poof and poofter are associated with the so-called poofter-bashing, an epidemic of violent assaults and murders of gay men in the 1970s thru 1990s, in which the police were complicit. I think that history makes poof/poofter more offensive in Australian English than they are in British English
https://www.nytimes.com/2017/01/30/world/australia/australia...
> Fwiw puff the magic dragon to me is synonymous with smoking weed.
When I was a little kid, my mother used to play "Puff the Magic Dragon" (the Peter, Paul and Mary song) all the time. That's what it makes me think of, my early childhood. I know some people linked it to weed, but that's never been a prominent link in my head-and it is a link which the band themselves have always vehemently denied. Anyway, who really wants to mentally associate weed with their parents’ taste in music?
I'd say 'poof' is worse. Jesse's a real name in some parts of the world.
You don't necessarily need the complete COM system. For example, you could remove use of the Windows Registry, use of IDL files, remove the `QueryInterface` method, remove the use of GUIDs, remove the class factories (when just a simple 'create' function would do), remove Windows API functions related to cross-module memory management (not using `CoTaskAlloc`). Then it would be portable to systems that aren't Windows. One thing you can't remove is specifying a calling convention for the class methods, because C++ `__thiscall` is not compatible with C code on Windows.
I’ve seen this type of “COM Lite” used for cross-platform plugin and driver APIs. For example Blackmagic Design, a manufacturer of pro video capture hardware, provides an SDK that is essentially identical on Windows, Linux and Mac using this design.
And you can do COM objects in C without using the preprocessor at all (outside of the "are we C++ or not" condition, then you could use real classes instead)
Also, being an object system, not only a type system, it is more advanced than C++ in parts of properties and signals. Of course that requires some cooking. For example, Qt had to resort to a custom pre-compiler for that.
That said, I believe it loses quickly in ergonomics to plain Objective C, which is best “practical C + objects” flavor ever done, imo.
I believe VirtualBox is another project with COM on other platforms. I see lots of GUIDs and HRESULTs in their error messages.
I actually really like the COM style when done well. HRESULT, the idea of somewhat standardized error handling that sub-divides the space of a 32-bit integer into various subsystem-specific error codes, is one of my favorite ideas from there.
Some things are not so nice. For example, everything being a virtual call is not good for performance. Reference counting is also great but over-use of it is also not great for performance (for example, in C++ it's considered poor form to make everything std::shared_ptr<> when you can get away with less).
But I think part of their objections for within a module (from memory, because I haven't looked into their rationale in a while) is it's not worth the performance cost.
On .NET, the way RCW/CCW works requires understanding both .NET and C++, in other to get it right, and now they decided to also have another way of accessing COM from .NET since CsWinRT was introduced, with quite a bit of boilerplate as well.
The we have the developer groups always keen in pushing ATL, WRL and now C++/WinRT, all with the boilerplate, and after 20 years, still no good VS tooling for IDL files or code generation.
By the way, registration free COM is a thing since Windows XP, when application manifests were introduced.
It is quite ironic that the competition (Borland originally) always had much better tooling to deal with COM, than WinDev is willing to offer to their own developer community.
The way IO and Driver Kit work on macOS/iOS is also COM like.
You can totally do that. DLLs export `DllGetClassObject`, you call that with the GUID you want, and `IID_IClassFactory`, then you get a factory object. Then you call `CreateInstance` from the factory object, and you have your COM object.
Useful for Video Codecs and manually building DirectShow filter graphs. Works whether the codec is properly installed or not.
typedef struct { /* ... */ } context;
typedef struct {
context ctx;
/* ... */
} foo_context;
typedef struct {
context ctx;
/* ... */
} bar_context;
void some_general_method(context* ctx, int a, int b);
void some_foo_specific_method(foo_context* foo_ctx, int c, int d);
foo_context* foo_ctx;
some_foo_specific_method(foo_ctx, 0, 0);
some_general_method(&foo_ctx->ctx, 1, 1);
For checked downcasts, you could include an enum type tag inside `context` and have something like `foo_context* as_foo_context(context* ctx)` for downcasts, which, if `context` is at the beginning of `foo_context`, could just check the tag and cast the `ctx` pointer to `foo_context*` (or do a little pointer arithmetic if `context` is somewhere else in the containing struct). Return `NULL` (or assert) if the tag doesn't match.I'd argue that the need for downcasting in a method working on the inner context would also be a code smell, and that you might want to reconsider the context split. In some cases, there's no way around it (like async events), but it might be more appropriate to pass additional context or a callback instead, to avoid a circular dependency.
To work around the problem, some libraries add padding fields, and some require passing the size of struct to determine ABI version.
The big plus is the index tag allows to detect use after free and other memory safety bugs in wast majority of cases greatly improving memory safety.
If one then expose this handle as a generic typedef type over some integer type, then the API will be not type-safe, but the type mismatch will be detected very early.
Another option is to wrap the handle into separated structs for type safety. Then the caller will need to convert from specific handle for page, pattern etc. to the base handle when calling common draw operations. But that will be a simple operation like page.base or pattern.base, not MYLIB_PATTERN_TO_BASE(pattern). The drawback is that the caller will be able to construct wrong handles via struct initialization, but that is a big abuse and the type mismatch will still be detected at runtime.
I think i'd go for the "object oriented" approach, but with convenience functions to avoid explicit upcasts. Start with three types:
cairo_t /* a generic context, could be a page or a pattern */
cairo_page_t
cairo_pattern_t
Functions only defined on pages take a page: void pdf_page_cmd_bob(cairo_page_t* ctx);
Functions defined on both take a generic context: void pdf_ctx_cmd_l(cairo_t* ctx, int x, int y);
Then you need some way to upcast from the child types to the parent (which would be implicit in C++ etc): cairo_t* pdf_page_to_ctx(cairo_page_t* ctx);
cairo_t* pdf_pattern_to_ctx(cairo_pattern_t* ctx);
So a call looks like: pdf_ctx_cmd_l(pdf_page_to_ctx(page), 10, 20);
But we can generate this: void pdf_page_cmd_l(cairo_page_t* ctx, int x, int y) {
pdf_ctx_cmd_l(pdf_page_to_ctx(ctx), x, y);
}
Which lets users write this: pdf_page_cmd_l(page, 10, 20);
The convenience functions could even be macros. There would be no loss of type safety from using macros that way. There would need to be a lot of convenience functions or macros, but they are trivial, and so could be generated by a script (or another macro!).https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.ht...
In defining the implementation, it's easy enough to do something like this (if the implementation really is common):
static int pdf_ll_foo_impl(pdf_ll_ctx_t c, pdf_ll_type_t t, ...)
{
//real implementation goes here, perhaps switching on t
}
int pdf_page_foo(pdf_page_ctx_t c, ..) { pdf_ll_foo_impl((pdf_ll_ctx_t) c, PDF_PAGE_TYPE, ..); }
int pdf_pattern_foo (pdf_pattern_ctx_t c, ..) { pdf_ll_foo_impl(pdf_ll_ctx_t) c, PDF_PATTERN_TYPE, ..); }
which you can also use macros to help generate if you want to.I don’t care about the details of your API. I have worked with API’s that triggered a WTF every 5 secs. But I don’t care. I figure it out, build an abstraction on top that makes sense to me, and move on with far more important things.
So do what Linus does with Linux. Don’t f* break clients! Add a separate new API instead or expand the old API with additional entry points. Never ever deprecate anything in your API!
API developers that think it makes sense to break all clients, and force those clients to make changes, just because they wake up one day thinking that renaming FooBar to BarXk is a good idea, makes me want to throw my PC out of the window.
And who actually thought that marking things “deprecated” is a good idea? It is still an API change. With annoying warnings/errors added as a bonus.
Don’t. Do. It.
Use your brain to figure out how to accomplish the same task without breaking clients. The Linux guys can do it. So can you.
Is the objective to provide a PDF Swiss Army Knife? This requirement (if taken literally) precludes the API designer from enforcing much of a coherent mental model.
In terms of cheating, is one pattern format superior to the others? Could you pick one, only allow drawing commands for that one kind of pattern, then have the only method of rendering to the page take one or more of those patterns?
My ignorance of the PDF format is vast, and the question may be obviously absurd to anyone with the least bit of experience.
As an aside, this reminds me of the quagmire I have unsuccessfully wrestled with the various times I've tried to wrap my head around OpenGL
That's a terrible way to use a FFI and you would still deal with all the tricky parts.
If that's what you need, you would write a separate stand alone program and call that.
I've done this one, and once only. I wouldn't do it again because the pain point is the lack of typing.
Yeah, yeah, I know, you've read all these blogs everywhere about how C is not type-safe, how C is weakly-typed, etc, but it's a damn sight better than runtime errors because something emitted valid JSON that missed a field, or has the field in the wrong child, or the field is of the incorrect type, etc.
If you're sending and receiving messages to another part of the program, using an untyped interface with runtime type-checking is the worst way to do it; the errors will not stop coming.
Every single time your FFI functions are entered, the function must religiously type-check every parameter, which means that every FFI call made has to now handle an extra possible error that may be returned - invalid params.
Every single time your FFI function return, the caller must religously type-cechk the response, which means that the caller itself may return an extra possible error - bad response.
Having the compiler check the types is so much better. C enforces types on everything[1], almost everywhere. Take the type enforcement.
[1] Unless the type check is explicitly and intentionally disabled by the programmer
I expect no but I hope yes
Hence why then you see those rants when they get surprised trying to port to another platform, or compiler vendor, or having optimizations sneak on their back that are perfectly valid from ISO C point of view.
OTOH, it would make a terrible, or at least highly unorthodox API.
I wish blog posts wouldn't use some random movie reference as clickbait when they could explain what their post is about instead.
oh, no, please don't.
just use c++ and you can have namespaces or classes to control visibility - there is no need to take on all the other c++ stuff if you don't want it.
I’d say that C++ way is a bad idea here, because it usually begs for some compoinherimorphism with operator overloads that makes things 10x worse for the cost of one additional signature.