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!
Michal Strehovský@mstrehovsky@mhmd_azeez @xoofx CoreRT can generate .dll/.so/.dylib and .lib/.a files that export C# functions in a way that makes them directly callable through standard FFI. A sample walkthrough is here: github.com/dotnet/corert/…07:58 AM - 02 May 2019
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:
- Build the native library by running
dotnet publish /p:NativeLib=Shared -r win-x64 -c Release
in its folder. - Create a new console app (I created a dotnet core console app, but it doesn't matter).
- Right click on the project and click
Add => Exisiting Item
. - Browse to
bin\Release\netcoreapp2.2\win-x64\native
folder of the native library project and then select the dll and click onAdd As Link
. - Right Click on the dll in Solution Explorer and click on properties.
- Change
Copy to Output Directory
toCopy if newer
. - Change Solutions Platform to
x64
- 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!
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)
Amazing
Thank you