Native AOT libraries with TypeScript

This is part of a series on Native AOT. Previous -- Top A few weeks ago I published some samples showing the use of Native AOT libraries from Rust. As I mentioned, the projection of .NET APIs to Rust requires quite a few ergonomic compromises. For example, the following line in C# is a call to the QuestPDF method to set the size of a page: page.Size(PageSizes.Letter); But the Rust projection is messier: page.Size_PageSize(&PageSizes::get_Letter()?)?; Rust doesn't have method overloading by name, and the Size() method has multiple overloads, so we have to give each one its own name, like Size_PageSize(). Rust doesn't have exceptions, and every .NET method might throw, but the metadata doesn't tell us anything about which kinds of errors we might want to propagate or not. So for the moment, the binding generator outputs every method with a return type of Result<>, which means we need to use ? after every call. Rust also lacks other things that .NET wants, including optional parameters, property getters/setters, and inheritance. Because of all these differences, Rust for general .NET development would typically involve too much pain to be practical. But the nice thing about Native AOT libraries, is that they can be used with anything that can call C. Hello TypeScript TypeScript has method overloads, exceptions, property getters/setters, and inheritance. Hmmm. For the last few weeks, I've been adding TypeScript as another output language for my binding generator. Things are in a rough state, but the results look positive, so I have published a nuget package and a working port of the QuestPDF sample. That sample is short, but it involves things like generic delegates and extension methods, so I consider it non-trivial. Interop between any two languages will always involve trouble spots, but TypeScript can look awfully similar to C#. Here's a visual diff from the Quest PDF sample, with TypeScript on the left, and C# on the right: All of the differences here are because I am projecting nullability more strictly than it is in C#. I currently think this is a feature rather than a bug, but I'm not completely settled on that, and if I stopped doing it this way, these two snippets would actually be identical. To be fair, let's acknowledge that this is just one sample. TypeScript lacks several things that require extra effort for .NET interop, including: byref parameters extension methods runtime type information I mean, if TypeScript were exactly like C#, it would be C#. Still, I see potential here. Details The binding generator itself is contained in a nuget package: https://www.nuget.org/packages/SourceGear.Bridge.NativeAOT.TypeScript/0.6.0 The QuestPDF sample is in the samples/typescript directory of this repo: https://github.com/sourcegear/bridge-info For this prototype, all the FFI-level stuff is setup for Deno (apologies to fans of node).> Related There's something going on in the following repo: https://github.com/microsoft/node-api-dotnet I'm not super-clear on where that project is headed. And there are significant differences relative to what I'm doing, but also some common ground. I think it looks interesting. Next steps I'm hoping that I will soon have an AvaloniaUI sample (it's a fair bit more complicated). Down the road a bit, I'd love to get this working with [at least the desktop versions of] MAUI, but I'm not sure yet what will be feasible. If you have any questions or feedback, please feel free to post in the Discussions or Issues area of the sourcegear/bridge-info repo linked above.

Posted on: 3 May 2023 | 2:00 pm

Binding Generator Preview Release

This is part of a series on Native AOT. Previous -- Top -- Next I have finally published a preview release of the Native AOT binding generator I've been working on. I wouldn't call it "production-ready" yet, but having the tool publicly available makes it more tangible and real. Folks can give it a try, and give feedback if they wish. There's a GitHub repo for samples and other information: https://github.com/sourcegear/bridge-info The content there is fairly bare-bones right now, but there are a few Rust samples, including the QuestPDF demo I discussed a few weeks ago, plus two little apps that use Avalonia. One of the Avalonia samples is written against the very basic controls. The other uses NXUI, which is a pretty cool library offering a different way of working with Avalonia: https://github.com/wieslawsoltes/NXUI These "Work on My Machine", and should hopefully work for you as well. A word about ugliness Some folks have contributed various aesthetic opinions about the nature of .NET APIs projected into Rust. :-) The simple fact is that Rust lacks a number of key things that C# uses a lot: Method overloading Exceptions Default parameter values Inheritance Properties etc This impedance mismatch causes the Rust projection of a .NET API to be far less ergonomic than things are in C#. So I am certainly not suggesting that Rust could become a preferred way of using .NET. I am exploring the boundaries of what is possible in interop situations. Some .NET libraries turn out better than others. For Rust, Avalonia hits almost all of the difficulties at one time. That makes it a nice test case, but not necessarily a practical use case. What got published The binding generator itself is contained in a nuget package: https://www.nuget.org/packages/SourceGear.Bridge.NativeAOT.Rust/0.5.0 But that package is not intended to be used directly. Rather, a Cargo build script will deal with all that. The dependency for that build script is this crate: https://crates.io/crates/sourcegear-bridge-build Several things need to be done to integrate that build script into a Cargo project. Take a look at the samples I mentioned above. Also, there is a Cargo subcommand designed to help make this easier: https://crates.io/crates/sourcegear-bridge-cargo If you have any questions or feedback, please feel free to post in the Discussions or Issues area of the sourcegear/bridge-info repo linked above. Enjoy!

