RyanLamansky / dotnet-webassembly

Create, read, modify, write and execute WebAssembly (WASM) files from .NET-based applications.
Apache License 2.0
789 stars 74 forks source link

Running WASM from inside Unity #20

Open Verdagon opened 4 years ago

Verdagon commented 4 years ago

Do you know if we can use dotnet-webassembly to run wasm from inside Unity? That would be a gamechanger!

(I'd be happy to spend a couple days testing it out if it's not known already!)

RyanLamansky commented 4 years ago

It should work on any platform that supports System.Reflection.Emit. This means no iOS, but you should be okay on other mainstream platforms.

I haven't heard of anyone trying it yet, so if you move forward I'm curious how well it works for you 🙂

Verdagon commented 4 years ago

Thanks for the intel! Looking at https://docs.unity3d.com/Manual/ScriptingRestrictions.html is looks like most Unity targets don't support System.Reflection.Emit... but some do! Universal Windows Platform, and Mono-based Standalone/Android/WiiU should be able to do it.

I'm gonna try it with UWP, and if that doesn't work, Mono Standalone, and report back here. Fingers crossed!

I know that using .dll files from Unity works, so if dotnet-webassembly could generate a .dll, I think that's mission accomplished. Is that what "Support saving generated assemblies as DLLs on .NET Framework via the above extensibility mechanism" in the post-1.0 means?

Verdagon commented 4 years ago

Realized after posting it that UWP won't work because it's restricted to .NET Core builds (as per ScriptingRestrictions link).

