How do you use System.Drawing in NativeAOT?

You, writing code to manipulate images!

Out of the box, NativeAOT does not work with System.Drawing.Image class and friends. I was discover that when made attempt to port existing application WinForms to NativeAOT.

So as responsible developer, I decide to fix that flaw. So I go to my pet project WinFormsComInterop and add support there. Once I implemented nescessary changes, I start looking for a target where I can show off what was done, and something simple at the same time. On first Google search I discover article from Scott Hanselman, “How do you use System.Drawing in .NET Core?”. I decide not look further and attempt to run samples from that article, since they are simple.

After inserting magic lines

Samples from the Scott’s article starts magically working.

How that works

Since claiming that my library is super helpful, is not worth of an article, I think would be interesting what stuff drive that magic.

The driver behind System.Drawing is GDI+. GDI+ Flat API which is used by .NET, are mostly plain C API, with some sprinkled IStream as part of parameters. For example:

And that’s what by default make NativeAOT unhappy. That because NativeAOT does not have COM Interop of full blown .NET behind it. Instead of that it relies on ComWrappers API introduced in .NET 5 to work. That put burden on developer to provide proper COM interop for their application. More explanation how that works, I wrote in my article “Using COM in NativeAOT”. So in order to convert .NET object to IStream I have to follow similar pattern and implement COM proxy.

All of that looks boring, because you have to implement each method of interface, and pass data to .NET object using well-established pattern. What is interesting here is that IStream declared as internal interface to System.Drawing.Common, and as such, I cannot cast object to it. Probably I can make that work by using reflection and invoke method directly on object, but that’s too expensive and bloat final size of code. So what to do?

I create separate project which I name System.Drawing.Common and declare interface with same name as in original assembly. I replicate all parameter names, and types, and namespaces to which they are belongs (just necessary types, not all types in namespace). One important distinction, is that I make these types public in my mirror assembly, where in the original one, they was internal. After that I add reference to that project in project where my ComWrapper reside and mark it with PrivateAssets=All.

Then add to my ComWrappers project IgnoreAccessChecksToAttribute class.

And then mark assembly with that attribute, to get access to internals of System.Drawing.Common. Now I was able to use IStream from my mirror assembly to write code, and because reference to it marked with PrivateAssets it would not be used further down the reference tree. Now because in my mirror assembly types are public, I can access them in my ComWrappers project, to compile code, and because of IgnoresAccessChecksTo attribute placed on the assembly, runtime allow me use internal classes from System.Drawing.Common during execution of application.

Results

Final application looks like this

Unfortunately I have to add following lines to project

Because WinFormsComInterop project depends on WinForms, I have to add UseWindowsForms which disable console, and without DisableWinExeOutputInference console output would not be visible which is annoying.

Summary

This produce executable of size 1,6Mb if using only System.Drawing.Common part, and using ImageSharp part size jump a bit to 3.3Mb.

Probably this size comparison a bit incorrect to ImageSharp since I use more of it. But best part of ImageShapr, is that it just works without any ComWrappers magic so you can freely use it in your NativeAOT application.

In conclusion. I believe that NativeAOT works great with more and more spectrum of applications. Play with technology, it allow you to make your application smaller and faster. In general if you afraid to make a switch, any changes which are added to compile application under NativeAOT most likely make your application run faster on regular .NET as well.

Project with sample code: https://github.com/kant2002/nativeaotimages

Trick with mirror System.Drawing.Common was provided by Jan Kotas

Math lover, lost in the woods of software development