Home About Eric Topics SourceGear

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.

(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