So, I tried the Mono Standalone versions, and alas, no luck =( It gives me this exception:

PlatformNotSupportedException: Operation is not supported on this platform. at WebAssembly.Runtime.Compile.FromBinary (WebAssembly.Reader reader, WebAssembly.Runtime.CompilerConfiguration configuration, System.Type instanceContainer, System.Type exportContainer) [0x0006e] in <1ae70f10ad514fce9dfa9d7defb7d199>:0 at WebAssembly.Runtime.Compile.FromBinary[TExports] (System.IO.Stream input, WebAssembly.Runtime.CompilerConfiguration configuration) [0x00048] in <1ae70f10ad514fce9dfa9d7defb7d199>:0 Rethrow as ModuleLoadException: At offset 8: Operation is not supported on this platform. at WebAssembly.Runtime.Compile.FromBinary[TExports] (System.IO.Stream input, WebAssembly.Runtime.CompilerConfiguration configuration) [0x000d4] in <1ae70f10ad514fce9dfa9d7defb7d199>:0 at WebAssembly.Runtime.Compile.FromBinary[TExports] (System.IO.Stream input) [0x00006] in <1ae70f10ad514fce9dfa9d7defb7d199>:0 at WebAssembly.Module.Compile[TExports] () [0x00015] in <1ae70f10ad514fce9dfa9d7defb7d199>:0 at IncendianFalls.RootPresenter.DoWasmThing () [0x000c6] in D:\IncendianFalls\Assets\IncendianFalls\Scripts\RootPresenter.cs:158 at IncendianFalls.RootPresenter.Start () [0x0000c] in D:\IncendianFalls\Assets\IncendianFalls\Scripts\RootPresenter.cs:66

Any ideas? If not, I imagine things will work once dotnet-webassembly can save .dll files.

Verdagon commented 4 years ago

Oh, I should mention, I got the above exception when trying to run the "Create and execute a WebAssembly file in memory" sample, on the "var instanceCreator = module.Compile();" line.

Verdagon commented 4 years ago

https://github.com/dotnet/corefx/issues/4491#issuecomment-469009414 mentions an alternative for saving .dll files, how long do you think it would take for someone like me to contribute to make dotnet-webassembly use that, and do you think that would accomplish my goals?

RyanLamansky commented 4 years ago

If I'm mapping that offset of that exception correctly, it's dying on DefineDynamicAssembly, which is the sort of place one might expect to see a PlatformNotSupportedException.

The alternative solution behind that link might work, though some effort may be needed to get it integrated. You're certainly welcome to make a fork and give it a try--the file you'll need to do the most work on is https://github.com/RyanLamansky/dotnet-webassembly/blob/master/WebAssembly/Runtime/Compile.cs .

As far as WebAssembly for .NET goes, my most pressing concern right now is getting to 100% spec compliance. Even if it supported AoT today, that's of little use if real-world WASMs don't work as expected. I'm in the "last 10% takes 90% of the time" phase of this project in that regard 😅

Verdagon commented 4 years ago

Did some more digging, it turns out Unity doesn't support .NET Core: https://docs.unity3d.com/2019.3/Documentation/Manual/dotnetProfileSupport.html Quite unfortunate.

Perhaps I could make dotnet-webassembly output .NET Standard .dll files? Looking at https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.assemblybuilder?view=netframework-4.8 it might be pretty easy, so I'll try it out. Is that the kind of approach you had in mind for "Support saving generated assemblies as DLLs on .NET Framework via the above extensibility mechanism."?

RyanLamansky commented 4 years ago

I'm mostly waiting on this: https://github.com/dotnet/corefx/issues/4491

When that's done, .NET Core (and maybe even .NET Standard) will have a built-in mechanism to create DLLs. Then this library could be part of a build pipeline or be used for one-time conversions.

It wouldn't be too hard to make a .NET 4.x version of this library, considering that I did so officially until earlier this year, which may give you a path to what you need before .NET Core is ready.

Verdagon commented 4 years ago

I've been hacking at it a bit, learning a lot, and I think I've made progress... I've made an alternate csproj which uses .NET Framework instead of core, and added an assemblyBuilder.Save("something.dll"); line (more at https://github.com/Verdagon/dotnet-webassembly/commit/59ab3e9f47489d4c83f3a8c133e3ff491a3c1ab9)

I'm hitting a "System.InvalidOperationException: 'Cannot save a transient assembly.'" during that Save call, which sounds like a great stopping point for now. If you have any ideas before I dive back in, they'd be much appreciated =D

RyanLamansky commented 4 years ago

Change AssemblyBuilderAccess.RunAndCollect to AssemblyBuilderAccess.Save. Other changes will be needed but this will get you past this blocker.

Verdagon commented 4 years ago

That worked, it produced a .dll file! Thanks for the tip!

It seems to be empty according to the object browser and ildasm, so the next step is for me to figure out why my functions aren't getting in there (I suspect my test program just isn't populating Module correctly).

The assembly is correctly calling itself CompiledWebAssembly, this is a very promising start!

Verdagon commented 4 years ago

