2023-01-18 11:00:00
Trivial example
This is part of a series on Native AOT.
Previous -- Top -- Next
Let's walk through a very simplistic example, a function that multiplies two integers and returns the result.
Conceptually, that function in C# might look like this:
using System; public static class NativeExports { public static int Multiply(int a, int b) { return a * b; } }
But that's not quite sufficient. Native AOT requires us to
use an UnmanagedCallersOnly
attribute to explicitly identify
and give names to the entry points we want to export:
using System;
using System.Runtime.InteropServices;
public static class NativeExports
{
[UnmanagedCallersOnly(EntryPoint = "multiply")]
public static int Multiply(int a, int b)
{
return a * b;
}
}
What we're saying with the attribute above is that the function known in .NET as NativeExports.Multiply
will be exported as a C-compatible function called multiply
.
The function resides in a static class only because C# needs it to, and the name of that class doesn't matter.
Let's remember here that Native AOT doesn't support
reference types. If one of the function parameters was
of type string
, the compiler would complain:
Build FAILED. ...\mul_cs\lib.cs(8,46): error CS8894: Cannot use 'string' as a parameter type on a method attributed with 'UnmanagedCallersOnly'.
We'll talk later about how to cope with that. For now, we're using integers to keep things simple.
When building for Native AOT, we need to add a PublishAOT
property to our csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0
<PublishAOT>True
</PropertyGroup>
</Project>
And that's it. You can build this as a native library with dotnet publish
.
$ dotnet publish
MSBuild version 17.4.1+9a89d02ff for .NET
Determining projects to restore...
Restored C:\Users\eric\dev\native-aot-samples\mul_cs\mul.csproj (in 71 ms).
mul -> C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\mul.dll
... Microsoft.NETCore.Native.Publish.targets(48,5): error : RuntimeIdentifier is required
for native compilation. Try running dotnet publish with the -r option value
specified. [C:\Users\eric\dev\native-aot-samples\mul_cs\mul.csproj]
Okay, actually we need one more thing. Since Native AOT is compiling everything to native code,
it needs to know the target operating system and CPU at build time.
We specify this with a RuntimeIdentifier, such as win-x64
, linux-arm64
.
(Note, however, that the .NET SDK currently doesn't support Native AOT cross-compilation from one
OS to another, so dotnet publish -r linux-x64
won't work on a Windows host.)
$ dotnet publish -r win-x64 MSBuild version 17.4.1+9a89d02ff for .NET Determining projects to restore... Restored C:\Users\eric\dev\native-aot-samples\mul_cs\mul.csproj (in 97 ms). mul -> C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\win-x64\mul.dll Generating native code Creating library bin\Debug\net7.0\win-x64\native\mul.lib and object bin\Debug\net7.0\win-x64\native\mul.exp mul -> C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\win-x64\publish\
Mission accomplished, we have a native dll:
Directory of C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\win-x64\publish ... 01/17/2023 01:18 PM 4,755,968 mul.dll ...
WAIT -- it's over 4 megabytes?!? All it does is multiply two ints!
Yep, that's dreadful, isn't it?
Native AOT involves a certain amount of overhead. There are ways to make this tighter, but that's beyond the scope of this trivial example, so I'll duck the issue for now.
To be fair, we should note that this dll is completely standalone, and even at 4+ MB, it is much smaller than a full deployment of the .NET runtime and class libraries. But still.
One thing worth clarifying is that on Windows, native shared libraries use
the same .dll
file extension as .NET assemblies, which can be confusing.
One level up from the publish directory, we have another
mul.dll:
Directory of C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\win-x64 ... 01/17/2023 01:18 PM 3,584 mul.dll ...
But these two mul.dll files are very different.
The mul.dll in the publish directory is the native library we've been talking about, the one produced by Native AOT.
The smaller one up a level is a .NET assembly.
(And yes, 3,584 bytes is still huge for a function that multiplies two ints, but keep in mind that .NET assemblies contain lots of useful metadata that regular native object files do not.)
Finally, let's try building a static library. The
dynamic library is the default setting, so in order to
get a .lib
, we need to specify the msbuild property NativeLib
with a value of Static
:
$ dotnet publish --property NativeLib=Static -r win-x64
MSBuild version 17.4.1+9a89d02ff for .NET
Determining projects to restore...
Restored C:\Users\eric\dev\native-aot-samples\mul_cs\mul.csproj (in 98 ms).
mul -> C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\win-x64\mul.dll
Generating native code
Microsoft (R) Library Manager Version 14.34.31935.0
Copyright (C) Microsoft Corporation. All rights reserved.
"/OUT:bin\Debug\net7.0\win-x64\native\mul.lib"
"obj\Debug\net7.0\win-x64\native\mul.obj"
mul -> C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\win-x64\publish\
Now the publish directory looks like this:
Directory of C:\Users\eric\dev\native-aot-samples\mul_cs\bin\Debug\net7.0\win-x64\publish ... 01/17/2023 01:42 PM 23,589,176 mul.lib ...
Wow -- the static library is even bigger than the DLL! Yeah, I still don't want to talk about that issue right now. For the moment, let's just say a lot of stuff is going to get removed when that .lib meets the linker.
Next steps
So, we can package our trivial multiply function as a native library. Now we need something to call into that library. Here is where we have lots and lots of choices. The details of calling native code vary by platform and language.
The code for this blog entry is available at:
https://github.com/ericsink/native-aot-samples/tree/main/mul_cs