CGlue Today And The Road Ahead

Published: July 23, 2021, 1:24 p.m.

Hello again, CGlue has had several updates already with 0.1.2 being the latest. Let's cover it! And also, I think it's a good a time to start talking about the road ahead.

Recap

For those unaware, CGlue is an ABI-Stable code generator. You can think of it as cbindgen for Rust traits. It takes a regular non-FFI-safe Rust trait, and produces C vtables and wrappers to be used safely across the FFI boundary. It's kind of like abi_stable, but focuses more on adapting existing code with no hassle, as well as easier path to stability in general with extras on top.

Changes

Quickly about 0.1.2. There have been several fixes and additions made thanks to using the crate, and integrating it with memflow. For starters, a new CIterator type was added. It allows to pass iterators across FFI-boundary. I believe one could already implement it with CGlue without an extra type, but I think it's a core part of Rust that should get preferential treatment for easier usage. A few tiny bugs were fixed in some of the CGlue types, and a nasty clippy lint was fixed when a blanket trait function implementation would declare a mutable parameter. This would lead a confusing clippy lint asking to remove the mut, while in fact it was necessary for the implementation to compile. I never made a changelog for 0.1.1, so the key part there was that no_std got supported, given alloc feature (thanks, ko1N!). I believe I could get rid of alloc requirement, by feature gating some types. But this is about it for the present changes. Now, onto the future.

The road ahead

On initial release, one individual commented about interop with C - there were no examples, even though I claimed it would work. So, I went ahead and added usage example for C/C++ code. But this is where the first design flaw comes into play. Did you know you can mark trait functions as extern "C"? I mean why couldn't you? But until today nobody would really have a reason to make use of C ABI trait functions. When writing CGlue I thought different. Given a function is marked as extern "C", CGlue generator could entirely omit the wrapper generation, and jump to the real implementation function with zero overhead. Neat, huh? To not confuse the generator code with multiple cases, I made all functions behave the same - the wrappers would accept the underlying type that is inside the container, rather than the container holding the type, vtables, and temporary variables. This poses some serious challenges when calling functions from C/C++. Have a look at this worst case scenario:

obj.vtbl_keyvaluestore->get_key_value(obj.instance.inner.instance, key_slice, &obj.instance.ctx);

Not only the user has to access the vtable manually, which is bad enough, albeit manageable, but they would also need to unpack the correct instance type, as well as temp context. Most of this can be abstracted away with macros, but a) it is unnatural, and b) it is non-deterministic - context is not always needed, the inner type can have different types based on container used, and sometimes you need to actually pass the whole container (for self-consuming calls), requiring at least 3 macros to cover all cases for a single container.

I want to fix this, and ultimately, here are the few key things I want to achieve:

  1. Make vtable arguments deterministic - always passing container type.

  2. Have just 1 C macro to rule them all: VCALL(THIS, GROUP, FUNC, ...) (THIS.vtbl_##GROUP->FUNC)(&THIS, VA_ARGS)

  3. Generate forward trait functions in C++ headers for simple and intuitive usage.

In other words, I want the previous call look like this in C++:

obj.get_key_value(key_slice);

This will require breaking the codegen API, and potentially saying goodbye to the zero-wrapper optimization. However, I do not think either of them are that big of a deal. Let me explain.

As for API change, yes it makes old binaries incompatible with the new ones, assuming the new ones change to CGlue 0.2. But if you have production code already using CGlue, you can freely stay on 0.1 - bug fixes, and some feature backports will still be provided at least until 0.3. In the future, I would consider extending this guarantee to several versions, depending on demand, but we will see that then. I am only human and I do make mistakes, as well as bad design decisions. Ultimately, what I would like CGlue to turn out to be is a good ABI stability solution, not necessarily stable across minor versions while it's getting there. So I think some breakage is necessary for the end goal.

As for the zero-wrapper optimization, I think it's a pretty niche use case. I will actually attempt to keep it behind feature flag, but it very much complicates C code, and I believe the performance benefits are not that significant (inlining is a thing), especially knowing how rare you can take advantage of them (function marked as extern "C", no wrapped types like slices, strs, etc.). I believe the cost outweighs the benefits.

As for other improvements, I would aim to land API stability checks for 0.2 - I believe using abi_stable for this would be ideal, as they did a great job at the StableAbi derivation. It will be a feature flag. Default or not still to be decided upon.

And finally, I would like to experiment with a potential nightly flag to use specialization for grouping - this would remove the need for manual cglue_impl_group definitions, and always pick the widest trait set available.

Closing thoughts

Anyways, this is it for now. I'm very glad how well the project was received! And I hope it can turn out to be something very useful to many! I will get on with the changes, and be back with more in the future. Thanks for reading this, and have a very nice day :)

P.S. feel free to discuss on reddit