CxxWrap std::vector<T> as a function argument


#1

Hello, is there a simple/built in way to pass array of C++ stucts from Julia to c++?

C++ code:

struct Foo
{
  std::wstring name;
  int dataLen;
  double* data;
};

void useFooVec(const std::vector<Foo>& v) # how to deal with the vector<T> to be called from julia?
{
  // ...
}

Julia side

using CxxWrap

wrap_modules(raw"PATH_TO_MODULE\foo.dll")

struct JFoo
  name::String
  data::Vector{Float64}
end

jFooVec = [JFoo("a", [1., 2., 3.]), JFoo("b", [11., 12., 13.])]

# CppFoo.useFooVec(jFooVec) # ???

#2
struct CFoo
  name::Ptr{Cwchar_t}
  len::Cint 
  data::Ptr{Float64}
end
struct JFoo
  name::String
  data::Vector{Float64}
end

function Base.cconvert(x::JFoo)
     # Might work a bit simpler, but for all I know this is the official way that makes sure we preserve gc roots!
    nptr = Base.unsafe_convert(Ptr{Cwchar_t}, Base.cconvert(Cwstring, x.name))
    dptr = Base.unsafe_convert(Ptr{Float64}, Base.cconvert(Ptr{Float64}, x.data))
    CFoo(nptr, Cint(length(x.data)), dptr)
end

I think that’s what you need to do… Sorry for the awkwardness, I think cstruct compatibility needs a revamp :wink:
edit
you might also want to see the ccall signature

j_interact_with_struct(x::JFoo) = ccall((:c_interact_with_struct, lib), Void, Tuple{CFoo}, x)

#3

If you can modify the type on the C++ side to add a constructor, and if you don’t need a “mirror” struct on the Julia side, this would be the current way to do it in CxxWrap:

C++:


struct Foo
{
  Foo(const std::wstring& n, jlcxx::ArrayRef<double,1> d) : name(n), data(d.begin(), d.end())
  {
  }

  std::wstring name;
  std::vector<double> data;
};

// In the module definition
foomod.add_type<Foo>("Foo")
  .constructor<const std::wstring&, jlcxx::ArrayRef<double,1>>()
  .method("name", [](Foo& f) { return f.name; })
  .method("data", [](Foo& f) { return jlcxx::ArrayRef<double,1>(&(f.data[0]), f.data.size()); });

foomod.method("print_foo_array", [] (jlcxx::ArrayRef<jl_value_t*> farr)
{
  for(jl_value_t* v : farr)
  {
    const Foo& f = *jlcxx::unbox_wrapped_ptr<Foo>(v);
    std::wcout << f.name << ":";
    for(const double d : f.data)
    {
      std::wcout << " " << d;
    }
    std::wcout << std::endl;
  }
});

Julia side:

using CxxWrap
wrap_modules(raw"PATH_TO_MODULE\foo.dll")
using FooMod: Foo, name, data, print_foo_array

foovec = Any[Foo("a", [1.0, 2.0, 3.0]), Foo("b", [11.0, 12.0, 13.0])] # Must be Any because of the boxing
@show name(foovec[1])
@show data(foovec[1])
print_foo_array(foovec)

This approach is not ideal:

  • The structs are boxed, so you have an array of pointers. To pack the data, you have to use @sdanisch 's approach with an intermediate isbits struct
  • Currently std::vector is not wrapped, so if you need that in your API it will need extra work
  • ArrayRef doesn’t support boxed values directly, so you need the jl_value_t* and unbox_wrapped_ptr ugliness.

#4

barche, thanks a lot :slight_smile:
I’ve been approaching your solution slowly… I’ve modified Foo on the C++ side, but the approach with the data copying looks much better.

struct Foo
{
  Foo(const std::wstring name, jlcxx::ArrayRef<double> data) : 
    _name{ name }, _data{data} {}
  std::wstring _name;
  jlcxx::ArrayRef<double> _data;
};

