Define a C++ struct/class in Julia for interop?

To efficiently call C or C++ libraries without an additional overhead of dereferencing pointers, you need to define a Julia struct with the same memory layout as the the struct/class in C or C++. For C, Clang.jl and CBinding.jl can automatically generate the Julia struct definitions by parsing the C headers. Is there something like this for C++? At a quick glance at CxxWrap.jl, it seems that the Julia side of the package simply uses a pointer to an opaque C++ struct, and the package doesn’t attempt to parse the struct/class definition to define it again on the Julia side (which I imagine is difficult in the presence of templates).

I’m thinking of a workaround: strictly speaking, on the Julia side, I just need to define a type that matches the size of the C/C++ struct (which can be found out using sizeof on the C++ side, possibly after template instantiation). This can be e.g. a StaticVector of N bytes, if N is the size of the C++ struct. It should work as long as all operations with the struct, including printing, is handled by the C++ side rather than the Julia side, right?

What about padding?

I think padding just means that the size of a C/C++ strut may be larger than the total size of the individual fields. This is OK since the larger number will be returned by sizeof when I inquire about the size of the struct.

No, there may also be padding between fields of an object. For example, this has a byte of padding between the two fields:

julia> struct Foo
           a::UInt8
           b::UInt16
       end

julia> Base.padding(Foo)
1-element Vector{Base.Padding}:
 Base.Padding(2, 1)

julia> sizeof(Foo)
4

The same can happen in a C++ struct, and observing that padding is not stable/legal.

In addition, if you want to call C++ native functions, you need to obey the relevant C++ calling convention, which julia can’t do natively. That’s why interop like this is usually done over C interfaces, because that ABI is much more stable.

Yes, I’m thinking of calling a C++ library using C as a bridge, without using CxxWrap.jl, if that helps me reduce the amount of pointer dereferencing.

That’s why I’ll run sizeof on the C/C++ side to find out the actual size which takes padding into account.

My actual problem is that my Julia code needs to to create an array of C++ objects of a certain class. Quoting the documentation of CxxWrap.jl, the C++ objects are mapped to this Julia type:

mutable struct WorldAllocated <: World
  cpp_object::Ptr{Cvoid}
end

This is a struct of a pointer to the actual C++ object, and since mutable structs are internally stored as pointers in Julia, my array will actually be an array of pointers to pointers, which is a bit too much, even though I’ll be happy to settle for an array of pointers (if they point to the actual objects directly).

P.S. Elaborating on the last paragraph above, if I write the interface myself without using CxxWrap, I could simply use a immutable struct for the WorldAllocated type above. The cost is that I’ll lose the ability to add a finalizer. But since my goal is creating an array of such immutable structs, I’ll just manually ensure that all the array elements are destructed before the array is out of scope.

1 Like

Can Clang.jl automatically generate the Julia struct definitions if I simply wrap the C++ library in a C library first? I suspect not, since parsing C++ headers (included by the C headers provided to Clang.jl) is more difficult, but I’ll be interested to hear from people who know about this.

I’m not just talking about any trailing padding, but about the padding between fields, which it’s illegal to read. Your NTuple approach would make them visible.

The issue with C++ classes is that their lifetimes are much more complicated than C structs. That’s why the wrapper needs that pointer internally, since the lifetime is managed by C++ and Julia can’t take direct ownership of the object.

This is all very abstract though, do you have a concrete example that we could use to work through this?

Thanks for the patience. :slightly_smiling_face:

As in the added paragraph in my updated post above, I’m now inclined to accept the use of pointers instead of reproducing the full struct definition on the Julia side. But I still don’t want to a situation like this

# This is what CxxWrap.jl generates, according to docs
mutable struct WorldAllocated <: World
  cpp_object::Ptr{Cvoid}
end

# User-written code
v = Vector{WorldAllocated}(undef, 100)

because v here will be an array of pointers to pointers, as an array of mutable structs is internally already an array of pointers in Julia, while the struct itself also holds a pointer.

Instead, I’ll bypass CxxWrap and write the wrapper from scratch (using C to bridge between Julia and C++). I’ll write

