How do you use System.Drawing in NativeAOT?
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
Then add to my ComWrappers project
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.
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.
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