Wrapping C++ class

Okay, I have a MWE that duplicates my issue: My wrapper correctly gives me the main application. The main application manages some other classes as members, but when I get those members back - they seem to not work. This example has a main class that has two member classes. One class just manages an integer, the other one manages a double. I wrote a C wrapper around the C++ library, and a C application that uses the wrapper works as expected - but there is something wrong with how I did it in Julia.

I squished all of the code together because it just made the question harder to read.
I expected to be able to run the julia wrapper with:

m = MainApp(1, 3.5)
a = m.borrowA()
a.get_id() # should yield 1, yields garbage

b = m.borrowB()
b.get_s() # should yield 3.5, yields garbage
C++ "library"
// stuff.h
class A {
    private:
        int id;

    public:
        A(int id);
        int get_id() const;
};

class B {
    private:
        double s;

    public:
        B(double s);
        double get_s() const;
};

class MainApp {
    private:
        A myA;
        B myB;

    public:
        A* borrowA();
        B* borrowB();

        MainApp(int one, double two);
};
// stuff.cpp
#include "stuff.h"

A::A(int id) : id(id) {};
int A::get_id() const { return id; }

B::B(double s) : s(s) {};
double B::get_s() const { return s; }

MainApp::MainApp(int one, double two) : myA(one), myB(two) {}
A *MainApp::borrowA() { return &myA; }
B *MainApp::borrowB() { return &myB; }
C wrapper
#include "wrap_stuff.h"
#include <new>

H_mainapp *MainApp_create(int a, double b) {
    return reinterpret_cast<H_mainapp *>(new (std::nothrow) MainApp(a, b));
}

void MainApp_destroy(H_mainapp *m) {
    MainApp *p = reinterpret_cast<MainApp *>(m);
    delete p;
}

H_A *MainApp_borrow_A(H_mainapp *m) {
    MainApp *p = reinterpret_cast<MainApp *>(m);
    return reinterpret_cast<H_A *>(p->borrowA());
}

H_B *MainApp_borrow_B(H_mainapp *m) {
    MainApp *p = reinterpret_cast<MainApp *>(m);
    return reinterpret_cast<H_B *>(p->borrowB());
}

int A_get_id(H_A *a) {
    A *aa = reinterpret_cast<A *>(a);
    return aa->get_id();
}

double B_get_s(H_B *b) {
    B *bb = reinterpret_cast<B *>(b);
    return bb->get_s();
}
Build Script for Libraries
#!/usr/bin/bash

g++ -shared -fPIC stuff.cpp -o libstuff.so

g++ -shared -fPIC wrap_stuff.cpp -o libstuff_wrapper.so \
    -Wl,-rpath=. -L. -lstuff

gcc test_c_interface.c -o test_c_interface \
    -Wl,-rpath=. -L. -lstuff_wrapper

Lastly,

Julia code to use the library

mutable struct MainApp
    p::Ptr{Cvoid}

    function MainApp(a, b)
        obj = @ccall "libstuff_wrapper.so".MainApp_create(a::Cint, b::Cdouble)::Ptr{Cvoid}
        if obj != C_NULL
            g = new(obj)
            finalizer(x -> ccall((:MainApp_destroy, "libstuff_wrapper.so"), Cvoid, (Ptr{Cvoid},), x.p), g)
        else
            error("Can't create MainApp");
        end
    end
end

function Base.getproperty(this::MainApp, s::Symbol)
    if s == :borrowA
        function()
            ah = @ccall "libstuff_wrapper.so".MainApp_borrow_A()::Ptr{Cvoid}
            @show ah
            A(ah)
        end
    elseif s == :borrowB
        function()
            B(@ccall "libstuff_wrapper.so".MainApp_borrow_B()::Ptr{Cvoid})
        end
    else
        getfield(this, s)
    end
end

#------------------------------------------------------------------------------#
mutable struct A
    p::Ptr{Cvoid}

    function A(x::Ptr{Cvoid})
        x == C_NULL && throw(error("argument cannot be C_NULL"))
        new(x)
    end
end

function Base.getproperty(this::A, s::Symbol)
    @show s
    if s == :get_id
        function()
            @ccall "libstuff_wrapper.so".A_get_id()::Cint
        end
    else
        getfield(this, s)
    end
end

#------------------------------------------------------------------------------#
mutable struct B
    p::Ptr{Cvoid}

    function B(x::Ptr{Cvoid})
        x == C_NULL && throw(error("argument cannot be C_NULL"))
        new(x)
    end
end

function Base.getproperty(this::B, s::Symbol)
    if s == :get_s
        function()
            @ccall "libstuff_wrapper.so".B_get_s()::Cdouble
        end
    else
        getfield(this, s)
    end
end

Ok - an even simpler one. This time, the class just has a double. There is a method on the class that returns that double. This seems to work.

This is the C++ "library"
// this is "stuff.h"
class MainApp {
    private:
        double g;

    public:
        double get_g() const;
        explicit MainApp(double g);
};
#include "stuff.h"
// this is "stuff.cpp"

MainApp::MainApp(double g) : g(g) {}
double MainApp::get_g() const { return g; }
This is the C wrapper
#pragma once
// this is "wrap_stuff.h"
#ifdef __cplusplus
#include "stuff.h"
#endif

typedef struct H_mainapp H_mainapp;

#ifdef __cplusplus
extern "C" {
#endif

H_mainapp *MainApp_create(double c);
void MainApp_destroy(H_mainapp *m);
double MainApp_get_g(H_mainapp *m);

#ifdef __cplusplus
} // extern

#include "wrap_stuff.h"
#include <new>
// This is "wrap_stuff.cpp"
H_mainapp *MainApp_create(double g) {
    return reinterpret_cast<H_mainapp *>(new (std::nothrow) MainApp(g));
}

