CxxWrap.jl
Package to make C++ libraries available in Julia
Install / Use
/learn @JuliaInterop/CxxWrap.jlREADME
CxxWrap
This package aims to provide a Boost.Python-like wrapping for C++ types and functions to Julia. The idea is to write the code for the Julia wrapper in C++, and then use a one-liner on the Julia side to make the wrapped C++ library available there.
The mechanism behind this package is that functions and types are registered in C++ code that is compiled into a dynamic library. This dynamic library is then loaded into Julia, where the Julia part of this package uses the data provided through a C interface to generate functions accessible from Julia. The functions are passed to Julia either as raw function pointers (for regular C++ functions that don't need argument or return type conversion) or std::functions (for lambda expressions and automatic conversion of arguments and return types). The Julia side of this package wraps all this into Julia methods automatically.
For this to work, the user must have a C++ compiler installed which supports C++17 (e.g. GCC 7, clang 5; for macOS users that means Xcode 9.3).
What's the difference with Cxx.jl?
With Cxx.jl it is possible to directly access C++ using the @cxx macro from Julia.
So when facing the task of wrapping a C++ library in a Julia package, authors now have two options:
- Use Cxx.jl to write the wrapper package in Julia code (much like one uses
ccallfor wrapping a C library) - Use CxxWrap to write the wrapper completely in C++ (and one line of Julia code to load the .so)
Boost.Python also uses the latter (C++-only) approach, so translating existing Python bindings based on Boost.Python may be easier using CxxWrap.
Features
- Support for C++ functions, member functions and lambdas
- Classes with single inheritance, using abstract base classes on the Julia side
- Trivial C++ classes can be converted to a Julia isbits immutable
- Template classes map to parametric types, for the instantiations listed in the wrapper
- Automatic wrapping of default and copy constructor (mapped to
copy) if defined on the wrapped C++ class - Facilitate calling Julia functions from C++
Installation
Just like any registered package, in pkg mode (] at the REPL)
add CxxWrap
CxxWrap v0.10 and later depends on the libcxxwrap_julia_jll JLL package to manage the libcxxwrap-julia binaries. See the libcxxwrap-julia Readme for information on how to build this library yourself and force CxxWrap to use your own version.
Boost Python Hello World example
Let's try to reproduce the example from the Boost.Python tutorial.
Suppose we want to expose the following C++ function to Julia in a module called CppHello:
std::string greet()
{
return "hello, world";
}
Using the C++ side of CxxWrap, this can be exposed as follows:
#include "jlcxx/jlcxx.hpp"
JLCXX_MODULE define_julia_module(jlcxx::Module& mod)
{
mod.method("greet", &greet);
}
Once this code is compiled into a shared library (say libhello.so) it can be used in Julia as follows:
# Load the module and generate the functions
module CppHello
using CxxWrap
@wrapmodule(() -> joinpath("path/to/built/lib","libhello"))
function __init__()
@initcxx
end
end
# Call greet and show the result
@show CppHello.greet()
The code for this example can be found in [hello.cpp] in the examples directory of the libcxxwrap-julia project and test/hello.jl.
Note that the __init__ function is necessary to support precompilation, which is on by default since Julia 1.0.
Compiling the C++ code
The recommended way to compile the C++ code is to use CMake to discover libcxxwrap-julia and the Julia libraries.
A full example is in the testlib directory of libcxxwrap-julia.
The following sequence of commands can be used to build:
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/path/to/libcxxwrap-julia-prefix /path/to/sourcedirectory
cmake --build . --config Release
The path for CMAKE_PREFIX_PATH can be obtained from Julia using:
julia> using CxxWrap
julia> CxxWrap.prefix_path()
Windows and MSVC
The default binaries installed with CxxWrap are cross-compiled using GCC, and thus incompatible with Visual Studio C++ (MSVC).
In MSVC 2019, it is easy to check out libcxxwrap-julia from git, and then build it and the wrapper module from source.
Details are provided in the README.
Module entry point
Above, we defined the module entry point as a function JLCXX_MODULE define_julia_module(jlcxx::Module& mod).
In the general case, there may be multiple modules defined in a single library, and each should have its own entry point, called within the appropriate module:
JLCXX_MODULE define_module_a(jlcxx::Module& mod)
{
// add stuff for A
}
JLCXX_MODULE define_module_b(jlcxx::Module& mod)
{
// add stuff for B
}
In Julia, the name of the entry point must now be specified explicitly:
module A
using CxxWrap
@wrapmodule(() -> "mylib.so",:define_module_a)
end
module B
using CxxWrap
@wrapmodule(() -> "mylib.so",:define_module_b)
end
In specific cases, it may also be necessary to specify dlopen flags such as RTLD_GLOBAL.
These can be supplied in a third, optional argument to @wrapmodule, e.g:
@wrapmodule(() -> CxxWrapCore.libcxxwrap_julia_stl, :define_cxxwrap_stl_module, Libdl.RTLD_GLOBAL)
More extensive example and function call performance
A more extensive example, including wrapping a C++11 lambda and conversion for arrays can be found in examples/functions.cpp and test/functions.jl.
This test also includes some performance measurements, showing that the function call overhead is the same as using ccall on a C function if the C++ function is a regular function and does not require argument conversion.
When std::function is used (e.g. for C++ lambdas) extra overhead appears, as expected.
Exposing classes
Consider the following C++ class to be wrapped:
struct World
{
World(const std::string& message = "default hello") : msg(message){}
void set(const std::string& msg) { this->msg = msg; }
std::string greet() { return msg; }
std::string msg;
~World() { std::cout << "Destroying World with message " << msg << std::endl; }
};
Wrapped in the entry point function as before and defining a module CppTypes, the code for exposing the type and some methods to Julia is:
types.add_type<World>("World")
.constructor<const std::string&>()
.method("set", &World::set)
.method("greet", &World::greet);
Here, the first line just adds the type.
The second line adds the non-default constructor taking a string.
Finally, the two method calls add member functions, using a pointer-to-member.
The member functions become free functions in Julia, taking their object as the first argument.
This can now be used in Julia as
w = CppTypes.World()
@test CppTypes.greet(w) == "default hello"
CppTypes.set(w, "hello")
@test CppTypes.greet(w) == "hello"
The manually added constructor using the constructor function also creates a finalizer.
This can be disabled by adding the argument jlcxx::finalize_policy::no:
types.add_type<World>("World")
.constructor<const std::string&>(jlcxx::finalize_policy::no);
The add_type function actually builds two Julia types related to World.
The first is an abstract type:
abstract type World end
The second is a mutable type (the "allocated" or "boxed" type) with the following structure:
mutable struct WorldAllocated <: World
cpp_object::Ptr{Cvoid}
end
This type needs to be mutable, because it must have a finalizer attached to it that deletes the held C++ object.
This means that the variable w in the above example is of concrete type WorldAllocated and letting it go out of scope may trigger the finalizer and delete the object.
When calling a C++ constructor, it is the responsibility of the caller to manage the lifetime of the resulting variable.
The above types are used in method generation as follows, considering for example the greet method taking a World argument:
greet(w::World) = ccall($fpointer, Any, (Ptr{Cvoid}, WorldRef), $thunk, cconvert(WorldRef, w))
Here, the cconvert from WorldAllocated to WorldRef is defined automatically when creating the type.
Warning: The ordering of the C++ code matters: types used as function arguments or return types must be added before they are used in a function.
The full code for this example and more info on immutables and bits types can be found in examples/types.cpp and test/types.jl.
Checking for null
Values returned from C++ can be checked for being null using the isnull function.
Setting the module to which methods are added
It is possible to add methods directly to e.g. the Julia Base module, using set_override_module.
After calling this, all methods will be added to the specified module.
To revert to the default behavior of adding methods to the current module, call unset_override_module.
mod.add_type<A>("A", jlcxx::julia_type("AbstractFloat", "Base"))
.constructor<double>();
mod.set_override_module(mod.julia_module());
// == will be in the wrapped module:
mod.method("==", [](A& a, A& b) { return a == b; });
mod.set_override_module(
Related Skills
node-connect
345.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
104.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
345.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
