10 ms·
Plain C API design, the real world Kobayashi Maru test
- CyberDildonics 4y agoThis is a few paragraphs about the C API of cairo, postscript and pdf and it doesn't seem all that insightful. I wish blog posts wouldn't use some random movie reference as clickbait when they could explain what their post is about instead.
- zabzonk 4y ago> you can have functions like pdf_page_cmd_l(page, x, y) 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.
- qsort 4y agoEven if you're using C++ internally, you're likely exposing a C API behind extern C, so you don't have access to those features at the API boundary.
- lelanthran 4y agoBut then it stops being a general tool that is used by python, PHP, java, lisp, C++, rust, nim, zig, lisp and becomes a specific tool for C++ programs.
- synergy20 4y agoC is truly the only universal common denominator for all other languages, C++ indeed is only for itself.
- wruza 4y agoHow would you use that in this case? These cmd_l ops are just similar ops to different objects, like pen on paper vs. brush on canvas. They don’t operate on a single type of object. 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.
- cozzyd 4y agoAside from the interlanguage interop issues, c++-like visibility control (via private) makes ABI compatibility essentially impossible.
- joeatwork 4y agoI know it isn’t really appropriate to the spirit of the article, but it seems like in this case there is a right answer, and it’s “Fully separate object types” - it’s explicit, prevents errors, is complete, and while it requires a lot of typing to implement it doesn’t require much complexity.
- ablob 4y agoIn which of the listed requirements of the article is this approach better, and why?
- joeatwork 4y agoI think it checks every box except “minimize the number of functions exposed”. Also, if your reasons for minimizing number of functions is about usability and complexity, then many functions that are all easily understood at once (and easy for the compiler to type check) might not substantially fail there either.
- cozzyd 4y agoYeah I agree. Macros can be used to avoid some of the typing in defining the interface when the implementation really is common, but unfortunately that makes it harder to document the generated interface. I wish doxygen had some way of supporting comments for macro-generated interfaces. 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.
- twic 4y agoThis is a nice concrete example of a situation where inheritance is useful for program design. 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!).
- fanf2 4y agoI have implemented this using the gcc/clang “transparent union” extension, which eliminates the need for explicit casting or helpers. https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.html#transparent_union https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.ht...
- jesse__ 4y agoI've done a few APIs similar in spirit to this one (and one similar in functionality, too), and I've found using a blend of a few of the mentioned methods to be pretty effective. 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.
- twisteriffic 4y agoThat style speaks to me. Thanks!
- lelanthran 4y agoFunnily enough that's the most common pattern I see in my personal C code (for example, my little lisp interpreter - https://github.com/lelanthran/csl/blob/master/src/parser/atom.c#L386 https://github.com/lelanthran/csl/blob/master/src/parser/ato...) but I still recommend using the `Generic` keyword in C. 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.
- Dwedit 4y agoCOM (Component Object Model) is compatible with C. It's still obviously C++ code being shoehorned into C by explicitly declaring the layout of the object (VTable member), but it does work. You get inheritance that way, and inter-module compatible cleanup. The downside is so much boilerplate code to declare the object, and the performance cost of virtual calls. 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.
- morelisp 4y agoThis is basically what the author is referring to with GObject.
- pavlov 4y ago> “Then it would be portable to systems that aren't 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.
- spacechild1 4y agoAnother example would be the VST3 SDK.
- kelnos 4y agoThis is more or less what GObject is, which the author mentions in passing. It's an OO system for C, but it does require quite a lot of boilerplate, and you need to manually initialize vtables when creating new subclasses. You also need to manually chain up to the superclass in virtual methods, in many places where it's easy to forget. It's a decent system, all things considered, but it's just a reminder that C's type system is very weak and implementing advanced features all but requires abuse of the preprocessor to avoid unreadable code.
- ar-nelson 4y agoI've had a lot of ideas for cross-language libraries that would need a C API, and this issue always comes up. The idea I had several years ago---but never implemented, because most of the projects I'd use this for are in limbo because I never seem to finish anything---is an API with only one function, which takes JSON and returns JSON, possibly via JSON-RPC. Basically a library that pretends it's a remote service. Slow, yes, but not as slow as some alternatives, and it makes FFI setup with other languages easy.
- CyberDildonics 4y agoSoftware that takes in text and outputs text is literally every command line program.
- ar-nelson 4y agoYes, but it's rare to see linked libraries that use this as their API, even though it would greatly simplify FFI.
- CyberDildonics 4y agoThat's the exact point. Why would someone use a linked library if speed doesn't matter and they are passing text back and forth to be parsed? 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.
- lelanthran 4y ago> is an API with only one function, which takes JSON and returns JSON, possibly via JSON-RPC. 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
- _0w8t 4y agoWhat is not considered in the article is to replace pointers to pages and patterns with handles that are tagged indexes into internal arrays. 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.
- tedunangst 4y agoI think the answer is function pointers.
- jacknews 4y agoOf course, you don't meed to recreate a full OOP system, to get the benefits required here. OTOH, it would make a terrible, or at least highly unorthodox API.
- nanofortnight 4y agoThis seems perfect for _Generic. https://en.cppreference.com/w/c/language/generic https://en.cppreference.com/w/c/language/generic
- mintplant 4y agoComposition? 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.
- dromtrund 4y ago+1, this approach will also highlight cases where you're trying to generalize something that isn't as generic as you thought. 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.
- uecker 4y agoI use this technique a lot and it is very powerful.
- zcbenz 4y agoAdding fields to context will break ABI compatibility. To work around the problem, some libraries add padding fields, and some require passing the size of struct to determine ABI version.
- cozzyd 4y agoI'm guessing context here is a pointer or a handle?
- 4y ago
- mkoubaa 4y agoSometimes I wonder whether C (the ISO programming language) will ever diverge from C (the de facto FFI and ABI for all systems ever) I expect no but I hope yes
- pjmlp 4y agoIn practice that has always been the case, if you do a random pub quiz, what many will answer will be "my C compiler does XYZ", not "ISO C defines XYZ". 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.
- ShroudedNight 4y ago> expose the full functionality of PDF directly 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
- deterministic 4y agoAPI stability is THE #1 most important thing to get right. 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.