Cxx Wrap

Aims to be like Boost

  • The Boost C++ Libraries are a set of libraries for the C++ programming language that provides support for tasks and structures such as linear algebra, pseudorandom number generation, multithreading, image processing, regular expressions, and unit testing. It contains 164 individual libraries…

    the idea

    • write C++ code to wrap interfaces with Julia
      • then one-liner of Julia access wrapped code

    the mechanism behind this package

    • functions and types are registered in C++
      • that is compiled into a DLL -> the DLL is loaded into Julia
        • Julia uses the data provided through a C interface
          • to generate functions accessible from Julia the functions are passed as RAW FUNCTION POINTERS, for regular cpp funcs that do not need argument or return type conversion, or std::functions, for lambda expressions and auto-conversion of arguments and return types.
            • Julia wraps all this automatically
          must have C++ compiler installed supporting C++17

Hello World Example

std::string greet()
  {
    return "hello, world";
  }

#include "jlcxx/jlcxx.hpp"

JLCXX_MODULE define_julia_module(jlcxx::Module& mod)
  {
    mod.method("greet", &greet);
  }
  • compile the code into shared lib `libhello.so`

recommended way to compile the C++ code

  • use CMake to discover `libcxxwrap-julia` and the julia libs
project(TestLib)

cmake_minimum_required(VERSION 3.5)
set(CMAKE_MACOSX_RPATH 1)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")

find_package(JlCxx)
get_target_property(JlCxx_location JlCxx::cxxwrap_julia LOCATION)
get_filename_component(JlCxx_location ${JlCxx_location} DIRECTORY)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib;${JlCxx_location}")

message(STATUS "Found JlCxx at ${JlCxx_location}")

add_library(testlib SHARED testlib.cpp)

target_link_libraries(testlib JlCxx::cxxwrap_julia)

install(TARGETS
  testlib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION lib)

the following commands can be used to to build the CMakeLists.txt

mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/path/to/libcxxwrap-julia-prefix /path/to/sourcedirectory
cmake --build . --config Release

path for CMAKEPREFIXPATH can be obtained using `CxxWrap.prefixpath()` in Julia

module CppHello
    using CxxWrap
    @wrapmodule(() -> joinpath("path/to/libbuild","libhello"))

    function __init__()
        @initcxx
    end
end

# try it out
@show CppHello.greet()

Module entry point

JLCXXMODULE definejuliamodule(jlcxx::Module& mod)

  • defines module entry point
    • each module has its own entry point

      JLCXX_MODULE define_module_a(jlcxx::Module& mod)
      {
        // add stuff for A
      }
      
      JLCXX_MODULE define_module_b(jlcxx::Module& mod)
      {
        // add stuff for B
      }
      
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 RTLDGLOBAL. These can be supplied in a third, optional argument to @wrapmodule,

Advanced Examples and Notes

the function call overhead is the same as `ccall` on a C func if the C++ func is a regular func and does not require argument conversion

  • when `std::function` is used extra overhead is expected…

Exposing Classes

struct World
{
  World(const std::string& message = "hello") : msg(message){}
  void set(const std::string& msg) {this->msg = msg; }
  std::string greet() {return msg; }
  std::string msg;
  ~World() { std::cout << "destry world" << msg << std::endl; }
};

JLCXX_MODULE define_module_world(jlcxx::Module& mod)
  {
    types.add_type<World>("World")
        .constructor<const std::string&>()
        .method("set", &World::set)
        .method("greet", &World::greet);
  }
// `constructor` creates a finalizer
// disable with the arg `jlcxx::finalize_policy::no`
types.add_type<World>("World")
  .constructor<const std::string&>(jlcxx::finalize_policy::no);
w = CppTypes.World()
CppTypes.greet(w) # "hello"
CppTypes.set(w, "hi")

# the add_type function actually builds 2 julia types related to World
abstract type World end
# boxed type
mutable struct WorldAllocated <: World
    cpp_object::Ptr{Cvoid}
end

# variable w may get deleted when out of scope
# the caller must manage the lifetime the of the result
greet(w::World) = ccall($fpointer, Any, (Ptr{Cvoid}, WorldRef), $thunk, cconvert(WorldRef, w))
# here cconvert from WorldAllocated to WorldRef is defined automatically

WARNING: the ordering of C++ code matters!!! types used as function arguments or return types must be added before they are used in a function

Average, Sum, and Multiple Averages

#include <iostream>
#include <vector>
#include <string>

#include "jlcxx/jlcxx.hpp"
#include "jlcxx/functions.hpp"
#include "jlcxx/stl.hpp"

using namespace std;
// primitive data passed and retrieved
double cpp_avg(int a, int b) {
  return (double) (a+b)/2;
}

string cpp_sum(std::vector< double > data) {
  double total = 0.0;
  double nelems = data.size();
  for (int i = 0; i< nelems; i++){
    total += data[i];
  }
  std::stringstream ss;
  ss << "the sum is " << total << endl;
  return ss.str();
}


std::vector< double > cpp_mult_avgs(std::vector < std::vector< double >> data) {
  std::vector <double> avgs;
  for (int i = 0; i < data.size(); i++){
    double isum = 0.0;
    double ni = data[i].size();
    for (int j = 0; j < data[i].size(); j++){
      isum += data[i][j];
    }
    avgs.push_back(isum/ni);
  }
  return avgs
}

JLCXX_MODULE define_julia_module(jlcxx::Module& mod) {
  mod.method("cpp_avg", &cpp_avg);
  mod.method("cpp_sum", &cpp_sum);
  mod.method("cpp_mult_avgs", &cpp_mult_avgs);
}
# Compile
cmd = `g++ --std=c++20 -shared -fPIC -o libcpp.so -I $julia_include_path -I $cxx_include_path  libcpp.cpp`
run(cmd)
# Generate the functions for Julia
# Once the lib is wrappd you can't wrap it again nor modify the C++ code, you need to restart Julia
@wrapmodule(() -> joinpath(pwd(),"libcpp"))
# Call the functions
cpp_hello() # Prints "Hello world from a C++ function"
avg        = cpp_average(3,4) # 3.5
data_julia = [1.5,2.0,2.5]
data_sum   = cpp_sum(StdVector(data_julia)) # Returns "The sum is 6"
typeof(data_sum)
typeof(data_sum) <: AbstractString
data_julia = [[1.5,2.0,2.5],[3.5,4.0,4.5]]
data       = StdVector(StdVector.(data_julia))
data_avgs  = cpp_multiple_averages(data) # [2.0, 4.0]
typeof(data_avgs)
typeof(data_avgs) <: AbstractArray
data_avgs[1]

wrapping GeographicLib C++ for GeographicModels.jl

using GeographicLib in C++

#include <GeographicLib.LambertConformalConic.hpp>
using namespace GeographicLib;
  • build code with CMake
find_package (GeographicLib REQUIRED)
include_directories (${GeographicLib_INCLUDE_DIRS})
add_executable (program source1.cpp source2.cpp)
target_link_libraries (program ${GeographicLib_LIBRARIES})
mkdir BUILD
cd BUILD
cmake -D CMAKE_INSTALL_PREFIX="/tmp/testgeog" \
  -S . -B BUILD
make -C BUILD -j4