Search code examples
c#cswig

How to call a C# method from C using SWIG?


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?


Solution

  • 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.