Julia and C++: a technical overview of CxxWrap.jl
C++ Code exposing class Foo
#include <jlcxx/jlcxx.hpp>
class Foo
{
public:
Foo(int i = 0) : m_value(i) {} // Constructor
int add(int i) const { return m_value + i; } // Member function
private:
int m_value; // Private data
};
JLCXX_MODULE define_julia_module(jlcxx::Module& mod)
{
mod.add_type<Foo>("Foo")
.constructor<int>()
.method("add", &Foo::add);
}
Usage from Julia
module LibFoo
using CxxWrap
@wrapmodule "libfoo/build/lib/libfoo"
function __init__()
@initcxx
end
export Foo, add
end
julia> using .LibFoo
julia> f = Foo(3)
Main.LibFoo.FooAllocated(Ptr{Nothing} @0x00000000021c9860)
julia> add(f,1)
4
A quick ccall
reminder
int foo(int a, int b) { return a + b; }
Compile using e.g:
clang -shared -o libfoo.dylib foo.c
Use in Julia:
julia> ccall((:foo, "libfoo"), Cint, (Cint,Cint), 1, 2)
3
That was easy! Let's try C++!
int foo(int a, int b) { return a + b; }
Compile using e.g:
clang++ -shared -o libfoocpp.dylib foo.cpp
Use in Julia:
julia> ccall((:foo, "libfoocpp"), Cint, (Cint,Cint), 1, 2)
ERROR: could not load symbol "foo":
dlsym(0x7fa0c8c4eef0, foo): symbol not found
Stacktrace:
[1] top-level scope at ./REPL[1]:1
C++ name mangling
nm -gU libfoo.dylib
0000000000000fa0 T _foo
nm -gU libfoocpp.dylib
0000000000000fa0 T __Z3fooii
julia> ccall((:_Z3fooii, "libfoocpp"), Cint, (Cint,Cint), 1, 2)
3
(Un)fortunately this is not portable
Alternative ccall
sequence
julia> using Libdl
julia> fooptr = dlsym(dlopen("libfoo"), :foo)
Ptr{Nothing} @0x000000010f5d3fa0
julia> ccall(fooptr, Cint, (Cint,Cint), 1, 2)
3
Exploiting this to call c++ functions
#include <vector>
int foo(int a, int b) { return a + b; }
// Filled using registration functions
std::vector<void*> registered_functions = {reinterpret_cast<void*>(foo)};
extern "C" {
void* getfunction(int function_idx)
{
return registered_functions[function_idx];
}
}
julia> fooptr = ccall((:getfunction, "libfoocpp"), Ptr{Cvoid}, (Cint,), 0)
Ptr{Nothing} @0x0000000126f79cc0
julia> ccall(fooptr, Cint, (Cint,Cint), 1, 2)
3
Additional complications
Solution: use std::function
#include <functional>
class Foo
{
public:
int foo(int a, int b) { return a+b; }
};
int main()
{
std::function<int(Foo&, int, int)> f([] (Foo& foo, int a, int b)
{
return foo.foo(a,b);
});
Foo myfoo;
int result = f(myfoo, 1, 2);
return 0;
}
Solution: use std::function
template<typename R, typename... Args>
struct CallFunctor
{
// ... skipping horrible stuff to compute return_type
static return_type apply(const void* function,
static_julia_type<Args>... args)
{
try
{
auto std_func =
reinterpret_cast<const std::function<R(Args...)>*>(function);
return convert_to_julia((*std_func)(convert_to_cpp<Args>(args)...));
}
catch(const std::exception& err)
{
jl_error(err.what());
}
}
};
Fundamental types
double
↔ Float64
void*
↔ Ptr{Cvoid}
Integers in C++
Fixed-size integers in C++
Choices had to be made 😰
Mac
char -> CxxChar
int -> Int32
unsigned int -> UInt32
long -> CxxLong
unsigned long -> CxxULong
long long -> Int64
unsigned long long -> UInt64
int32_t -> Int32
uint32_t -> UInt32
int64_t -> Int64
uint64_t -> UInt64
Linux 64 bit
char -> CxxChar
int -> Int32
unsigned int -> UInt32
long -> Int64
unsigned long -> UInt64
long long -> CxxLongLong
unsigned long long -> CxxULongLong
int32_t -> Int32
uint32_t -> UInt32
int64_t -> Int64
uint64_t -> UInt64
Other types
C structs map directly, e.g:
C and C++
struct Foo {
void* p;
};
Julia
struct Foo
p::Ptr{Cvoid}
end
The pointer can be any C++ object, so we use this a lot!
Type creation mechanism
add_type
really creates three types
mod.add_type<Foo>("Foo");
abstract type Foo end
mutable struct FooAllocated <: Foo
cpp_object::Ptr{Cvoid}
end
struct FooDereferenced <: Foo
cpp_object::Ptr{Cvoid}
end
References and pointers
We need distinct types for T*, const T*, T&, const T&
abstract type CxxBaseRef{T} <: Ref{T} end
struct CxxPtr{T} <: CxxBaseRef{T}
cpp_object::Ptr{T}
end
struct ConstCxxPtr{T} <: CxxBaseRef{T}
cpp_object::Ptr{T}
end
struct CxxRef{T} <: CxxBaseRef{T}
cpp_object::Ptr{T}
end
struct ConstCxxRef{T} <: CxxBaseRef{T}
cpp_object::Ptr{T}
end
Example
class Foo
{
public:
Foo(int i = 0) : m_value(i) {}
int add(int i) const { return m_value + i; } // Member function
Foo* thisptr() { return this; }
Foo& thisref() { return *this; }
Foo thiscopy() { return *this; }
const int* valueptr() { return &m_value; }
private:
int m_value;
};
Example
julia> f = Foo(2)
Main.LibFoo.FooAllocated(Ptr{Nothing} @0x0000563768a76130)
julia> thiscopy(f)
Main.LibFoo.FooAllocated(Ptr{Nothing} @0x0000563768149440)
julia> thisref(f)
CxxWrap.CxxWrapCore.CxxRef{Foo}(Ptr{Foo} @0x0000563768a76130)
julia> thisptr(f)
CxxWrap.CxxWrapCore.CxxPtr{Foo}(Ptr{Foo} @0x0000563768a76130)
julia> thisref(f)[]
Main.LibFoo.FooDereferenced(Ptr{Nothing} @0x0000563768a76130)
julia> thisptr(f)[]
Main.LibFoo.FooDereferenced(Ptr{Nothing} @0x0000563768a76130)
julia> add(thisref(f), 2)
4
julia> valueptr(f)
CxxWrap.CxxWrapCore.ConstCxxPtr{Int32}(Ptr{Int32} @0x0000563768a76130)
julia> valueptr(f)[]
2
Caveat: object lifetime
This is fine, right?
julia> function usefoo()
f = Foo(2)
valptr = valueptr(f)
return valptr[]
end
usefoo (generic function with 1 method)
julia> usefoo()
2
Caveat: object lifetime
Not quite...
julia> function usefoo()
f = Foo(2)
valptr = valueptr(f)
for i in 1:100
f2 = Foo(rand(100:200))
end
GC.gc()
return valptr[]
end
usefoo (generic function with 1 method)
julia> usefoo()
-1899136832
Caveat: object lifetime
Use GC.@preserve
julia> function usefoo()
f = Foo(2)
valptr = valueptr(f)
GC.@preserve f return valptr[]
end
usefoo (generic function with 1 method)
julia> usefoo()
2
We can add methods in the normal way:
module LibFoo
using CxxWrap
@wrapmodule "libfoo/build/lib/libfoo"
function __init__()
@initcxx
end
function extendedadd(a::Foo, b::Foo)
return valueptr(a)[] + valueptr(b)[]
end
export Foo, add, thisptr, thisref, thiscopy, valueptr
end
julia> a = Foo(1)
julia> b = Foo(2)
julia> extendedadd(a,b)
3
Problem with references:
julia> extendedadd(thisref(a), b)
ERROR: MethodError: no method matching
extendedadd(::CxxWrap.CxxWrapCore.CxxRef{Foo}, ::Main.LibFoo.FooAllocated)
Closest candidates are:
extendedadd(::Foo, ::Foo) at /Users/bjanssens/CloudStation/projects/julia/juliacon/2020/cxxwrap-talk/examples/Foo.jl:11
Stacktrace:
[1] top-level scope at REPL[7]:1
Problem with references and pointers
extendedadd(a::CxxRef{Foo}, b::Foo)
extendedadd(a::Foo, b::CxxRef{Foo})
extendedadd(a::CxxRef{Foo}, b::::CxxRef{Foo})
Secret wish 🙏
abstract type AbstractCxxRef{T} <: T end
The @cxxdereference
macro
module LibFoo
using CxxWrap
@wrapmodule "libfoo/build/lib/libfoo"
function __init__()
@initcxx
end
@cxxdereference function extendedadd(a::Foo, b::Foo)
return valueptr(a)[] + valueptr(b)[]
end
export Foo, add, thisptr, thisref, thiscopy, valueptr, extendedadd
end
The @cxxdereference
macro
julia> extendedadd(thisref(a),b)
3
julia> methods(extendedadd)
# 1 method for generic function "extendedadd":
[1] extendedadd(a::Union{CxxWrap.CxxWrapCore.CxxBaseRef{Foo},
CxxWrap.CxxWrapCore.SmartPointer{Foo},
Foo},
b::Union{CxxWrap.CxxWrapCore.CxxBaseRef{Foo},
CxxWrap.CxxWrapCore.SmartPointer{Foo},
Foo}) in Main.LibFoo
julia> @macroexpand @cxxdereference function extendedadd(a::Foo, b::Foo) return valueptr(a)[] + valueptr(b)[] end
:(function extendedadd(a::(CxxWrap.CxxWrapCore).reference_type_union(Foo), b::(CxxWrap.CxxWrapCore).reference_type_union(Foo); )
#= /home/bjanssens/.julia/packages/CxxWrap/ZOkSN/src/CxxWrap.jl:787 =#
a = (CxxWrap.CxxWrapCore).dereference_argument(a)
b = (CxxWrap.CxxWrapCore).dereference_argument(b)
#= REPL[9]:2 =#
return (valueptr(a))[] + (valueptr(b))[]
end)
dereference_argument(x) = x
dereference_argument(x::CxxBaseRef) = x[]
dereference_argument(x::SmartPointer) = x[]
Standard library support
std::vector
Usage example
#include <jlcxx/jlcxx.hpp>
#include <jlcxx/stl.hpp>
// ...
JLCXX_MODULE define_julia_module(jlcxx::Module& mod)
{
mod.add_type<Foo>("Foo")
//...
mod.method("sumfoos", [] (const std::vector<Foo>& vec)
{
int result = 0;
for (auto foo : vec)
{
result += *foo.valueptr();
}
return result;
});
}
Usage example
julia> foo_vec = StdVector(Foo.(1:10000));
julia> foo_vec[1]
Main.LibFoo.FooDereferenced(Ptr{Nothing} @0x000000000198d250)
julia> sumfoos(foo_vec)
50005000
Implementation
Module definition:
JLCXX_MODULE define_cxxwrap_stl_module(jlcxx::Module& stl)
{
jlcxx::stl::wrap_string(stl.add_type<std::string>("StdString", julia_type("CppBasicString")));
jlcxx::stl::wrap_string(stl.add_type<std::wstring>("StdWString", julia_type("CppBasicString")));
jlcxx::add_smart_pointer<std::shared_ptr>(stl, "SharedPtr");
jlcxx::add_smart_pointer<std::weak_ptr>(stl, "WeakPtr");
jlcxx::add_smart_pointer<std::unique_ptr>(stl, "UniquePtr");
jlcxx::stl::StlWrappers::instantiate(stl);
}
Module definition continued:
JLCXX_API void StlWrappers::instantiate(Module& mod)
{
m_instance.reset(new StlWrappers(mod));
m_instance->vector.apply_combination<std::vector, stltypes>(
stl::WrapVector());
m_instance->valarray.apply_combination<std::valarray, stltypes>(
stl::WrapValArray());
smartptr::apply_smart_combination<std::shared_ptr, stltypes>(mod);
smartptr::apply_smart_combination<std::weak_ptr, stltypes>(mod);
smartptr::apply_smart_combination<std::unique_ptr, stltypes>(mod);
}
Module definition continued:
JLCXX_API StlWrappers::StlWrappers(Module& stl) :
m_stl_mod(stl),
vector(stl.add_type<Parametric<TypeVar<1>>>(
"StdVector", julia_type("AbstractVector"))),
valarray(stl.add_type<Parametric<TypeVar<1>>>(
"StdValArray", julia_type("AbstractVector")))
{
}
Module definition continued:
struct WrapVector
{
template<typename TypeWrapperT>
void operator()(TypeWrapperT&& wrapped)
{
using WrappedT = typename TypeWrapperT::type;
using T = typename WrappedT::value_type;
wrapped.module().set_override_module(StlWrappers::instance().module());
wrapped.method("cppsize", &WrappedT::size);
wrapped.method("resize", [] (WrappedT& v, const cxxint_t s) {
v.resize(s);
});
wrapped.method("append", [] (WrappedT& v, jlcxx::ArrayRef<T> arr)
{
const std::size_t addedlen = arr.size();
v.reserve(v.size() + addedlen);
for(size_t i = 0; i != addedlen; ++i)
{
v.push_back(arr[i]);
}
});
wrapped.module().unset_override_module();
}
};
Hook to add a type on the fly:
template<typename T>
inline void apply_stl(jlcxx::Module& mod)
{
TypeWrapper1(mod, StlWrappers::instance().vector)
.apply<std::vector<T>>(WrapVector());
TypeWrapper1(mod, StlWrappers::instance().valarray)
.apply<std::valarray<T>>(WrapValArray());
}
Julia side: implement interfaces
Base.IndexStyle(::Type{<:StdVector}) = IndexLinear()
Base.size(v::StdVector) = (Int(cppsize(v)),)
Base.getindex(v::StdVector, i::Int) = cxxgetindex(v,i)[]
function Base.setindex!(v::StdVector{T}, val, i::Int) where {T}
cxxsetindex!(v, convert(T,val), i)
end
Adding new STL container types
std::valarray