Home About Eric Topics SourceGear

2023-02-15 13:00:00

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:

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:

    .Text(x =>
        x.Span("Page ");

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.

        |x : &TextDescriptor|
            x.Span_String(Some(&System::String::from("Page ")))?;

The video is available at: