Using Nim with C# .NET
Nim is a statically typed programming language that compiles to C featuring a tracing garbage collector, zero overhead iterators, and powerful compile time evaluation of user defined functions. It features an indentation based syntax with a powerful AST based macro system. Thanks to the fact that Nim currently compiles to C, it can run on many platforms and architectures via a native binary. Cross compilation (for example, compiling a Windows executable on Linux) is also possible with the correct compiler configuration.
C# is a programming language from Microsoft that is developed as part of the .NET framework. It is also strongly typed and offers multiple programming paradigms, though it is primarily object orientated. Until very recently, the .NET framework was a proprietary framework available only for Windows. With the .NET Core initiative though, Microsoft have started opening .NET onto other platforms including Linux and the Mac. However, many third party libraries (and several first party ones) do not work with .NET Core due to changes that Microsoft made to program structure and configuration.
In this post, I will be exploring how easy it is to use libraries written in Nim within your C# projects. Thanks to the fact that Nim compiles to C, it's incredibly easy to build a DLL that can be accessed from within .NET. To begin, we'll write an extremely simple Nim library with a single function that adds two integers and returns the result.
Writing a simple Nim library
The first step is to create a folder to keep your library in. For the purpose of this post, we will be using the directory ~/NimAdder
. The first thing to do is create a project file: ~/NimAdder/adder.nim
:
proc addTwoInts*(a, b: cint): cint {.cdecl, exportc, dynlib.} =
## addTwoInts adds two integers together.
result = a + b
Some key things to note here are:
- The
proc
keyword - this is how you define a function in Nim. - The
*
symbol after the function name - this is how you declare a function or type to be public within a Nim module. - The arguments and return types are all
cint
- this means that the arguments and return value are all C style 32 bit (usually) integer values. - The
pragmas
included after the function return definition - these are enclosed within curly brackets with a dot ({.
and.}
) after the function definition and are comma separated. The pragmas used here arecdecl
,exportc
anddynlib
pragmas. - The implicit
result
variable - you can use the return statement to return a value from a function, but using the result variable is preferred unless you need to return early from some function.
You can compile this code using the below command from the ~/NimAdder directory:
nim c -d:release --header --noMain --app:lib adder.nim
Note the flags we used here:
-c
- this tells the compiler to compile the file it is passed.-d:release
- this results in an optimised release build with several runtime checks turned off and optimisations turned on.--header
- this results in a C header file being output to the./nimcache
folder, which can be used to see the C functions that are created.--app:lib
- this tells the compiler to build the project as a DLL.
This will output some information (as seen below) about the compilation process and you should end up with a file named adder.dll
(on Windows, or .dylib
on Mac or .so
on Linux and other systems).
Hint: used config file '/usr/local/Cellar/nim/0.14.2/nim/config/nim.cfg' [Conf]
Hint: system [Processing]
Hint: adder [Processing]
CC: adder
CC: stdlib_system
Hint: [Link]
Hint: operation successful (10299 lines compiled; 1.107 sec total; 15.504MiB; Debug Build) [SuccessX]
Creating a C# project to use the created DLL
Now that we've built a library, we need to create a C# project. Start Visual Studio and create a new project - the type doesn't matter, but I will use a Console Application as an example.
The first thing to do is to set the platform target for your project from the project settings. This is located under the Build
tab and has to be set for all build configurations (Build
and Release
by default). By default the target is Any CPU
, you need to change it to match the architecture your Nim library was built for. If you built it on a 64 bit system, you must set the .NET project to target x64 for example.
Now add the DLL you built earlier to your project as an existing item (Project > Add Existing Item > Browse to adder.dll) and set it to copy to the output directory if newer (Right click on adder.dll in the Solution Explorer and select Properties, change Copy to Output Directory to Copy if newer). As .NET loads DLLS dynamically, this ensures that the DLL will be copied alongside your executable when your project is built.
Now to reference the function that we exported in the Nim code. We do this using the DllImport attribute from System.Runtime.InteropServices:
namespace NimAdder
{
class Program
{
[DllImport("adder.dll")]
public static extern void NimMain();
[DllImport("adder.dll")]
public static extern int addTwoInts(int a, int b);
static void Main(string[] args)
{
}
}
}
You can now simply use addTwoInts
like any other function:
static void Main(string[] args)
{
var res = addTwoInts(40, 2);
Console.WriteLine("The meaning to life, the universe and everything is: {0}", res);
Console.ReadKey();
}
Closing
As you can see, accessing simple functions written in Nim from .NET is pretty easy. This approach should also allow you to call Nim functions from any other .NET language (such as Visual Basic .NET and F#).