I have been searching for some explanations/code snippets that illustrate how to call a C# method from C using SWIG. The best that I could find is this: Callback from C++ to C# using SWIG. Alas, this is for C++ and not C. How can we achieve the same functionality but for C?
You can do this using SWIG to help (a little) still, but fundamentally if you're using a C compiler you can't enable SWIG directors, which the example you linked to relies upon:
test.i:1: Error: Directors are not supported for C code and require the -c++ option
So you'll have to rebuild some parts of that functionality in C in order to do this. I built an example of a C++ enabled director/callbacks setup and used that as a basis to build my understanding of the C# mechanics. Let's work this through backwards, starting with the test we want to run:
public class TestImpl : CallbackInterface {
public override void callback() {
System.Console.WriteLine("handling event...");
}
public static void Main() {
CallbackInterface cb = new TestImpl();
test.test_runner(cb);
}
}
We want to define an abstract class, CallbackInterface
that we can pass instances around and have our C code call the C# callback
implementation.
Here's my .i SWIG interface file:
%module test
%{
#include <assert.h>
%}
%typemap(csclassmodifiers) struct CallbackInterface "public abstract class";
%typemap(cscode) struct CallbackInterface %{
public delegate void callback0(); // [2]
public CallbackInterface() : this(null) { // [3]
connect();
}
private void connect() {
setCallback(new callback0(callback));
}
public abstract void callback();
%}
// [4]
%typemap(cstype) cb0 "CallbackInterface.callback0";
%typemap(imtype) cb0 "CallbackInterface.callback0";
%typemap(csin) cb0 "$csinput";
%ignore CallbackInterface::cb;
%extend struct CallbackInterface {
CallbackInterface(cb0 cb) { // [3]
struct CallbackInterface *ret = malloc(sizeof *ret);
ret->cb = cb;
return ret;
}
void setCallback(cb0 cb) {
$self->cb = cb;
}
}
%inline %{
typedef void (*cb0)();
struct CallbackInterface {
cb0 cb; // [1]
};
static void test_runner(struct CallbackInterface *interface) {
assert(interface->cb);
fprintf(stderr, "Doing test, pointer=%p, cb=%p\n", interface, interface->cb);
interface->cb();
}
%}
From investigating the normal directors implementation in C# it seems that the C++ layer is working with function pointers through the pinvoke interface. So we define a struct in our interface at 1, which has a single member, a function pointer.
In order to actually get a function pointer out of our C# code we need to use a delegate, we've set things up for this at 2, which is C# code that's going to be part of our CallbackInterface
class.
In SWIG we've "extended" (but just using C still) our struct to provide a constructor, which takes a delegate as an argument at 3. This is a little bit weird, because we only pass null
in here, but that works as a handy overload for our default constructor later and ensures that we do at least initalise the cb
member of the struct too when we create an instance, with a delegated constructor call at 3. (Aside: I originally tried to create the delegate here, but this is a static context it would seem so you can't do that at this point). Then inside the body of the default constructor we call our private connect
method that sets up a delegate and uses our %extend
setCallback
method to actually modify the struct.
If you were so inclined you could provide an overload of setCallback
as well, such that C function pointers can be set up as handlers, but that's an added extra especially given the C limitation means you can't write classes at all. With some extra typemaps at 4 to make sure that the delegate gets passed through to the C as a function pointer we're in business and so this Makefile below can run and test things for us:
.PHONY: run
all: run
test_wrap.c: test.i
swig -csharp -Wall -debug-tmsearch test.i
test.cs: test_wrap.c
CallbackInterface.cs: test_wrap.c
testPINVOKE.cs: test_wrap.c
test.exe: test.cs runme.cs CallbackInterface.cs testPINVOKE.cs
mcs $^
libtest.so: test_wrap.c
gcc -shared -Wall -Wextra $^ -o $@ -fPIC
run: test.exe libtest.so
LD_LIBRARY_PATH=. ./test.exe
With Mono on Ubuntu at least that does work as expected.