DEV Community

Muhammad Azeez
Muhammad Azeez

Posted on • Updated on • Originally published at mazeez.dev

Writing Native Libraries in C# and using them in other languages

Note: Stefan Hausotte has ported this little experiment to F#.

Recently I stumbled upon this article from Michal Strehovsky. It was a great introduction to CoreRT, it made me curious, can you write native libraries with CoreRT? and the answer was Yes!

I have a little library that I want to be available for multiple languages, so I was quite interested in it. So I tried out the official sample and was delighted with the results.

I compiled the library using dotnet publish /p:NativeLib=Shared -r win-x64 -c Release and it produced a 4.52 MB dll. With the help of Michal and by following the steps of this article, I was able to get the size down to 1.67 MB, which is good enough for me.

The official sample has a class that contains two methods: Add and WriteLine which demonstrate how to take primitives and strings as parameters. By default, CoreRT only allows primitives as parameter types, you'll have to marshal anything else that's more complex. However, System.Runtime.InteropServices.Marshal does some have helpful methods.

public class Class1
{
    [NativeCallable(EntryPoint = "add", CallingConvention = CallingConvention.StdCall)]
    public static int Add(int a, int b)
    {
        return a + b;
    }

    [NativeCallable(EntryPoint = "write_line", CallingConvention = CallingConvention.StdCall)]
    public static int WriteLine(IntPtr pString)
    {
        // The marshalling code is typically auto-generated by a custom tool in larger projects.
        try
        {
            // NativeCallable methods only accept primitive arguments. The primitive arguments
            // have to be marshalled manually if necessary.
            string str = Marshal.PtrToStringAnsi(pString);

            Console.WriteLine(str);
        }
        catch
        {
            // Exceptions escaping out of NativeCallable methods are treated as unhandled exceptions.
            // The errors have to be marshalled manually if necessary.
            return -1;
        }
        return 0;
    }
}

The methods that are decorated with NativeCallable cannot be called through normal C# methods, but they can be called through P/Invoke 😈.

To do that, you have to:

  1. Build the native library by running dotnet publish /p:NativeLib=Shared -r win-x64 -c Release in its folder.
  2. Create a new console app (I created a dotnet core console app, but it doesn't matter).
  3. Right click on the project and click Add => Exisiting Item.
  4. Browse to bin\Release\netcoreapp2.2\win-x64\native folder of the native library project and then select the dll and click on Add As Link.
  5. Right Click on the dll in Solution Explorer and click on properties.
  6. Change Copy to Output Directory to Copy if newer.
  7. Change Solutions Platform to x64 Solutions Platform
  8. And then change the code in Program.cs as follows:
class Program
{
    [DllImport("NativeLibrary.dll", EntryPoint = "add", CallingConvention = CallingConvention.StdCall)]
    public static extern int Add(int a, int b);

    [DllImport("NativeLibrary.dll", EntryPoint = "write_line", CallingConvention = CallingConvention.StdCall)]
    public static extern void WriteLine(string text);

    static void Main(string[] args)
    {
        var result = Add(1, 2);
        WriteLine(result.ToString());
        WriteLine("Hello World!");
    }
}

Now run the console app and you'll get an output like this:

3
Hello World!

But Why?

Now p/invoking the library might not be very useful, but compiling a class library as a native library opens doors for other languages to call the library.

It was a long and painful process, but I was eventually able to reference the library from the C++ app. This video and this article were super helpful.

Here are the steps:
1 . Add an empty C++ project to the solution.

2 . Add a source file and paste in this code snippet:

#include <iostream>
#include <NativeLibrary.h>
using namespace std;

void main()
{
    int result = add(1, 2);
    cout << result << endl;
    write_line("Hello World!");
}

3 . Create a new header file called NativeLibrary.h (That's the name of the library) and paste in this code snippet:

#pragma once
extern "C" int __stdcall add(int a, int b);
extern "C" void __stdcall write_line(const char* pString);

As you can see have written the signatures of the functions that are exported from NativeLibrary.

4 . Right Click on the C++ project and Click on Properties.

5 . Choose All Configurations from Configuration:. This will make sure that the changes apply to both Release and Debug configurations (And any other configuration you might have).

6 . Go to General and change Output Directory to $(ProjectDir)bin\$(Platform)\$(Configuration)\. This is not necessary, but I felt more at home like this.

7 . Go to C\C++ > Linker > General and add $(SolutionDir)NativeLibrary\bin\Release\netcoreapp2.2\win-x64\native to Additional Library Directories. This allows the linker to discover NativeLibrary.lib.

8 . Go to C\C++ > Linker > Input and add NativeLibrary.lib to the list of Additional Dependencies.

9 . Go to Build Events > Post-Build Event and paste in this code snippet to Command Line:

xcopy /y /d "$(SolutionDir)NativeLibrary\bin\Release\netcoreapp2.2\win-x64\native\NativeLibrary.dll" "$(OutDir)"

This will copy NativeLibrary.dll to the output dir whenever you build the C++ project.

10 . Build and run the application and you should see this output:

3
Hello World!

If this is not cool, I don't know what is.

The source code is available on GitHub.

Top comments (1)

Collapse
 
artydev profile image
artydev

Amazing
Thank you