double MainApp_get_g(H_mainapp *m) {
    MainApp *p = reinterpret_cast<MainApp *>(m);
    return p->get_g();
}

void MainApp_destroy(H_mainapp *m) {
    MainApp *p = reinterpret_cast<MainApp *>(m);
    delete p;
}

And this is the Julia code that uses the wrapper.


mutable struct MainApp
    p::Ptr{Cvoid}

    function MainApp(a)
        obj = @ccall "libstuff_wrapper.so".MainApp_create(a::Cdouble)::Ptr{Cvoid}
        if obj != C_NULL
            g = new(obj)
            finalizer(x -> ccall((:MainApp_destroy, "libstuff_wrapper.so"), Cvoid, (Ptr{Cvoid},), x.p), g)
        else
            error("Can't create MainApp");
        end
    end
end

propertynames(::MainApp, private=false) = (:get_g,)

function Base.getproperty(this::MainApp, s::Symbol)
    if s == :get_g
        function()
            @ccall "libstuff_wrapper.so".MainApp_get_g(this.p::Ptr{Cvoid})::Cdouble
        end
    else
        getfield(this, s)
    end
end

Why did you do that? Did you consider:

(or CxxInterface.jl directly; "CxxWrap.jl that “is probably the most mature option”)?

I realize it’s common to make a C interface, since C++ is famously hard to call from other languages. And that is of course valid if you want to call from a C application (only), i.e. as you’re doing “that uses the wrapper works as expected”, but was that just for a test case? I’m just trying to be helpful, I discovered CxxCall.jl recently and a YouTube video stating it’s the easiest way.

[If you want your C++ code callable from many languages, e.g. Python (and or C itself), then it’s valuable to have the wrapper on the C side, i.e. a neutral language. Python people might use Boost Python (or some other alternative), and I’m curious canyou use such wrappers directly from Julia without Python’s involvement? And same, can you use C++ code made to work for R, directly from Julia without R or RCall.jl?]

Even if you make your code seemingly usable only with Julia, somehow, e.g. the wrapper on the Julia side, then that doesn’t strictly rule out using the C++ code from e.g. Python, see JuliaCall/PythonCall.jl.

EDIT: @frylock FYI also found GitHub - grasph/wrapit: Automatization of C++--Julia wrapper generation

2 Likes

I have looked at a couple of the wrapper generator libraries, but what really messes up life for me is that the library that I really want to wrap throws exceptions - and that just breaks everything. I sort of figured out a way to handle the exceptions at the C layer, but my additional complication has me confused about writing a wrapper using the existing packages.

I’m just trying to distil things down to the simplest thing that has my problem in it - and that problem seems to be that getting pointers to classes that are members of another class.

I will revisit CxxWrap.jl though

Ok - I figured this out, but in doing so, I make this a poster child for automated wrapper generators: I missed some function arguments when I hand wrote the example. TL;DR the @ccall parts should have referenced this.p::Ptr{Cvoid}. I naively expected that an error would have been thrown for wrong arguments.

the longer version
--- julia_test.jl       2023-08-07 16:35:21.170275259 -0500
+++ julia_test.jl.ori   2023-08-07 16:34:55.630275688 -0500
@@ -15,13 +15,13 @@
 function Base.getproperty(this::MainApp, s::Symbol)
     if s == :borrowA
         function()
-            ah = @ccall "libstuff_wrapper.so".MainApp_borrow_A(this.p::Ptr{Cvoid})::Ptr{Cvoid}
+            ah = @ccall "libstuff_wrapper.so".MainApp_borrow_A()::Ptr{Cvoid}
             @show ah
             A(ah)
         end
     elseif s == :borrowB
         function()
-            B(@ccall "libstuff_wrapper.so".MainApp_borrow_B(this.p::Ptr{Cvoid})::Ptr{Cvoid})
+            B(@ccall "libstuff_wrapper.so".MainApp_borrow_B()::Ptr{Cvoid})
         end
     else
         getfield(this, s)
@@ -42,7 +42,7 @@
     @show s
     if s == :get_id
         function()
-            @ccall "libstuff_wrapper.so".A_get_id(this.p::Ptr{Cvoid})::Cint
+            @ccall "libstuff_wrapper.so".A_get_id()::Cint
         end
     else
         getfield(this, s)
@@ -62,7 +62,7 @@
 function Base.getproperty(this::B, s::Symbol)
     if s == :get_s
         function()
-            @ccall "libstuff_wrapper.so".B_get_s(this.p::Ptr{Cvoid})::Cdouble
+            @ccall "libstuff_wrapper.so".B_get_s()::Cdouble
         end
     else
         getfield(this, s)

@frylock Do the structs need to be mutable?

Oh, it has to be mutable to be finalized…

I wasn’t exactly sure why - but they didn’t work without making them mutable.

Btw, did you eventually determine that CxxWrap is a better choice? I find it unnecessarily complex and non-native.

I never revisited it, and I agree that CxxWrap seemed very complex - we had a solution that sort of worked (i.e., the above) and pushed off any upgrades to it until later. That might be long while …

There was a suggestion to use “wrapit” above, based on the language mix in GitHub, it appears to be written mostly in C++ …

@frylock I have some memory leak due to this C++ wrapping. I’m certain the memory leak is happening at the Julia/C interface layer. Did you experience this too?

Oh no - I hadn’t noticed, but I hadn’t checked. My application tends to run quickly and then stop … I’ll try to check it out, and if I find anything I’ll post back here.

Any chance you could post a small example somewhere that demonstrates the leak?

When I get a chance, I might try this out: Valgrind with Julia.