Cross-Platform interop of C# and C/C++(CMake+PInvoke)

Cross-Platform interop of C# and C/C++(CMake+PInvoke)

Sometimes we want to wrap some methods in C++ because of its high performace at computation. However, it's more convenient for us to develop our program using a managed language, such as CSharp. So what we need to do is to wrap some methods in C++ and call them from C#.

Besides, we often want to write only one piece of code while applying it in multi-platforms.

This blog will tell you how to construct cross-platform interop bwtween C/C++ and C# by CMake.

Steps

  1. Wrap some methods in C/C++ and give an implemention.
  2. Write CMake Files and make them with host-compile or cross-compile, which depends on you.
  3. Import the danmic libraray we generated at step 2 and call them.

Environment

I compile the C/C++ library on Ubuntu 20.04 LTS, x86_64. My target platform of CMake is host and Windows x86_64.

I use .NET 5.0/6.0 runtime to call the dynamic library generated.

Wrap some methods with pure C

The directory structure is:

.
├── CMakeLists.txt
└── lib
    ├── CMakeLists.txt
    ├── hello.c
    └── hello.h

hello.h

#ifndef HELLO_H
#define HELLO_H

#include <stdio.h>

void HelloFunc();
int add(int a, int b);
#endif

hello.c

#include "hello.h"
void HelloFunc()
{
    printf("Hello World\n");
}

int add(int a, int b)
{
    return a + b;
}

lib/CMakeLists.txt

SET(LIBHELLO_SRC hello.cpp)

ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

CMakeLists.txt

PROJECT(PInvoke)

ADD_SUBDIRECTORY(lib)

Then, go to the root directory of our project and run the commands below.

mkdir build
cd build
cmake ..
make

There will be an file called libhello.so, which is exactly what we want.(If you are in Windows, it should be libhello.dll.)

Call the library from C

Let's create a command line project of .NET.

Program.cs

using System.Runtime.InteropServices;

[DllImport(@"libhello", EntryPoint = "HelloFunc")]
extern static void HelloFunc();

[DllImport(@"libhello")]
extern static int add(int a, int b);

int t = add(2, 3);
Console.WriteLine(t);

HelloFunc();

We use "EntryPoint" when the entry of a method of our library is different from the name of method we declared.

Note: the libhello.so(or libhello.dll) should be put in the directory where the executable file of our .NET program is generated. Mine is bin/Debug/net6.0.

Now let's run it and we will see the result!

How to deal with pointer?

There is no difference in the C/C++ files. What is different is we need to use unsafe block in C#. Remember to allow that by setting <AllowUnsafeBlocks>true</AllowUnsafeBlocks> in the.csproj file(vscode) or enabling it in project/build/general(VS).

hello.h

#ifndef HELLO_H
#define HELLO_H

#include <stdio.h>
void HelloFunc();
int add(int a, int b);
void print(int *p, int len);
#endif

hello.c

#include "hello.h"
void HelloFunc()
{
    printf("Hello World\n");
}

int add(int a, int b)
{
    return a + b;
}

void print(int *p, int len)
{
    for (int i = 0; i < len; i++)
    {
        printf("%d ", p[i]);
        p[i] *= 2;
    }
    printf("\n");
}

Program.cs

using System.Runtime.InteropServices;


[DllImport(@"libhello", EntryPoint = "HelloFunc")]
extern static void HelloFunc();

[DllImport(@"libhello")]
extern static int add(int a, int b);

unsafe{
    [DllImport(@"libhello")]
    extern static int print(int *p, int len);

    int[] a = new int[10];
    for (int i = 0; i < 10; i++){
        a[i] = add(i, i);
    }
    fixed(int* p = &(a[0])){
        print(p, 10);
    }
    for (int i = 0; i < 10; i++){
        Console.Write(a[i] + ", ");
    }
    Console.Write("\n");
}

int t = add(2, 3);
Console.WriteLine(t);

HelloFunc();

What we do in C/C++ library is print the values of the array and change them. Recompile the CMake project and run the .NET program, you will see the result.

How to deal with struct

There is also nothing different in C/C++. What we need to remember when programming in C# is that

  • We need to specify the structure layout so that the members of the structure is arranged in order.
  • We need to declare the members of the structure as public.
  • The content of structure of C/C++ and C# should be the same.

