Consider simple C++ interface class:
File `shape.h`
// C++ Interface Class
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual ~Shape() {}
};
// Concrete implementations of the interface
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14 * radius * radius; }
double perimeter() const override { return 2 * 3.14 * radius; }
};
class Square : public Shape {
private:
double side;
public:
Square(double s) : side(s) {}
double area() const override { return side * side; }
double perimeter() const override { return 4 * side; }
};
Since Cxx.jl
in obsolete, so you cannot generate julia types from C++, and CxxWrap.jl
means you have to make changes on C++ side, so I’ve made two C library wrappers for this C++ interface:
1) Opaque pointer with every method exported
File `shapes_lib1.cpp`:
#include "shapes.h"
// C API for Shape interface
extern "C" {
typedef struct Shape* ShapeHandle;
ShapeHandle Shape_create_circle(double radius) {
return reinterpret_cast<ShapeHandle>(new Circle(radius));
}
ShapeHandle Shape_create_square(double side) {
return reinterpret_cast<ShapeHandle>(new Square(side));
}
double Shape_area(ShapeHandle handle) {
return reinterpret_cast<Shape*>(handle)->area();
}
double Shape_perimeter(ShapeHandle handle) {
return reinterpret_cast<Shape*>(handle)->perimeter();
}
void Shape_destroy(ShapeHandle handle) {
delete reinterpret_cast<Shape*>(handle);
}
}
Julia usage example
File `shapes_lib1.jl`
using Libdl
lib1_path = abspath("build/libshapes_lib1.dll")
lib1 = Libdl.dlopen(lib1_path, Libdl.RTLD_GLOBAL)
# cr = ccall((:Shape_create_circle, lib1_path), Ptr{Cvoid}, (Cdouble,), 10) - the same
cr = @ccall lib1_path.Shape_create_circle(5::Cdouble)::Ptr{Cvoid}
sq = @ccall lib1_path.Shape_create_square(5::Cdouble)::Ptr{Cvoid}
@ccall lib1_path.Shape_area(cr::Ptr{Cvoid})::Cdouble
@ccall lib1_path.Shape_area(sq::Ptr{Cvoid})::Cdouble
@ccall lib1_path.Shape_perimeter(cr::Ptr{Cvoid})::Cdouble
@ccall lib1_path.Shape_perimeter(sq::Ptr{Cvoid})::Cdouble
@ccall lib1_path.Shape_destroy(cr::Ptr{Cvoid})::Cvoid
@ccall lib1_path.Shape_destroy(sq::Ptr{Cvoid})::Cvoid
Libdl.dlclose(lib1)
2) Convert C++ interface into a C struct with instance and methods pointers and export its constructors
File `shapes_lib2.cpp`
#include "shapes.h"
// C API for Shape interface
extern "C" {
typedef struct {
const void* instance;
double (*area)(const void*);
double (*perimeter)(const void*);
void (*destroy)(const void*);
} ShapeHandle;
ShapeHandle Shape_create_circle(double radius) {
Circle* circle = new Circle(radius);
ShapeHandle handle;
handle.instance = circle;
handle.area = [](const void* inst) -> double {
return static_cast<const Circle*>(inst)->area();
};
handle.perimeter = [](const void* inst) -> double {
return static_cast<const Circle*>(inst)->perimeter();
};
handle.destroy = [](const void* inst) {
delete static_cast<const Circle*>(inst);
};
return handle;
}
ShapeHandle Shape_create_square(double side) {
Square* square = new Square(side);
ShapeHandle handle;
handle.instance = square;
handle.area = [](const void* inst) -> double {
return static_cast<const Square*>(inst)->area();
};
handle.perimeter = [](const void* inst) -> double {
return static_cast<const Square*>(inst)->perimeter();
};
handle.destroy = [](const void* inst) {
delete static_cast<const Square*>(inst);
};
return handle;
}
}
Julia usage example:
File `shapes_lib2.jl`
using Libdl
lib2_path = abspath("build/libshapes_lib2.dll")
lib2 = Libdl.dlopen(lib2_path, Libdl.RTLD_GLOBAL) # <4>
mutable struct ShapeHandle
instance::Ptr{Cvoid}
area::Ptr{Cvoid}
perimeter::Ptr{Cvoid}
destroy::Ptr{Cvoid}
end
cr = @ccall lib2_path.Shape_create_circle(5::Cdouble)::ShapeHandle
sq = @ccall lib2_path.Shape_create_square(5::Cdouble)::ShapeHandle
@ccall $(cr.area)(cr.instance::Ptr{Cvoid})::Cdouble
@ccall $(sq.area)(sq.instance::Ptr{Cvoid})::Cdouble
@ccall $(cr.perimeter)(cr.instance::Ptr{Cvoid})::Cdouble
@ccall $(sq.perimeter)(sq.instance::Ptr{Cvoid})::Cdouble
@ccall $(cr.destroy)(cr.instance::Ptr{Cvoid})::Cdouble
@ccall $(sq.destroy)(sq.instance::Ptr{Cvoid})::Cdouble
# @ccall $(unsafe_load(c_ptr).perimeter)(unsafe_load(c_ptr).instance::Ptr{Cvoid})::Cdouble
Libdl.dlclose(lib2)
First option looks more compact, but I need to call every method from a shared library.
Second option looks more verbose - need to copy interface into a handle structure, but then I just get the interface structure and then work directly with function pointers (possibly to a concrete class).
Are there any other considerations to choose one or another?