How to pass a big chunk of data from C++ to a Julia script without making a copy?

Say I have a meg of data of some structure. For example,

struct NodeData {
    float position[3];
    float orientation[4];
    unsigned int connectedNodeIndices[5];
};

I want to pass this data from C++ to a Julia script for some processing without having to copy all the data first, but such that the Julia side has an equivalent struct and is able to reference each node the same way C++ does.
In C++ pointers solve this problem for passing big chunks of data from one module to another. The receiving module would simply cast the data something like this:

MyModuleClass* MyModuleFactory(void *_in) {
    MyModuleClass *ptr = new MyModuleClass();
    ptr->data = (NodeData *) _in;
    return ptr;
};

So passing a C++ pointer and turning it into an object reference in Julia somehow.
Is it possible? If so, what would be the best way?

Certainly this is possible. Let’s say you had the following C++ code, which is compiled into a shared library:

test_lib.cpp

#include <stdlib.h>

struct NodeData {
    float position[3];
    float orientation[4];
    unsigned int connectedNodeIndices[5];
};

extern "C" {
  
NodeData* alloc_nodes(int N) {
    // We use malloc rather than `operator new[]` here so that Julia
    // can just use `free()` on the other side.
    // If your application differs in how it allocates memory, you may
    // need to write a `delete_nodes()` function for Julia to call.
    NodeData* data = (NodeData*)malloc(sizeof(NodeData)*N);
    for(int i = 0; i < N; ++i) {
        data[i].position[0] = i;
        data[i].position[1] = 0;
        data[i].position[2] = -i;
        // ... and fill in other fields
    }
    return data;
}

} // extern "C"

I’ll assume you want to call your C++ code from Julia (rather than embedding Julia in C++) as this is much easier to demonstrate.

To make this into a shared library which you can call from Julia, you can use gcc, for example with the following flags (assuming you’re compiling on linux):

g++ -fPIC -shared test_lib.cpp -o test_lib.so

test_lib.jl

Now you can make a Julia struct to mirror the exact layout of your C++ struct, and call your C++ function alloc_nodes() from Julia using @ccall:

# StaticArrays is convenient here, but not required. You could use
# NTuple{3,Cfloat} to get the same memory layout as SVector{3,Cfloat}
using StaticArrays

struct NodeData
    position::SVector{3,Cfloat}
    orientation::SVector{4,Cfloat}
    connectedNodeIndices::SVector{5,Cuint}
end

# Assume test_lib.so is in the current directory with the Julia script
const test_lib = "./test_lib.so"

function alloc_nodes(N)
    node_data_ptr = @ccall test_lib.alloc_nodes(N::Cint)::Ptr{NodeData}
    # Here we wrap the C++ data in a Julia Array for convenience.
    # This isn't strictly necessary - you could use `unsafe_load`
    # with node_data_ptr instead.
    # Note `own=true` here β€” in this example, we assume Julia will take
    # responsibility for calling `free(node_data_ptr)` for memory cleanup.
    node_data = unsafe_wrap(Array, node_data_ptr, N; own=true)
end

function test_interop()
    N = 4
    nodes = alloc_nodes(N)
    for i = 1:4
        @info "Node $i" nodes[i].position
    end
end

Calling this from the julia REPL, we can see the data is being correctly passed from C++ to Julia:

julia> test_interop()
β”Œ Info: Node 1
β”‚   (nodes[i]).position =
β”‚    3-element SVector{3, Float32} with indices SOneTo(3):
β”‚     0.0
β”‚     0.0
β””     0.0
β”Œ Info: Node 2
β”‚   (nodes[i]).position =
β”‚    3-element SVector{3, Float32} with indices SOneTo(3):
β”‚      1.0
β”‚      0.0
β””     -1.0
β”Œ Info: Node 3
β”‚   (nodes[i]).position =
β”‚    3-element SVector{3, Float32} with indices SOneTo(3):
β”‚      2.0
β”‚      0.0
β””     -2.0
β”Œ Info: Node 4
β”‚   (nodes[i]).position =
β”‚    3-element SVector{3, Float32} with indices SOneTo(3):
β”‚      3.0
β”‚      0.0
β””     -3.0
11 Likes

Great stuff, thanks for taking the time, I really appreciate it!

1 Like