No dice =( I did the same thing your TableImportTests did, with the same table2.wasm, and it still made an empty .dll file.

Any idea what I should try next? Otherwise, I'm thinking I'll go backwards from a program that does create a non-empty .dll file, and slowly make it call the same things in the same order as your program does, to see when it breaks.

RyanLamansky commented 4 years ago

There's probably a missing step at the very end where it actually finalizes all the generated objects into the DLL.

I know the process works because what you're doing, I did myself in the early phases of this project when I wanted to make sure my code generation was correct.

I'll take a look at your fork tomorrow and see if I can figure it out.

Verdagon commented 4 years ago

Thanks for offering to take a look! Luckily, I found a way past that roadblock.

In addition to the Save call I added, I needed to pass another parameter to DefineDynamicModule, like in this answer: https://stackoverflow.com/a/3963033. Suddenly, there's a CompiledWebAssembly.dll with things in it!

The next mystery: when I try to use it from visual studio, I get the error "A reference to CompiledWebAssembly.dll could not be added. Please make sure that the file is accessible, and that it is a valid assembly or COM component."

As per https://stackoverflow.com/a/15832691, I ran regsvr32 something.dll as admin, and I got the error "The module CompiledWebAssembly.dll was loaded but the entry-point DllRegisterServer was not found. Make sure that CompiledWebAssembly.dll is a valid DLL or OCX file and then try again."

Some googling later, and I found https://stackoverflow.com/a/13948668 which confirms that the .dll is expected to have a DllRegisterServer method.

Tomorrow I'll try generating an empty DllRegisterServer method, to see if that helps.

Verdagon commented 4 years ago

Not much progress. I discovered that besides regsvr32 there's a "regasm" program which also might register my .dll, but I'm starting to suspect that I'm down the wrong path... because I opened a random other .dll in ildasm (a MathNet.Numerics dll) and it doesn't have a DllRegisterServer method at all! And Visual Studio loads it just fine...

Their dll does however have a bunch of custom attributes in the manifest, which look like witchcraft. My next step is probably to copy all those weird custom attributes into mine and see what happens, and slowly make my dll more and more like theirs. Fingers crossed!

Verdagon commented 4 years ago

Another clue! I tried loading it at runtime with Assembly.Load instead of through Visual Studio and I got the error "The module was expected to contain an assembly manifest." which hints that my manifest isn't right yet.

Random question: does dotnet-webassembly make a managed .dll or a native .dll?

Suchiman commented 4 years ago

@Verdagon System.Reflection.Emit, which dotnet-webassembly uses, produces managed assemblies but unlike .dlls typically compiled by the C# compiler which compile against Reference Assemblies, they compile against implementation assemblies, so they're littered with implementation details of a specific runtime and might not work on a different runtime.

RyanLamansky commented 4 years ago

@Suchiman Interesting. What kind of changes are required to target reference assemblies?

Suchiman commented 4 years ago

@RyanLamansky i don't know if that's possible, i wanted to peek at boo which uses SRE too but it seems they also have that issue https://github.com/boo-lang/boo/issues/201#issuecomment-537466606 . If it's possible, then in places where you're using e.g. typeof(int) and pass that Type object to SRE, that would need to be reflected from the Reference Assembly instead (somehow, it must be avoided to "resolve" the reference assembly when trying to inspect its types). Roslyn uses System.Reflection.Metadata which is a very low level and painful API to read and write assemblies.

Verdagon commented 4 years ago

Before I dive into that, can either of you give me some insight/guess of the chances of success and how long it would take? Or perhaps, additional tips besides the one in the Boo thread comment? Some extra hope and guidance would be much appreciated!

Also, hold my beer / check my math: what if we just accepted that the .dll wouldn't be portable? But alas, that would mean that the eventual machine that runs the .dll would crash... except... Unity has a mode where instead of shipping the .dll as part of the client, it instead uses the IL2CPP tool to convert, at build time, on this computer, from CLR to native code, so it can ship a game with only native code. In other words, if we run dotnet-webassembly on the same machine (and same framework version) that will be running IL2CPP, then maybe it's fine that the .dll isn't super universal/portable. Does that sound like it would work?

RyanLamansky commented 4 years ago

WASM is fundamentally simple. It doesn't (yet) intrinsically support objects at all, for one thing, and it only has 4 types (int32/64, float32/64). So, any dependencies it takes on the .NET Framework or WebAssembly for .NET itself are purely for convenience. It never mattered for the JIT-oriented approach I've taken so far.

Probably the easiest approach would be to make the minimal modifications to this library to make it emit a DLL, use an IL disassembler to convert it to IL text, use text processor to switch out the framework-specific bindings, and then reassemble.

Verdagon commented 4 years ago

Questions for @Suchiman :

Re: "in places where you're using e.g. typeof(int) and pass that Type object to SRE, that would need to be reflected from the Reference Assembly instead (somehow, it must be avoided to "resolve" the reference assembly when trying to inspect its types)."

Re: "Roslyn uses System.Reflection.Metadata which is a very low level and painful API to read and write assemblies."

Questions for @RyanLamansky :

Re: "It doesn't (yet) intrinsically support objects at all, for one thing, and it only has 4 types (int32/64, float32/64)."

Re: "switch out the framework-specific bindings"

Thanks, to both of you!

Suchiman commented 4 years ago

Does this mean that an 'int' on my machine or .NET framework is different from the one on yours? Is there a universal 'int' which is agreed upon by all reusable/universal .dlls? If so, is the "reference assembly" where we get that int?

Primarily it's about the assembly the type is contained in. On .NET Framework, those types live in mscorlib.dll which is a huge monolith dll. On .NET Core, it lives contractually in System.Runtime.dll but practically it lives in System.Private.CoreLib.dll and System.Runtime.dll type forwards this to the latter. This is important, because in IL, each type reference is prefixed with the assembly the type is contained within, e.g. [mscorlib]System.Int32. If you were now to compile on .NET Core at runtime, you'd get [System.Private.CoreLib]System.Int32 and that type couldn't be found on other runtimes because the assembly couldn't be found, that is also why initially, .NET Core couldn't load .NET Framework dlls. To bridge this gap, .NET Core added a mscorlib.dll that type forwards to System.Private.CoreLib.dll like System.Runtime does. A Reference Assembly is an assembly you "reference" for compilation purposes, that contains the public API contract (but no implementation) of a given version, e.g. there are reference assemblies for each version of .NET Framework and .NET Core, that's how you can target different versions of .NET Framework and only see APIs available in that version while only being able to install one version of .NET Framework.

I thought System.Reflection.Emit was made for compilers... I'm surprised to hear that we can't use it, is that an oversight on Microsoft's part? Perhaps a bug?

The primary goal of that API was to allow runtime code generation, it wasn't strictly designed for compilers. Roslyn had a System.Reflection.Emit codegen backend in its early prototype releases, but they've removed that before open sourcing because its limitations don't allow to implement all of C#'s features.

Are we supposed to use System.Reflection.Metadata instead of System.Reflection.Emit, or with it?

That depends on what you're trying to achieve, e.g. for runtime codegen only its fine, Saving assemblies isn't supported on Core (because of the complications around implementation details) and is more considered a caching mechanismus than a distribution mechanismus. Alternate choices also include Mono.Cecil which is popular.

Do we use System.Reflection.Metadata for the instructions themselves, or just for the metadata of functions and types?

That i don't know how roslyn does it, System.Reflection.Metadata provides access to the metadata tables in .NET Assemblies but the IL instruction stream is stored as a blob in these tables.

RyanLamansky commented 4 years ago

That's surprising! Does that mean pointers are represented as int32/64?

The 1.0 spec of WebAssembly's memory is accessed using unsigned 32-bit offsets from a base of 0. This gives an effective max raw memory of 4 GiB. You can theoretically go far beyond that using global variables--the spec gives you room for 2^32 of those, so that's another 32 GiB if they were all int64/float64. That's not reachable in practice: V8 has a hard-coded limit of 1 million globals and 2 GiB of memory and although WebAssembly for .NET doesn't have any hard-coded limits, you'll hit CLR limits somewhere between V8 and the theoretical maximum.

Okay, onto the hard technical stuff: Int32 may not be the best example because that's natively represented in IL, so if you're not calling any methods on it (eg, .ToString()), there wouldn't be any [mscorlib]System.Int32 references in the generated IL.

I can think of a few definite issues right off the top of my head, though.

If the WASM uses any not-in-CIL math instructions, (like f32.sqrt) you'll see a reference to [mscorlib]System.Math, if it uses memory, you'll see a reference to WebAssembly for .NET's UnmanagedMemory class. Imports or exports will also take dependencies, and you'll definitely have those for the library to be usable.

I think that can be worked around by post-processing the generated DLL with an IL disassembly+text replace+reassemble. You can write C# apps to generate .NET Standard DLLs and decompile those if you're not sure what something's supposed to look like, which would avoid the need to try to reverse engineer Rosyln.

You won't be able to work around the fact that UnmanagedMemory very recently added some run-time code generation to leverage the not-accessible-from-C# initblk CIL instruction for fast memory initialization. This week I experimentally confirmed that using some "unsafe" C# code to zero-out allocated memory is only 5-10% slower, so I'll probably switch to that this weekend to help with AoT projects like this.

Longer term, I'm thinking I should reshuffle my priorities to do more to make AoT possible, since that's where the greatest interest in WASM on .NET has been forever and spec compliance is in an "okay" state right now... 🤔

Verdagon commented 4 years ago

Doing text processing and refactoring the whole project to use System.Reflection.Metadata or Cecil or an entire text processing stage is a bit outside of my time constraints =( but I'm still attempting to make a DLL that can at least be used on this machine, to accomplish the goal. If I can do that, and show that Unity can use new languages, maybe some enterprising individual with more .NET expertise can make it portable enough to work on more than the IL2CPP target.

Thank you for answering all those questions! It all makes sense. Two questions remain:

  1. I think I've narrowed down my manifest problem to the fact that I'm producing multiple different .dll files... it seems like one is being produced for "the assembly" (with good manifest and no classes/functions) and one is being produced for each module (with too small manifest, but does have classes/functions). Do either of you know a way I can trick System.Reflection.Emit into making a "multi-file assembly"?

  2. So, is mscorlib.dll the reference assembly? Or would I have to make my own?

kkukshtel commented 4 years ago

Just wanted to check in here - did you ever get this working?

Verdagon commented 4 years ago

Alas, never did! It will take some CLR and DLL knowledge to really figure out how to make this work.

toasterpic commented 3 years ago

Can https://github.com/ericsink/wasm2cil help here?

kkukshtel commented 3 years ago

@Verdagon I was also wondering last week if something like wasmer would help here? Basically embed wasmer in the unity sln and use that to load wasm stuff?

Verdagon commented 2 years ago

Unity might finally be moving on from .NET framework to .NET Core, judging by this thread: https://forum.unity.com/threads/net-6-support.1059992/

...in which case, we might be able to use dotnet-webassembly inside Unity!

waldobronchart commented 2 years ago

@Verdagon I haven't tried yet, but it seems like Wasmer might work in unity with the C# bindings. The bindings might be out of date, the Wasmer project is still active but the C# bindings were written 2 years ago. Latest runtimes can be found here: https://github.com/wasmerio/wasmer/releases

marwie commented 2 years ago

Did a quick test using wasmer 0.7.0 (howto) and assemblyscript and that worked in 2020.3.25

andybak commented 1 year ago

Did a quick test using wasmer 0.7.0 (howto) and assemblyscript and that worked in 2020.3.25

Any chance you could upload your test somewhere? Haven't a working Unity project as a starting point would save me a ton of time.

marwie commented 1 year ago

Did a quick test using wasmer 0.7.0 (howto) and assemblyscript and that worked in 2020.3.25

Any chance you could upload your test somewhere? Haven't a working Unity project as a starting point would save me a ton of time.

Oh I'm afraid I don't have the project anymore and don't seem to have made a repo for it (search is running through my local git folder tho - I'll upload it here if it happens to still exist)

shadowfire-ew commented 16 hours ago

Did a quick test using wasmer 0.7.0 (howto) and assemblyscript and that worked in 2020.3.25

Any chance you could upload your test somewhere? Haven't a working Unity project as a starting point would save me a ton of time.

Oh I'm afraid I don't have the project anymore and don't seem to have made a repo for it (search is running through my local git folder tho - I'll upload it here if it happens to still exist)

I managed to get wasmer, wasmtime, and extism to all work in unity. So far, I got i to work for the editor and windows standalone (as i have a windows box currently) but I am willing to bet it will work on linux (once I put the linux runtimes back into the project). You can check it out here.