hello.h

#ifndef HELLO_H
#define HELLO_H

#include <stdio.h>

typedef struct vector3
{
    float x, y, z;
} vector;

void HelloFunc();
int add(int a, int b);
void print(int *p, int len);

vector mul(vector *rls, vector *rhs);
#endif

hello.c

#include "hello.h"
void HelloFunc()
{
    printf("Hello World\n");
}

int add(int a, int b)
{
    return a + b;
}

void print(int *p, int len)
{
    for (int i = 0; i < len; i++)
    {
        printf("%d ", p[i]);
        p[i] *= 2;
    }
    printf("\n");
}

vector mul(vector *rls, vector *rhs)
{
    vector v;
    v.x = rls->x * rhs->x;
    v.y = rls->y * rhs->y;
    v.z = rls->z * rhs->z;
    return v;
}

Program.cs

using System.Runtime.InteropServices;


[DllImport(@"libhello", EntryPoint = "HelloFunc")]
extern static void HelloFunc();

[DllImport(@"libhello")]
extern static int add(int a, int b);

unsafe{
    [DllImport(@"libhello")]
    extern static int print(int *p, int len);

    [DllImport(@"libhello")]
    extern static vector3 mul(vector3* lhs, vector3* rhs);

    int[] a = new int[10];
    for (int i = 0; i < 10; i++){
        a[i] = add(i, i);
    }
    fixed(int* p = &(a[0])){
        print(p, 10);
    }
    for (int i = 0; i < 10; i++){
        Console.Write(a[i] + ", ");
    }
    Console.Write("\n");

    vector3 l, r;
    l.x = 1;
    l.y = 2;
    l.z = 3;
    r.x = 2;
    r.y = 3;
    r.z = 4;

    vector3 v = mul(&l, &r);
    Console.WriteLine($"x: {v.x}, y: {v.y}, z: {v.z}");
}

int t = add(2, 3);
Console.WriteLine(t);

HelloFunc();

[StructLayout(LayoutKind.Sequential)]
struct vector3{
    public float x, y, z;
}

Aslo, recompile the library and run the C# program, you will get the result!

How to deal with callback?

Sometimes we want to let our C# program "know" that the C/C++ library has stepped into a certain position and manipulate something in C#. In this condition, we use delegate in C# and function pointer in C/C++ to deal with it.

firstly, we define a delegate and offer an implemention of it.

Program.cs

using System.Runtime.InteropServices;

[DllImport(@"libhello")]
extern static int callBackTest(printFromCSharp cSharp, int value);

printFromCSharp callback = printFromCSharpImpl;
callBackTest(callback, 10);

static void printFromCSharpImpl(int value){
    Console.WriteLine("This is called from CSharp: " + value);
}

delegate void printFromCSharp(int value);

Then, we declare a pointer to function in C/C++.

hello.h

void callback(int value);
void callBackTest(void (*p)(int), int value);

And we should offer an implemention of it. Here we just call the callback function of C# in C/C++.

hello.c

void callBackTest(void (*p)(int), int value)
{
    p(value);
}

Add these to the code and run it, you will find that we call C/C++ methods from C#, and we aslo call C# methods from C/C++!

How to deal with C++?

If we just rename hello.c to hello.cpp, we will get an error. You can just have a try.

That's because when compiling C++ files, the real name of methods will be changed so that we cannot find the entry of our methods.

The solution is simple, you just need to add extern "C" to the header file of your C/C++ program.

hello.h

#ifndef HELLO_H
#define HELLO_H
#if CPLUSPLUS
extern "C"
{
#endif

#include <stdio.h>

typedef struct vector3
{
    float x, y, z;
} vector;

void HelloFunc();
int add(int a, int b);
void print(int *p, int len);

void callback(int value);
void callBackTest(void (*p)(int), int value);

vector mul(vector *rls, vector *rhs);
#if CPLUSPLUS
}
#endif
#endif

Then we change the lib/CMakeLists.txt to activate our macro.

lib/CMakeLists.txt

SET(LIBHELLO_SRC hello.cpp)

add_definitions(-DCPLUSPLUS)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

This time we will succeed to run the C# program.

How to cross-compile to other platforms such as Windows?

I referenced to this blog. You can follow it or search for an answer. There have been a lot of answers of this question.