Pass vectors by reference using CxxWrap.@safe_cfunction

Hello,
I have a Julia function like

function f(x::AbstractVector{Float64}, y::AbstractVector{Float64})
    y[1] = 2.0*x[1];
    y[2] = 2.0*x[2];
    return nothing;
end;

which takes in input two arrays of Float64 and modifies the second one in place.

I am trying to use CxxWrap to evaluate this function with x and y as std::vector<double>, but I haven’t been able to succeed so far.

On the C++ side I have:

JLCXX_MODULE define_julia_module(jlcxx::Module& mod) {

    mod.method("call_val", [](void(*func)(std::vector<double>, std::vector<double>)) {
        std::vector<double> x = {1.0, 2.0};
        std::vector<double> y(x.size());
        func(x, y);
        return y;
    });

    mod.method("call_ref", [](void(*func)(std::vector<double>&, std::vector<double>&)) {
        std::vector<double> x = {1.0, 2.0};
        std::vector<double> y(x.size());
        func(x, y);
        return y;
    });
}

while on the Julia side:

f_val_c = @safe_cfunction(f, Cvoid, (CxxWrap.StdLib.StdVectorAllocated{Float64}, CxxWrap.StdLib.StdVectorAllocated{Float64},));
f_ref_c = @safe_cfunction(f, Cvoid, (CxxRef{CxxWrap.StdLib.StdVectorAllocated{Float64}}, CxxRef{CxxWrap.StdLib.StdVectorAllocated{Float64}},));

a = call_val(f_val_c);
println(a);

b = call_ref(f_ref_c);
println(b);

the first option doesn’t throw any error, but since y is passed by value to func, I have no way to use the outcome of the function evaluation.

In the second case, I get this error:

ERROR: Incorrect argument type for cfunction at position 1, expected: CxxRef, obtained: CxxRef

I tried to modify the signature passed to @safe_cfunction in few different ways, but none seems to work.

Do you have any suggestion?

Hi @afossa !

See the section “Working with arrays” in the CxxWrap documentation here:

Thanks @Ronis_BR
I have already checked out the documentation multiple times, but there is no answer for my particular case.

My final objective is to write a Julia interface to a C++ library. This library has some methods which expect as input a function with signature:

std::vector<double> f(const std::vector<double>&) const;

such that they can evaluate it on std::vector<double> created and managed in C++. I am not interested in evaluating f on some Julia arrays, nor to immediately retrieve the output of the function evaluation, since it will be stored in a C++ class I will be able to query later.

In the example above I rewrote f as an inplace function because it seemed easier for me to interface, and then I can just define a wrapper around it in C++ to match the expected signature.

I see. In this case, maybe the easiest option would be to keep everything in C++ and use CxxWrap to create the interfaces for mutating the values. Those interfaces will receive the values that must be applied to the std::vector. Would it work for your use case?

As long as I can define my function in Julia, and I can evaluate it also on a regular Julia Vector{Float64} (for testing, comparison, etc.), I think it will work. Could you clarify a bit more what you mean?

I would also like to minimize allocations and copying values between Julia and C++ vectors, since these can be quite large in size.

I am doing like this:

  1. I create a singleton in C++ to load and store any C++ value.
  2. I create interfaces to send and receive those values in Julia.

Hence, instead of having a function that will modify the elements in place, you would pass the elements (like integers) and the C++ function will change inside the vector. It works perfectly for me, but it can be hard / impossible if you need to send / receive a lot of values.

For now I came up with this solution:

On the C++ side I have

typedef std::function<std::vector<double>(const std::vector<double>&)> function_t;

function_t make_callable(void *f, size_t n) {
  auto g = reinterpret_cast<void(*)(const double*, double*)>(f);
  return [g,n](const std::vector<double>& x) {
    auto x_ptr = x.data();
    auto y = std::vector<double>(n);
    auto y_ptr = y.data();
    g(x_ptr, y_ptr);
    return y;
  };
}

JLCXX_MODULE define_julia_module(jlcxx::Module& mod) {
    mod.method("eval", [](void *f, jlcxx::ArrayRef<double> a, const size_t n) {
        auto g = make_callable(f, n);
        const std::vector<double> x(a.begin(), a.end());
        return g(x);
    });
}

while in Julia I do

function f!(y::T, x::T) where T<:AbstractVector{Float64}
    y[1] = 5.0*x[1];
    y[2] = 7.0*x[2];
    return nothing;
end;

function fwrap!(f!::Function, x_ptr::Ptr{Float64}, y_ptr::Ptr{Float64}, n::Integer, m::Integer)
    x = unsafe_wrap(Array, x_ptr, n);
    y = unsafe_wrap(Array, y_ptr, m);
    f!(y,x);
    return nothing;
end;

v = [2.0, 3.0];
n = 2;
y = zeros(Float64, n);
f!(y,v);
println(y);

fwrap_c = @cfunction((x,y) -> fwrap!(f!,x,y,n,n), Cvoid, (Ptr{Float64}, Ptr{Float64}));
y_c = eval(fwrap_c, v, n);
println(y_c);

I am not sure if it is the best solution, but it works.

I also tried with @safe_cfunction instead of the bare @cfunction, e.g.

fwrap_cs = @safe_cfunction((x,y) -> fwrap!(f!,x,y,n,n), Cvoid, (Ptr{Float64}, Ptr{Float64}));
y_cs = eval(fwrap_cs, v, n);
println(y_cs);

and then

function_t make_callable(jlcxx::SafeCFunction f, size_t n) {
  auto g = jlcxx::make_function_pointer<void(const double*, double*)>(f);
  return [g,n](const std::vector<double>& x) {
    auto x_ptr = x.data();
    auto y = std::vector<double>(n);
    auto y_ptr = y.data();
    g(x_ptr, y_ptr);
    return y;
  };
}

JLCXX_MODULE define_julia_module(jlcxx::Module& mod) {
    mod.method("eval", [](jlcxx::SafeCFunction f, jlcxx::ArrayRef<double> a, const size_t n) {
        auto g = make_callable(f, n);
        const std::vector<double> x(a.begin(), a.end());
        return g(x);
    }, "Evaluates `arg1` on `arg2` returning an array of size `arg3`.");
}

but in this case I get a cryptic error Type Pd has no Julia wrapper

1 Like