While reading this blogpost that talks about the journey of adding Z3 bindings to Crystal I was going to suggest the author to start using
to_unsafe in their code so they don’t have to manually convert things from Crystal to C.
But then I realized that wouldn’t always work… I’ll try to explain why.
In Z3 there’s an opaque
type LibZ3::Ast = Void* type. Then we can define a wrapper for it:
module Z3 struct IntExpr def initialize(expr : LibZ3::Ast) @expr = expr end end end
to_unsafe in that struct we can directly pass such instances to C. So far, so good!
However, in some places the C API accepts a pointer of
LibZ3::Ast together with a length: the usual way one passes lists of things to C.
If in Crystal you are working with the high-level interface, you are working with
Z3::IntExpr instances. If you have a bunch of these, you will have
Array(Z3::IntExpr). Then if you want to pass that array to C, you have to do something like:
That is, go from
Array(LibZ3::Ast). Because there’s
Array#to_unsafe, that can be automatically passed to C as the
to_unsafe method of this array will be
So with all of this I had some observations:
map(&.to_unsafe)call will allocate a new array, but the byte contents of the array contents will be exactly the same. After all a wrapper struct holds the same bytes as the wrapped struct. So first it’s inefficient to do so. In fact in the compiler’s code we do something different, something like
array.to_unsafe.as(LibZ3::Ast*). That is, we assume the
to_unsafecall has a pointer that points to the same memory layout we need.
- It seems wrapper structs are very common. We can have a
to_unsafe, but why not make it simpler?
So here’s the final idea:
- If you define a Crystal struct, and that struct contains exactly one instance variable, and that variable’s type matches the C type, just let it pass, without any extra work. Let’s call this a “wrapper struct”.
- If you have a pointer of a wrapper struct, and the C function expects a pointer of the wrapped struct, just let it pass: the memory layout behind the pointer will match
- If you have a type that doesn’t match the C function argument, try to call
to_unsafeon it. And here apply the same rules again: if that type matches, it’s all good. And it’s also good if the
to_unsafecall returns a pointer of a wrapper struct, and the C function expects a pointer of a wrapped struct
I created a draft PR for this. It was very easy to implement! And I tried this in the compiler’s code, and also in the Z3 bindings that are being created. The code ends up being so much simpler.
Is there a catch? Like everything, yes. I only see one downside of this: it’s implicit behavior. So if you have a struct that wraps a C value, but then you also have some other instance vars, it won’t work out of the box. You will have to have a
to_unsafe method. And of course arrays of these types won’t work. Adding other instance vars can also be done by reopening a type, so your code might stop compiling as soon as you do that.
BUT. I think in most cases:
- wrapper structs just wrap one value. It’s uncommon to also include more data
- reopening wrapper structs is very, very uncommon. I’d say it simply doesn’t happen. Nobody wants to mess with a type that wraps a C type. Nobody wants to mess with C :-D
- you can always define a
to_unsafeif the implicit wrapper struct doesn’t work for you
What do you think?
I sent a few different proposals in the past, but I feel this proposal actually adds value to the language, by simplifying interfacing with C, and also by ending up with more efficient code (no need to