Posted on: 10 April 2023 | 2:00 pm

Delegates

This is part of a series on Native AOT. Previous -- Top -- Next Developing with .NET often involves delegates, which we can think of as objects that represent things that are callable. For example: public static int count_files_with_e(string path) { return System.IO.Directory.GetFiles(path) .Where(x => x.Contains("e")) .Count() ; } The extension method Where accepts a delegate. We're calling it with a lambda expression, which the compiler converts into the correct delegate type. How do we deal with delegates in a Native AOT library? The signature for System.Linq.Enumerable.Where() is fairly hard on the eyes: public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource,bool> predicate ); All those generics! Yikes. We need to start with a simpler example: public delegate int MapOne(int x); public static int MapSum( int n, MapOne f ) { var sofar = 0; for (var i=0; i<n; i++) { sofar += f(i); } return sofar; } The delegate type MapOne is a simple mapping of one integer value to another. The MapSum function loops over the first n integers and applies a delegate to each one, returning the sum of the results. Calling MapSum from C# with a lambda expression might look like this: public static int CountDivisibleBy42(int n) { return MapSum( n, x => ((x % 42) == 0) ? 1 : 0 ); } But functions exposed by a Native AOT library must follow the rules of C, and C doesn't have delegates -- it has function pointers: typedef int (*MapOne)(int); For the sake of illustration, let's observe that C# 9 added support for function pointers, so one option here is to just rewrite MapSum to use them: [UnmanagedCallersOnly(EntryPoint = "map_sum_with_funcptr")] public static unsafe int MapSumWithFuncPtr( int n, delegate* unmanaged<int,int> f ) { var sofar = 0; for (var i=0; i<n; i++) { sofar += f(i); } return sofar; } This results in a function signature which is compatible with Native AOT, so we could call it from C. First, since C doesn't have lambdas, we need to define the map function: int divisible_by_42(int x) { return ((x % 42) == 0) ? 1 : 0; } And the call from C to the Native AOT function looks like this: int32_t total = map_sum_with_funcptr( 1000, divisible_by_42 ); printf("%d\n", total); But Native AOT libraries won't be much fun if we need to rewrite everything. It would be preferable to leave MapSum unchanged and still provide a way to call it. In other words, we want to convert our function pointer into a delegate. We can do that with System.Delegate.CreateDelegate(). The .NET class libraries provide CreateDelegate as a way to create delegates of a given type from other methods. It has several overloads, but none of them accept a function pointer, so we need to wrap our function pointer in a something that CreateDelegate can accept. I call this wrapper a "shadow" class: private unsafe class my_shadow { readonly delegate* unmanaged<int,int> _funcptr; public my_shadow(delegate* unmanaged<int,int> funcptr) { _funcptr = funcptr; } public unsafe int Invoke(int x) { return _funcptr(x); } } Now we have a regular C# object that contains the function pointer and provides a method to invoke it. So we can create a delegate of type MapOne by making an instance of that shadow class and passing it to CreateDelegate: var shadow = new my_shadow(funcptr); var del = Delegate.CreateDelegate( typeof(MapOne), shadow, typeof(my_shadow).GetMethod("Invoke") ); Note that some of the overloads for CreateDelegate cause AOT or trimmer warnings. I'm using an overload that Native AOT likes. Using the GCHandle approach described previously, we can return the delegate object across the Native AOT boundary so it can be passed back, and for that purpose we need to expose our original MapSum function with a signature that accepts that delegate object handle: [UnmanagedCallersOnly(EntryPoint = "map_sum_with_delegate")] public static int MapSumWithDelegate( int n, IntPtr del ) { var actualDelegate = (MapOne) GCHandle.FromIntPtr(del).Target; return MapSum(n, actualDelegate); } And finally, we can call MapSum from C like this: intptr_t del = create_delegate( divisible_by_42 ); int32_t total = map_sum_with_delegate( 1000, del ); printf("%d\n", total); The code for this blog entry is available at: https://github.com/ericsink/native-aot-samples/tree/main/delegate_i32

