Two ways to get C++ interface class from C API

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?

1 Like