# Immutable struct to reduce the number of pointer layers
struct WorldAllocated
  cpp_object::Ptr{Cvoid}
  # Add constructor to call `new` in C++
end

v = Vector{WorldAllocated}(undef, 100)

Julia does not allow defining a finalizer for immutable types (to wrap the C++ destructor), so I’ll handle the destruction of array elements manually. For example, situation 1: the vector v is created by a function. I’ll run something like this before the end of the function:

for i in eachindex(v)
    destruct(v[i]) # `destruct` is defined by my wrapper and calls `delete` on the C++ side
end

Situation 2: the vector v is a data field of another mutable struct I’ve defined in Julia, then I’ll add the above deallocation code to the finalizer of the mutable struct.

If we’re talking about C-compatible structs that just happen to be defined in C++, that can be done with a 1:1 reproduction on the julia side without issues. It’s the use of C++ classes that is non-trivial and requires more specific handling, because of the additional state classes have compared to raw structs (dispatch tables etc).

It would be very helpful if you could present a concrete example that you’re working on. In particular, the example you’re talking about seems to me to be solved through the use of CxxRef, which does exactly the job you’re trying to do manually with your immutable struct (see e.g. here and here). With that, there’s no need for manual lifetime management and the finalizer can stay attached just fine. It would be good to have a concrete MWE to work with here.

1 Like

@Sukera Here’s an arbitrary example for wrapping a C++ class, std::map<int, int>. The C++ code is

#include "jlcxx/jlcxx.hpp"
#include <map>
namespace try_cxx_wrap
{
    using IntMap = std::map<int, int>;
}
JLCXX_MODULE define_julia_module(jlcxx::Module& types)
{
    using namespace try_cxx_wrap;

    types.add_type<IntMap>("IntMap")
	.constructor<>()
	.method("size", &IntMap::size);
}

The Julia code is

module TryCxxWrap
  using CxxWrap
  @wrapmodule(() -> joinpath("build", "lib", "libtryCxxWrap.so"))
  function __init__()
    @initcxx
  end
end

my_maps = [TryCxxWrap.IntMap() for i in 1:10]
println(
    map(TryCxxWrap.size, my_maps)
)

My questions are

  • my_maps in the Julia code is internally an array of pointers to pointers, as I said in previous posts. How do I reduce the number of pointer layers using the CxxRef you mentioned?
  • An unrelated new question. The actual use std::map in C++ relies on overloaded operators. After declaring
    std::map<int, int> m();
    I’ll use it like
    m[10]=20;
    How do I expose the overloaded array indexing operator [] to the Julia side, so that I can use the same Syntax?

Right:

and it’s worse in Rust, since not only is the padding not deterministic, but neither the order:

In Julia the order is always the same, and the padding, which seems good, but it handicaps the compiler, I’m not sure most Julia programmers realize the order can matter for size, and thus speed. Are there tools that point it out, that it might be better to rearrange? Also could it be done with a macro? It’s probably available in some package, and to allow disabling padding (or is such in Base?). Rust (also Mojo) has the most freedom for speed, and does allow C rules/deterministic. If you know the C++ (or Rust) compiler, it seems you should be able to assume what it does but I’m not sure, or ask for what it did…

@greatpet, I think you mean destructor i.e. on the C++ side. It doesn’t have finalizers, the similar concept GC-languages like Julia, Java and C# have. C# mistakenly called them destructors which doesn’t help with confusion, then officially renamed to (correct) finalizers, and I believe they’ve disabled them, or only on exit. Java has deprecated finalization, and maybe Julia should too, I know there an issue about disabling them on exit only, as C# did. I’m not sure if Java has done that already, likely not, they really want to keep their (API) compatibility, and I know of only one very rarely used (if ever actually), for threading API removed (recently in just out Java 22). I didn’t check if they just want to get rid of finalizers fully as a concept, or if Julia should then too, or if C# did.

1 Like

Apologies, I missed pressing “send” on my reply!

You allocate an intermediary just for passing the pointers to C++, e.g. by broadcasting CxxRef.(my_array). Since CxxRef are immutable, the pointers are stored inline and there is no additional indirection.

The array indexing syntax lowers to calls to getindex. See this section of the manual:

https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array