Posted on: 2 March 2023 | 1:00 pm

Looking ahead: QuestPDF/Rust demo

This is part of a series on Native AOT. Previous -- Top -- Next So far in this blog series, I have been writing about Native AOT mostly in the context of libraries, discussing how things work at a fairly low level. For this post, I want to take a step back and look at the big picture, and the road ahead. Where is all this going? Why do we care about Native AOT libraries, and what can we do with them? One of the most commonly cited benefits of Native AOT is faster startup time. But if that were the only thing that mattered, we wouldn't need support for libraries, would we? A primary benefit of Native AOT for libraries is the ability to interop with other languages and ecosystems. If we can compile our C# library into a regular native library that pretends to be written in C, then we can integrate it into almost anything. Native AOT libraries do make some new things possible, but that doesn't mean those things are easy. As we have noted in this series, exported functions need to follow the rules of C, and that's a really big impedance mismatch with .NET classes. Furthermore, unless we are actually consuming the library from C, we have another canyon to cross, bridging to something idomatic for Go or Rust or Python or whatever. This set of problems is somewhat new to .NET, but in general, it is fairly well-trodden ground. Typically, these kinds of gaps get crossed by generating bindings automatically. There are many examples of tools that generate bindings or glue code or middleware to make it easier to connect code written in one language to another. Citing one example seems better than zero, so I'll mention SWIG. Wikipedia says it has been around for 27 years. But like I said, there are lots of other examples. Anything that becomes popular creates a demand for it to interoperate with something else. But for .NET there hasn't been much of this kinda thing going on, at least not for the "calling .NET code from something else" direction. Prior to Native AOT, calling a C# library from native code usually involved hosting the CLR. I'm not saying nobody does that, but, well, nobody does that. But now we have Native AOT libraries. We're going to want binding generators. And intuitively, we should be able to have good ones. All that metadata in a .NET assembly ought to be really useful information when generating glue. I've been experimenting along these lines, and I am seeing some encouraging results. There are several pieces in play: The core binding generator is a console app that reads a set of .NET assemblies and uses the metadata to generate exported Native AOT functions, plus Rust bindings. Its command-line options are ... complicated. That console app is contained within a nuget package that provides MSBuild targets which integrate with the Native AOT stuff in the .NET SDK. It gathers up the necessary information and invokes the binding generator. A Cargo build script provides integration with the Rust tools. It invokes dotnet publish, which triggers the MSBuild targets, which generates the bindings and makes them available for the rest of the build process. A .csproj file in the Rust crate directory is used to specify which assemblies are available for Native AOT binding. This includes the regular .NET class libraries, and can also include nuget package references, or other references. A config file is used to specify which classes and members we want to generate bindings for. This is currently not very friendly, but if we didn't provide a way to limit things, we would end up generating bindings for everything, including all the nuget packages and the entire .NET system. This would greatly increase build time and the size of the resulting library. These tools have not yet been released, so in lieu of posting sample code, the demo for this chapter is in the form of a video. It's just a few minutes long, and it shows a QuestPDF sample app, first in C#, then compiled to a native library with Native AOT, and ported to Rust. As a teaser, here's a snippet from the original C#, using fluent extension methods, and a lambda: page.Footer() .AlignCenter() .Text(x => { x.Span("Page "); x.CurrentPageNumber(); }); The Rust equivalent is noisier, as Rust tends to be, but it's structurally similar. The binding generator has created wrapper objects for the necessary .NET classes, and traits for the extension methods. Where the lambda used to be, we have a Rust closure that gets wrapped as a .NET delegate. page.Footer()? .AlignCenter()? .Text_Action( |x : &TextDescriptor| { x.Span_String(Some(&System::String::from("Page ")))?; x.CurrentPageNumber()?; Ok(()) } )?; The video is available at: https://youtu.be/XQI1SvvqbGk

