SteveSandersonMS / dotnet-wasi-sdk

Packages for building .NET projects as standalone WASI-compliant modules
518 stars 36 forks source link

Task.Delay fails application due to missing "internal call" #42

Open khalidabuhakmeh opened 2 years ago

khalidabuhakmeh commented 2 years ago
var count = 0;
while (true)
{
    Console.WriteLine($"{count++} - Hello, World!");
    await Task.Delay(TimeSpan.FromSeconds(1));
}

Results in the following output.

0 - Hello, World!
[wasm_trace_logger] cant resolve internal call to "System.Threading.TimerQueue::SetTimeout" (tested without signature also)

Your mono runtime and class libraries are out of sync.
The out of sync library is: System.Private.CoreLib.dll

When you update one from git you need to update, compile and install
the other too.
Do not report this as a bug unless you're sure you have updated correctly:
you probably have a broken mono install.
If you see other errors or faults after this message they are probably related
and you need to fix your mono install first.

I think the mention of Mono is a red-hearing since its likely talking about WASM runtime. Note that the code executes and fails at the call to Task.Delay.

Project Information

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <RootNamespace>wasm_lock</RootNamespace>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Wasi.Sdk" Version="0.1.2-preview.10061" />
    </ItemGroup>

</Project>
eduard-dumitru commented 1 year ago

Hi @khalidabuhakmeh, big fan 😅

I've discovered that the error message is misleading.

My 2 cents regarding the problem:

No one wants to use up a thread just to schedule completions of Task.Delay or equivalent operations, duuh, and this only makes sense if the WASI host supports multiple threads to begin with, which is not true for browser tabs or their workers.

Therefore, you need full bi-directional chatter with the WASI host to pull off a Task.Delay.

dotnet-wasi-sdk seems to expect the host to implement the so-called wasi-snapshot-preview1 standard, and by the looks of it, there's no "wake-me-up-in-X" in there.

One way to circumvent the problem

I also think this is the way to solve the problem:

  1. Do what @SteveSandersonMS mentions here.

From what I gather, in his comment, Steve was answering a question about how one could emulate Blazor's JSExportAttribute ability (where you write your own static method and you want to expose it) and possibly how one could call a "custom" function provided by the WASI host.

However, the exact same technique can be used to backfill .NET BCL methods marked extern. So in a sense, instead of defining custom interop calls, you'd be "lighting up .NET capabilities" that were originally deactivated (with catastrophic and misleading errors).

Funny enough, in the very c file Steve points out, we see SetTimeout itself being declared:

mono_add_internal_call ("System.Threading.TimerQueue::SetTimeout", fake_settimeout);
  1. Now, I haven't gone more in-depth, but the function pointer's name ("fake_settimeout") doesn't sound too good to me, and that leads us to the last half of the journey.

You should probably import a function, let's call it "set_timeout", thus requiring the WASI host to provide it, by doing something like this:

__attribute__((import_name("start_http_server")))
void start_http_server (MonoObject* dotnet_http_server, int port);

and then fulfill the import in the WASI host.

That being said, running wasmtime from the command line, and passing it the KhalidConsoleApp.wasm won't work anymore, and neither will running the project in the Visual Studio, which I believe is doing the same thing.

That is because wasmtime implements wasi-snapshot-preview1, and even if it supports more than that, we probably didn't match some obscure 'set-timeout' it might be offering either in name or signature.

I think wasmer is in the same boat. All the while, browsers' WebAssembly hosts don't even offer wasi-snapshot-preview1, they're not WASI at all, but they can be taught to offer both this standard + our custom set-timeout. It's not straightforward but it's possible.

A pure For-.NET-By-.NET solution would be using the wasmtime Nuget package and building your own host:

part of the csproj

<ItemGroup>
<PackageReference Include="Wasmtime" Version="2.0.2" />
</ItemGroup>

and then do something along the lines of (this is not the end of the story, but from here it should be clear what to look for and what to do):

Program.cs


using System.Reflection;
using Wasmtime;

using Engine engine = new();

using Linker linker = new(engine); linker.DefineWasi(); linker.DefineFunction("The module name, gotta do research?", "set-timeout", (int cMillis) => { Task.Delay(TimeSpan.FromMilliseconds(cMillis)).ContinueWith( () => // ... now we need an Export from the Module and that export // should excite the BCL the same way the full blown CLR would do // apparently we don't have any correlation IDs in this whole story // so the CLR must be managing a table of sorts // and this is just a wake up call to check things ) });

using Store store = new(engine); store.SetWasiConfiguration( new WasiConfiguration() .WithInheritedStandardOutput());

const string name = "Runner.ConsoleApp1.wasm";

using var stream = Assembly .GetExecutingAssembly() .GetManifestResourceStream(name)!;

using var module = Wasmtime.Module.FromStream(engine, name, stream);

var instance = linker.Instantiate(store, module);

var _start = instance.GetAction("_start")!; _start();