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

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