Posted on: 15 February 2023 | 1:00 pm

Must follow C rules, no exceptions

This is part of a series on Native AOT. Previous -- Top -- Next As we have said, functions exported by a Native AOT library must follow the rules of C, and that means exceptions cannot be thrown. More specifically, it means that if we attempt to throw an exception past the Native AOT function boundary, the program will crash. C doesn't have exceptions, so it can't deal with them. Once again, here's the code for a Native AOT function to get the length of a string: [UnmanagedCallersOnly(EntryPoint = "get_string_length")] public static int GetStringLength(IntPtr v) { GCHandle h = GCHandle.FromIntPtr(v); object ob = h.Target; string s = (string) ob; int len = s.Length; return len; } As trivial as this code is, it does have places an exception might get thrown. In fact, we did that intentionally in the previous chapter by passing in an invalid GCHandle. What should we do about this? We basically have only two options. We can allow the exception to cause a crash, or we can catch the exception and somehow propagate the error. For this particular function, we might actually just want to leave it alone. Some errors can't be usefully handled. As far as I can tell, aside from memory corruption, the only way this function can crash is when it was given an invalid argument, which suggests a bug somewhere else that should be found and fixed. But that won't work in every scenario. In many cases, the body of a Native AOT function might need to look something like this: [UnmanagedCallersOnly(EntryPoint = "my_function")] public static int my_function() { try { // do something return 0; // 0 means no error } catch (Exception e) { return -1; // TODO some kind of meaningful error code } } There are a myriad of choices here, but the basic point is that we would need to decide how we want to represent errors, catch exceptions, and return those exceptions in the representation we chose. In this example I chose the commonly used approach of an integer error code, but I do not wish to imply that this would be simple. Error handling rarely is. Let's pretend for a moment that we actually do want our string length function to catch and propagate. How would do that? If we want to return an error code, we now have two return values, since the function already wants to return the string length. One or both of these values are going to need to be returned through a pointer parameter (C rules, remember?). Let's propagate the error code through a pointer (which requires unsafe): [UnmanagedCallersOnly(EntryPoint = "get_string_length_errcode_parm")] public unsafe static int WithErrorCodeParm(IntPtr v, int* ptr_error_code) { try { GCHandle h = GCHandle.FromIntPtr(v); object ob = h.Target; string s = (string) ob; int len = s.Length; *ptr_error_code = 0; // no error return len; } catch (Exception e) { *ptr_error_code = -1; // TODO meaningful error code return -1; // TODO but what result should we return? } } One problem here is that we still have to return something for the result even when the exception is caught. A string length cannot be less than zero, so we just return something invalid. Hey, maybe this function doesn't need the error code to be separate? Couldn't we just make all our error codes negative and thus store the length and the error code in the same number? In this case, sure, that'll work: [UnmanagedCallersOnly(EntryPoint = "get_string_length_neg")] public static int AsNegativeLength(IntPtr v) { try { GCHandle h = GCHandle.FromIntPtr(v); object ob = h.Target; string s = (string) ob; int len = s.Length; return len; } catch (Exception e) { return -1; // TODO meaningful negative error code } } But that certainly won't work in all cases. This function just happens to have a return type that isn't using all its possible values. And either way, the caller of this function now has to actually deal with error conditions. It needs to check for a negative length, or it needs to check that separate error code. There is not much reason to propagate errors and then ignore them. And that brings us full circle to the realization that error propagation for this particular function is a cure that is worse than the disease. Broadly speaking, error handling is a very complex topic. For now, my intent is merely to scratch the surface, and to observe that Native AOT libraries bring error handling challenges that many .NET developers will find unfamiliar. In typical .NET development: We don't need to decide how to represent errors, because System.Exception is the standard way to do that. The decision of whether to propagate error information does not affect the signature of a method, because exceptions can be thrown from anywhere. Providing a Native AOT API for a library can raise a bunch of questions that don't typically come up. I'll close with one more thing. Even with Native AOT, we could maaaaaybe stay with System.Exception as the standard way of propagating error information. Using a GCHandle, we can return exception objects -- we just can't throw them. [UnmanagedCallersOnly(EntryPoint = "get_string_length_ex_parm")] public unsafe static int WithExceptionParm(IntPtr v, IntPtr* ptr_ex) { try { GCHandle h = GCHandle.FromIntPtr(v); object ob = h.Target; string s = (string) ob; int len = s.Length; *ptr_ex = IntPtr.Zero; // no error return len; } catch (Exception e) { *ptr_ex = GCHandle.ToIntPtr(GCHandle.Alloc(e)); return -1; // TODO but what result should we return? } } That solves only one of our problems (and not one of the more difficult ones). And it raises more problems of its own: Object handles are opaque, so in order to turn that Exception object into any sort of useful information, we'll have to make another trip across the Native AOT boundary, and, well, what happens if an exception gets thrown while trying to get information about the exception that got thrown? The code for this blog entry is available at: https://github.com/ericsink/native-aot-samples/tree/main/no_exceptions

Posted on: 9 February 2023 | 1:00 pm

A "gotcha" with object handles

This is part of a series on Native AOT. Previous -- Top -- Next In the previous chapter, we talked about GCHandle as a way to pass object references into native code. Let's dive a little deeper and talk about a problem that can happen with these object handles in the context of Native AOT. As we said in the previous chapter, the IntPtr from a GCHandle is "opaque", the only thing we can do with it is "give it back to the .NET code and ask it to do something". But we need to be a bit more precise. That GCHandle is owned by one particular instance of the .NET GC, and we can only "give it back" to that particular instance. If there happen to be multiple copies of the GC around, we must not ... cross the strands. Let's illustrate this by taking the code from last chapter's sample and breaking it into two libraries. One library will have get_hello_string and free_objecthandle. The other 2 functions, get_string_length and banish_letter_l, will go into a separate library.. All we've done here is split one library into two. We moved the functions, but we made no changes to their code. We can leave the C++ code unchanged. We just need to change its little build script to reference both libraries. When we run the program, we now get an error: Unhandled Exception: System.InvalidCastException: Specified cast is not valid. at System.Runtime.TypeCast.CheckCastClass(MethodTable*, Object) + 0x6b at NativeExports.GetStringLength(IntPtr) + 0x70 Aborted For convenience and review, here's the code for get_string_length: [UnmanagedCallersOnly(EntryPoint = "get_string_length")] public static int GetStringLength(IntPtr v) { GCHandle h = GCHandle.FromIntPtr(v); object ob = h.Target; string s = (string) ob; int len = s.Length; return len; } We are given an IntPtr, which we convert back to its GCHandle, and then grab the original object from the Target property, and then cast that object to a string. The exception is being thrown on the attempted cast, but the core problem is that the GCHandle is ... foreign. Remember in the previous chapter when we said that the GC keeps track of everything? Our transgression here is that we allocated a string in one library and tried to use it in a different library. That's not okay. The GC can't keep track of stuff outside of its library. In other words: Each library has its own GC. We can see from the size of the two shared libraries that each .so is large enough to suggest that it has its own copy of all the dependencies it needs: $ ls -l cs_make_string/bin/Debug/net7.0/linux-x64/publish/ total 15448 -rw-r--r-- 1 eric eric 15814944 Jan 31 10:19 make_string.so $ ls -l cs_use_string/bin/Debug/net7.0/linux-x64/publish/ total 15452 -rw-r--r-- 1 eric eric 15820792 Jan 31 10:33 use_string.so Strictly speaking, this "foreign handle" problem is as old as GCHandle itself, but it has been relatively uncommon. Before Native AOT libraries, you kinda had to go out of your way to have multiple instances of the GC and pass a GCHandle across them. Now that Native AOT (by default) bundles up all the dependencies with each library, the result is a potentially bigger limitation in practice: When multiple Native AOT libraries are in play, we cannot share objects between them. This has ramifications for the use cases that can be addressed with Native AOT libraries. For example, suppose that Carole, Monica, and Kate each have a C# library that they want to make available as a package for C++ developers. Such a package might contain the compiled library (built with Native AOT) plus a C++ wrapper. Carole's library is more foundational, and each of the other two libraries have a dependency on it. With the current limitations of Native AOT, that scenario is difficult or impossible. One possible solution here is "just don't share objects". For some cases, that might be okay. For example, with a string, we could convert to/from a C++ representation that no longer relies on the object handle. But this is not a general solution. What if the object is a network socket? How did we end up here? I suspect the .NET team has simply prioritized ease over power. Building a Native AOT library is really elegant. Just type dotnet publish -r RID and the tooling will figure out everything you need, warn you about incompatibilities, and get it done. The resulting shared library doesn't have any weird dependencies -- everything it needs has been included, and everything else has been trimmed out. An alternative approach, one which prioritized power-user scenarios, would have been possible, but it would have taken longer to develop, and it would have made the feature much harder to use. Instead of being self-contained, the resulting shared library would depend on other shared libraries, probably quite a few of them. (I should note here that the early support for static libraries does offer cause for optimism, although not yet a clean solution. When Native AOT builds a static library, it does not include a copy of the GC (and such things). Rather, (as mentioned in a previous chapter), the necessary runtime dependencies need to be added at link time. However, each static library build by Native AOT does get its own copy of certain other things, so using two static libraries results in duplicate symbols.) In my opinion, the team made the right choice for the early stages of Native AOT. Nonetheless, I do hope the feature gets more flexibility in this area later. The code for this blog entry is available at: https://github.com/ericsink/native-aot-samples/tree/main/problem_multiple_libraries

Posted on: 31 January 2023 | 1:00 pm

Objects

This is part of a series on Native AOT. Previous -- Top -- Next So far, our examples have been very simplistic, using only integer types. Native AOT won't be very useful if we can never use objects. And we can. We just need to express them in terms of the conventions of C. Actually, the techniques for dealing with .NET objects in unmanaged code are not new. We've had good interop capabilities much longer than we've had Native AOT. The primary way to pass an object reference to unmanaged code is a GCHandle, and it first appeared in .NET Framework about 20 years ago. If you are already familiar with GCHandle, please bear with me as I explain things from first principles in the context of Native AOT. In many languages, memory is managed manually. If you need a bit of memory, you have to ask for it, and when you are done, you have to release it. In C, the standard library functions for this are called malloc and free. The need to carefully manage memory has been the source of countless bugs. The memory for .NET objects is managed automatically by a Garbage Collector (GC). When you construct an object, memory is allocated, but you don't have to worry about explicitly releasing it. The .NET runtime keeps track of things for you, and when a block of memory is no longer being used, it is classified as "garbage" and freed. This works because .NET knows about all objects. But if you want to store an object reference somewhere that .NET cannot see, then the GC doesn't know about that reference, so it might decide the object is garbage, and your reference would become invalid. This is the problem a GCHandle is designed to solve. We can create a GCHandle for any object, and when we do so, we are telling the GC that "as long as this handle exists, the object is not garbage". The GCHandle can be passed into unmanaged code and stored in unmanaged memory. The following Native AOT function returns an object (a string): [UnmanagedCallersOnly(EntryPoint = "get_hello_string")] public static IntPtr GetHelloString() { string s = "Hello World"; GCHandle h = GCHandle.Alloc(s); return GCHandle.ToIntPtr(h); } Start with a string value. Construct a GCHandle for that string using GCHandle.Alloc(). Convert the GCHandle to an integer representation using GCHandle.ToIntPtr(). Because the string variable is local to the function, it would become garbage when the function returns, but the GCHandle prevents that. An IntPtr is an integer that is the same size as a pointer. On most modern systems, that'll be 64 bits. On 32-bit systems, pointers are 32 bits wide, so IntPtr is as well. In any case, an IntPtr is an integer, so we can return it across the Native AOT boundary, where it can be used by unmanaged code in whatever way we like. Well, actually, the unmanaged code can't do much with it at all. The IntPtr is "opaque". It's probably the numerical address of a block of memory, but it doesn't have to be, and even if it is, we're not supposed to modify that memory or even look at it. The only thing we can do with our IntPtr is give it back to the .NET code and ask it to do something. But that opens lots of possibilities. Here's a Native AOT function that retrieves the length of a string: [UnmanagedCallersOnly(EntryPoint = "get_string_length")] public static int GetStringLength(IntPtr v) { GCHandle h = GCHandle.FromIntPtr(v); object ob = h.Target; string s = (string) ob; int len = s.Length; return len; } This is the typical pattern when we have an object handle in unmanaged code and we pass it back to .NET and ask it to do something. Start with the IntPtr Convert it back to a GCHandle with GCHandle.FromIntPtr Retrieve the underlying object with the Target property of the GCHandle Cast the object to the type we expect it to be Do something So far, we've seen one code snippet that converts an object to an IntPtr, and one code snippet that converts an IntPtr back to an object. But it's quite common to need both in the same function. Here's a Native AOT function that accepts a string and returns another string made by calling String.Replace(). [UnmanagedCallersOnly(EntryPoint = "banish_letter_l")] public static IntPtr BanishLetterL(IntPtr v) { var s = (string) GCHandle.FromIntPtr(v).Target; var s2 = s.Replace("l", "NOT"); return GCHandle.ToIntPtr(GCHandle.Alloc(s2)); } It is important to remember that every GCHandle must be released. So, if we're going to return objects from Native AOT functions, we must also provide something like the following: [UnmanagedCallersOnly(EntryPoint = "free_object_handle")] public static void FreeObjectHandle(IntPtr v) { GCHandle h = GCHandle.FromIntPtr(v); h.Free(); } The GCHandle concept is a way of bridging the gap between the automatic memory management of .NET and the world where memory is managed manually. Like most any other form of manual memory management, GCHandle is very unforgiving. If we don't release a handle, the object will never be freed, and we get a memory leak. If we release a handle more than once, or if we release a handle that does not exist, we are likely to cause memory corruption. Finally, the C++ code below shows an example of how to call the functions shown above. #include <cstdint> #include <stdio.h> extern "C" uintptr_t get_hello_string(); extern "C" int32_t get_string_length(uintptr_t); extern "C" uintptr_t banish_letter_l(uintptr_t); extern "C" void free_object_handle(uintptr_t); int main() { // the original string is "Hello World" uintptr_t s1 = get_hello_string(); // the length of the original string is 11 int32_t len1 = get_string_length(s1); printf("%d\n", len1); // the new string should be "HeNOTNOTo WorNOTd" uintptr_t s2 = banish_letter_l(s1); // the length of the new string is now 17 int32_t len2 = get_string_length(s2); printf("%d\n", len2); // need to release both string objects free_object_handle(s1); free_object_handle(s2); return 0; } Two final thoughts about the code sample for this chapter: To keep things focused, I'm still giving no attention to proper error handling, but that topic needs to be discussed. So far, all the samples have been shown on Windows, even though they are inherently cross-platform. For the sake of mixing things up, this one is configured for Linux. The code for this blog entry is available at: https://github.com/ericsink/native-aot-samples/tree/main/hello_string

Posted on: 25 January 2023 | 11:00 am

Static libraries

This is part of a series on Native AOT. Previous -- Top -- Next Buckle up folks, this part of the ride gets a little bumpy. As of .NET 7, using Native AOT with static libraries is implemented, but is not yet considered a supported and documented feature. As mentioned previously, building a static library with Native AOT is straightforward. Just set the NativeLib property to static: $ dotnet publish --property NativeLib=Static -r win-x64 And the code to call our multiply() function is no more complicated than it would normally be for whatever language is in play. In this sample, we're using C++: #include <cstdint> #include <stdio.h> extern "C" int32_t multiply(int32_t, int32_t); int main() { int32_t c = multiply(7, 6); printf("%d\n", c); return 0; } Where things get tricky is when we try to link. To be fair, the various command line options for any C++ compiler are usually arcane and complicated, so we'll try to blame Native AOT only for the complexity that it added to an already messy situation. Using the Microsoft C++ compiler, our first attempt to build the program shown above might look something like the following: cl.exe (compiler options) mul_main.cpp ../mul_cs/bin/Debug/net7.0/win-x64/publish/mul.lib (windows libraris) I've omitted the usual compiler options and the long list of Windows libraries so we can focus on the two things we really care about. What we want is to compile the C++ snippet from above, and link it with the static library containing our multiply() function, built by Native AOT. Unfortunately, this command will result in the linker complaining about a bunch of unresolved symbols. These symbols correspond to the various things that Native AOT code needs, a stripped-down form of the .NET runtime. In principle, our trivial multiply() function should not need that stuff, but it is a degenerate case. There are settings we could have used to omit certain dependencies, but we didn't use those settings, so Native AOT has assumed that this library will need all the usual stuff. So we need to add a bunch of static libraries to our link. When Native AOT building a dynamic library, this chore is performed automatically, because the .dll (or .so or .dylib) is built by the linker, but with a static library, we're deferring the actual link step until later. But what libraries do we need? And where are they? A few chapters ago, when we set things up for building with Native AOT, we had to add the PublishAOT property to our csproj. When the .NET SDK sees this property, it does a lot of work behind the scenes, including adding references to several nuget packages which contain the tools and libraries that Native AOT needs. In the case of Windows, those libraries are in the nuget package runtime.win-x64.microsoft.dotnet.ilcompiler. I mentioned above that none of this is considered to be a supported feature for .NET 7. In fact, the currently suggested way to figure this stuff out is to run a Native AOT build with detailed logging and examine the output to see the actual command line options for the C++ compiler. That gives us the list of libraries, plus the fact that we need to require the NativeAOT_StaticInitialization symbol. The final command line is just too ugly to show in a blog entry, but all the gory detail is in a BAT file in the sample code. We end up with one self-contained executable file (which is still ridiculously large, grumble). C:\Users\eric\dev\native-aot-samples\mul_cpp_win_static> dir Directory of C:\Users\eric\dev\native-aot-samples\mul_cpp_win_static ... 01/23/2023 09:53 AM 4,763,136 mul_main.exe ... C:\Users\eric\dev\native-aot-samples\mul_cpp_win_static> .\mul_main.exe 42 The code for this blog entry is available at: https://github.com/ericsink/native-aot-samples/tree/main/mul_cpp_win_static

Posted on: 23 January 2023 | 10:00 am

Silly

This is part of a series on Native AOT. Previous -- Top -- Next Native AOT produces libraries that can be called from any language that can call C. C# is one such language. Yes folks, that's right -- even though it is [probably] not useful, you can call into a Native AOT library from C# using P/Invoke: using System; using System.Runtime.InteropServices; static class MulCsPinvoke { [DllImport("mul")] static extern int multiply(int a, int b); public static void Main() { var c = multiply(7, 6); Console.WriteLine($"{c}"); } } After we copy mul.dll into place, we can run it: $ cp ../mul_cs/bin/Debug/net7.0/win-x64/publish/mul.dll . $ dotnet run 42 The code for this blog entry is available at: https://github.com/ericsink/native-aot-samples/tree/main/mul_cs_pinvoke

Posted on: 18 January 2023 | 1:00 pm

Multiplying two integers from Rust

This is part of a series on Native AOT. Previous -- Top -- Next Rust has a really nice feature called raw-dylib, which allows calling external functions without linking at build time. Given the name of the shared library, Rust will dynamically load the library and lookup the function. (At the time of this writing, raw-dylib is supported only on Windows.) So, because of this raw-dylib feature, Rust is one of the simpler examples of how to invoke our trivial multiply() function. We just need to declare the extern function signature and include a link attribute to let Rust know that the function should be found in a shared library with the base name "mul": #[link(name="mul", kind="raw-dylib")] extern { fn multiply(a : i32, b : i32) -> i32; } FWIW, raw-dylib in Rust is basically the same feature as P/Invoke in .NET: [DllImport("mul")] static extern int multiply(int a, int b); Anyway, once the declaration is in place, we can call the function from Rust, keeping in mind that Rust considers any FFI function to be unsafe: fn main() { let c = unsafe { multiply(7, 6) }; println!("{}", c); } Of course, the program will complain if it can't find the dynamic library: $ cargo run Compiling mul_rs_win_dynamic v0.1.0 (C:\Users\eric\dev\native-aot-samples\mul_rs_win_dynamic) Finished dev [unoptimized + debuginfo] target(s) in 0.41s Running `target\debug\mul_rs_win_dynamic.exe` error: process didn't exit successfully: `target\debug\mul_rs_win_dynamic.exe` (exit code: 0xc0000135, STATUS_DLL_NOT_FOUND) C:/Users/eric/.cargo/bin/cargo.exe: error while loading shared libraries: ?: cannot open shared object file: No such file or directory But once mul.dll (built with Native AOT in the previous chapter) is copied into place, we can finally multiply two numbers from Rust: $ cp ../mul_cs/bin/Debug/net7.0/win-x64/publish/mul.dll . $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target\debug\mul_rs_win_dynamic.exe` 42 The code for this blog entry is available at: https://github.com/ericsink/native-aot-samples/tree/main/mul_rs_win_dynamic

Posted on: 18 January 2023 | 12:00 pm