And the trick with print_foo_array...:+1:
Despite the not wrapped std::vector and the boxed ugliness, the solution looks great.
Could you please explain, what work is needed to wrap the std::vector?

Thanks to everyone who is supporting C++ usage with Julia, great work guys!


#5

OK, great! Note that ArrayRef does not protect the referenced array from garbage collection, so the array should be referenced explicitly on the Julia side if you keep an ArrayRef stored somewhere.

Regarding std::vector, I think that something like what I’m doing for the Trilinos Kokkos View should work. The idea is that you get a pointer to the C++ data, which is wrapped in a Julia type that implements the Array interface. To be completely general, for element types that are non-bits the actual vector should be wrapped and accessed using std::vector access functions, instead of using the raw data pointer. The C++ code:

Julia code:


#6

Thanks, for the note. Your solution looks better, I like the constructor trick.

Regarding the std::vector,the way seems to be clear. Thanks for help!


#7

One more question: Is there a way to call a callback defined in Julia where input parameter is Vector?

function testf_arf(v::Vector{Float64}, s::String)
  r = sum(v)
  print_with_color(:green, "callback in Julia: $(s) = $(r)\n")
  return r
end

@show c_func_arf = safe_cfunction(testf_arf, Float64, (CxxWrap.CppArray{Float64}, String))

C++ side:

foomod.method("fn_clb", [](double(*fnClb)(jlcxx::ArrayRef<double>, wstring))
{
  vector<double> v{ 1., 2. };
  auto ar = jlcxx::ArrayRef<double, 1>(v.data(), v.size());
  // fnClb(ar.wrapped(), L"calledFromCPP"); // ??? 
});

#8

I’m using Julia 0.6 and the Base.cconvert generates an error, and that was the reason I was preferring the CxxWrap solution.

julia> Base.cconvert(CFoo, jFoo)
ERROR: MethodError: Cannot `convert` an object of type JFoo to an object of type CFoo
This may have arisen from a call to the constructor CFoo(...),
since type constructors fall back to convert methods.
Stacktrace:
 [1] cconvert(::Type{T} where T, ::JFoo) at .\essentials.jl:149

the problem can be overcome by writing a constructor for CFoo, which works fine:

struct CFoo
  name::Ptr{Cwchar_t}
  len::Cint 
  data::Ptr{Float64}

  function CFoo(a::JFoo)
    dptr = Base.unsafe_convert(Ptr{Float64}, Base.cconvert(Ptr{Float64}, a.data))
    nptr = Base.unsafe_convert(Ptr{Cwchar_t}, Base.cconvert(Cwstring, a.name))
    new(nptr, Cint(length(a.data)), dptr)
  end
end

jFooVec = [JFoo("a", [1., 2., 3.]), JFoo("b", [11., 12., 13.])]
cFooVec = [CFoo(a) for a in jFooVec]

#9

Yes, but here too the parameters (both string and vector) need to be boxed:

mod.method("fn_clb", [](double (*fnClb)(jl_value_t*, jl_value_t*)) {
    std::vector<double> v{1., 2.};
    auto ar = jlcxx::ArrayRef<double, 1>(v.data(), v.size());
    fnClb((jl_value_t *)ar.wrapped(), jlcxx::box(std::wstring(L"calledFromCPP")));
});

And then in Julia the cfunction signature must use Any:

c_func_arf = safe_cfunction(testf_arf, Float64, (Any,Any))
fn_clb(c_func_arf)

It’s also possible to directly use the regular Julia function:

mod.method("fn_clb2", [] (jl_function_t* f) {
    std::vector<double> v{1., 2.};
    auto ar = jlcxx::ArrayRef<double, 1>(v.data(), v.size());
    jlcxx::JuliaFunction fnClb(f);
    fnClb((jl_value_t*)ar.wrapped(), std::wstring(L"calledFromCPP"));
});

Then in Julia you just need:

fn_clb2(testf_arf)

BTW, if this needs to be elaborated further it may be better to create an issue on the CxxWrap github.