Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Tasks (JSPI) #31

Merged
merged 24 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
03796e9
Refactoring ProcessThread into an async task
kelnishi Nov 27, 2024
d3ba981
Adding support for Async boundary calls (Task/JSPI)
kelnishi Nov 27, 2024
71e5562
Adding test for Async boundary call
kelnishi Nov 27, 2024
4296ebe
Feature.Detect adding a hardcoded list of supported features
kelnishi Nov 27, 2024
0b98c33
Tweaking Feature.Detect to show conceptually supported JS features
kelnishi Nov 28, 2024
6efff7c
Optimizing Transpiled value unwrap, fields for properties
kelnishi Nov 28, 2024
4f67628
Adding IsAoTCompatible flag to projects
kelnishi Nov 28, 2024
5e7d0e5
Optimization properties and fields for direct access
kelnishi Nov 28, 2024
a0618f6
Optimizing Transpiled value wrap, reduce boxing
kelnishi Nov 28, 2024
cd317f0
Cleaning up stats instrumentation
kelnishi Nov 28, 2024
479bb62
Optimizing more transpiled boxes
kelnishi Nov 28, 2024
42701f6
Code style clean up + removing gas accumulator from Execute
kelnishi Nov 28, 2024
ab41c8d
More transpiler instructions
kelnishi Nov 28, 2024
3c153c8
Lifting Async invocation for synchronous calls
kelnishi Nov 29, 2024
4e29129
Fanning out MemoryOps for direct value passing
kelnishi Nov 29, 2024
dee9a77
Manually inline Sequence within ExecContext
kelnishi Nov 29, 2024
012619c
Updating README.md a bit
kelnishi Nov 29, 2024
70f7773
Implementing async wasm invocation, (stack invoker only)
kelnishi Nov 30, 2024
d141d1a
Adding async wasm test
kelnishi Nov 30, 2024
9301651
WACS-v0.3.0
kelnishi Nov 30, 2024
22a07f6
Updating README.md
kelnishi Dec 1, 2024
8a56a82
Fixing dotnet versions, net8.0;net6.0;netstandard2.1
kelnishi Dec 1, 2024
ce29364
Setting ci dotnet to net8.0
kelnishi Dec 1, 2024
dcd79c6
Removing net6.0
kelnishi Dec 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'

- name: Restore Dependencies
run: dotnet restore
Expand Down
2 changes: 1 addition & 1 deletion .run/Wacs.Core Publish Wacs.Core.dll.run.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Wacs.Core Publish Wacs.Core.dll" type="ShConfigurationType" folderName="Unity Publish">
<option name="SCRIPT_TEXT" value="dotnet publish -c Release -o ../unity/Runtime/Plugins/" />
<option name="SCRIPT_TEXT" value="dotnet publish -f netstandard2.1 -c Release -o ../unity/Runtime/Plugins/" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
Expand Down
2 changes: 1 addition & 1 deletion .run/Wacs.Core Publish Wacs.WASIp1.dll.run.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Wacs.Core Publish Wacs.WASIp1.dll" type="ShConfigurationType" folderName="Unity Publish">
<option name="SCRIPT_TEXT" value="dotnet publish -c Release -o ../unity/Runtime/Plugins/" />
<option name="SCRIPT_TEXT" value="dotnet publish -f netstandard2.1 -c Release -o ../unity/Runtime/Plugins/" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [0.3.0]
- Implemented JSPI-like async binding and execution
- Hooked up more super-instruction threading

## [0.2.0]
- Implemented super-instruction threading
- Precomputed (non-allocating) block labels
Expand Down
29 changes: 26 additions & 3 deletions Feature.Detect/DetectFeatures.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
using System;
// /*
// * Copyright 2024 Kelvin Nishikawa
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Spec.Test;
using Wacs.Core;
using Wacs.Core.Runtime;
using Wacs.Core.Types;
using Xunit;
using Xunit.Abstractions;

namespace Feature.Detect
{
Expand Down Expand Up @@ -40,7 +57,13 @@ public void Detect(FeatureJson.FeatureJson file)
}
else
{
Assert.Fail($"{file.Name} not supported.");
var supportedJsParadigms = new List<string>
{
"jspi"
};

var supported = file.Features?.All(feature => supportedJsParadigms.Contains(feature)) ?? false;
Assert.True(supported, $"{file.Name} not supported.");
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Feature.Detect/Feature.Detect.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>9</LangVersion>
<TargetFrameworks>net8.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion Feature.Detect/FeatureJson/FeatureJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ public override string ToString()
.Append("}");
return sb.ToString();
}

}

public class Options
Expand Down
13 changes: 7 additions & 6 deletions Feature.Detect/trx-to-markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ class TrxToMarkdown {
testDef['Phase'] = 0;
}

testDef['outcome'] = test.outcome;
if (testDef['Source'].includes('in WebAssembly'))
testDef['outcome'] = 'Javascript';
else
testDef['outcome'] = test.outcome;

testDef['outcome'] = 'JS.' + testDef['outcome'];
else if (testDef.Id == 'big-int')
testDef['outcome'] = 'JS.' + testDef['outcome'];

tests.push(testDef);
});
Expand Down Expand Up @@ -96,9 +96,10 @@ class TrxToMarkdown {

let status = '❔';
switch (testDef['outcome']) {
case 'Passed': status = '✅'; break;
case 'Javascript': status = '<span title="Browser idioms, not directly supported">🌐</span>'; break;
case 'Failed': status = '❌'; break;
case 'Passed': status = '✅'; break;
case 'JS.Failed': status = '<span title="Browser idioms, not directly supported">🌐</span>'; break;
case 'JS.Passed': status = '<span title="Browser idiom, but conceptually supported">✳️</span>'; break;
}
markdown.push(`|[${testDef['Name']}](${testDef['Proposal']})|${testDef['Features']}|${status}|`);
});
Expand Down
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The chapters and sections from the spec are commented throughout the source code
- **Unity Compatibility**: Compatible with **Unity 2021.3+** including AOT/IL2CPP modes for iOS.
- **Full WebAssembly MVP Compliance**: Passes the [WebAssembly spec test suite](https://github.com/WebAssembly/spec/tree/main/test/core).
- **Magical Interop**: Host bindings are validated with reflection, no boilerplate code required.
- **Async Tasks**: [JSPI](https://github.com/WebAssembly/js-promise-integration)-like non-blocking calls for async functions.
- **WASI:** Wacs.WASIp1 provides a [wasi\_snapshot\_preview1](https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md) implementation.

## Getting Started
Expand Down Expand Up @@ -228,27 +229,33 @@ How does this differ from executing the wasm instructions linearly with the WACS
- The CLR's implementation can use hardware more effectively (use registers instead of heap memory)
- Avoids instruction fetching and dispatch

In my testing, this leads to roughly 60% higher instruction processing throughput (10Mips -> 16Mips). These gains are situational however.
In my testing, this leads to roughly 60% higher instruction processing throughput (128Mips -> 210Mips). These gains are situational however.
Linking of the instructions into a tree cannot 100% be determined across block boundaries. So in these cases, the transpiler just passes
the sequence through unaltered. So WASM code with lots of function calls or branches will see less benefit.

There's still some headroom for optimization. Optimization is an ongoing process and I have a few other strategies yet to implement.
### Prebaked Block Labels
The design of the WASM VM includes block labelling for branch instructions and a heterogeneous operand/control stack.
WACS uses a split stack that separates operands and control. This enables us to make some key optimizations:
- Non-flushing branch jumps. We can leave operands on the stack if intermediate states don't interfere.
- Precomputed block labels. We can ditch the control frame's label stack entirely!
- Modern C# ObjectPools and ArrayPools minimize unavoidable allocation

Optimization is an ongoing process and I have a few other strategies yet to implement.

My plan for 1.0 includes:
- Prebaked super-instructions for memory operations
- Replace some object pools with pre-computed statics
- Implement the above transpiling for SIMD instructions (currently only i32/i64/f32/f64 instructions are optimized)
- Provide an API for 3rd party super-instruction optimization

## Roadmap

The current TODO list includes:

- **ExecAsync**: Thread scheduling and advanced gas metering (basically JSPI, but C# Tasks)
- **Source Generated Bindings**: Use Roslyn source generator for generating bindings.
- **Wasm Garbage Collection**: Support wasm-gc and heaptypes.
- **Text Format Parsing**: Add support for WebAssembly text format.
- **WASI p1 Test Suite**: Validate WASIp1 with the test suite for improved standard compliance.
- **WASI p2 and Component Model**: Implement the component model proposal.
- **Text Format Parsing**: Add support for WebAssembly text format.
- **SIMD Intrinsics**: Add hardware-accelerated SIMD (software implementation included in Wacs.Core).
- **Unity Bindings for SDL**: Implement SDL2 with Unity bindings.
- **JavaScript Proxy Bindings**: Maybe support common JS env functions.
Expand All @@ -263,7 +270,7 @@ Harnessed results from [wasm-feature-detect](https://github.com/GoogleChromeLabs
|Proposal |Features| |
|------|-------|----|
|Phase 5|
|[JavaScript BigInt to WebAssembly i64 integration](https://github.com/WebAssembly/JS-BigInt-integration)|||
|[JavaScript BigInt to WebAssembly i64 integration](https://github.com/WebAssembly/JS-BigInt-integration)||<span title="Browser idiom, but conceptually supported">✳️</span>|
|[Bulk memory operations](https://github.com/webassembly/bulk-memory-operations)||✅|
|[Extended Constant Expressions](https://github.com/WebAssembly/extended-const)|extended_const|✅|
|[Garbage collection](https://github.com/WebAssembly/gc)|gc|❌|
Expand All @@ -283,7 +290,7 @@ Harnessed results from [wasm-feature-detect](https://github.com/GoogleChromeLabs
|[Memory64](https://github.com/WebAssembly/memory64)|memory64|❌|
|[Threads](https://github.com/webassembly/threads)|threads|❌|
|Phase 3|
|[JS Promise Integration](https://github.com/WebAssembly/js-promise-integration)|jspi|<span title="Browser idioms, not directly supported">🌐</span>|
|[JS Promise Integration](https://github.com/WebAssembly/js-promise-integration)|jspi|<span title="Browser idiom, but conceptually supported">✳️</span>|
|[Type Reflection for WebAssembly JavaScript API](https://github.com/WebAssembly/js-types)|type-reflection|<span title="Browser idioms, not directly supported">🌐</span>|
||
|[Legacy Exception Handling]( https://github.com/WebAssembly/exception-handling)|exceptions|❌|
Expand Down
92 changes: 80 additions & 12 deletions Spec.Test/BindingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
// */

using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Wacs.Core;
using Wacs.Core.Runtime;
using Xunit;
Expand All @@ -24,11 +26,26 @@ namespace Spec.Test
{
public class BindingTests
{
static int BoundHost(int a, out int b)
{
b = a;
int c = a * 2;
return c;
}

static async Task<int> BoundAsyncHost(int a)
{
await Task.Delay(1000); // Simulate work
return a*2;
}


[Fact]
public void BindStackBinder()
{
var runtime = new WasmRuntime();
runtime.BindHostFunction<hostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostAsyncInRet>(("env","bound_async_host"), BoundAsyncHost);
using var fileStream = new FileStream("../../../engine/binding.wasm", FileMode.Open);
var module = BinaryModuleParser.ParseWasm(fileStream);
var moduleInst = runtime.InstantiateModule(module);
Expand All @@ -49,7 +66,8 @@ public void BindStackBinder()
public void BindParamAndResultI32()
{
var runtime = new WasmRuntime();
runtime.BindHostFunction<hostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostAsyncInRet>(("env","bound_async_host"), BoundAsyncHost);
using var fileStream = new FileStream("../../../engine/binding.wasm", FileMode.Open);
var module = BinaryModuleParser.ParseWasm(fileStream);
var moduleInst = runtime.InstantiateModule(module);
Expand All @@ -67,7 +85,8 @@ public void BindParamAndResultI32()
public void BindParamAndResultF32()
{
var runtime = new WasmRuntime();
runtime.BindHostFunction<hostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostAsyncInRet>(("env","bound_async_host"), BoundAsyncHost);
using var fileStream = new FileStream("../../../engine/binding.wasm", FileMode.Open);
var module = BinaryModuleParser.ParseWasm(fileStream);
var moduleInst = runtime.InstantiateModule(module);
Expand All @@ -81,18 +100,12 @@ public void BindParamAndResultF32()
Assert.Equal(3f * 2f, invoker(3f));
}

static int BoundHost(int a, out int b)
{
b = a;
int c = a * 2;
return c;
}

[Fact]
public void BindHostFunction()
{
var runtime = new WasmRuntime();
runtime.BindHostFunction<hostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostAsyncInRet>(("env","bound_async_host"), BoundAsyncHost);

using var fileStream = new FileStream("../../../engine/binding.wasm", FileMode.Open);
var module = BinaryModuleParser.ParseWasm(fileStream);
Expand All @@ -105,6 +118,61 @@ public void BindHostFunction()
Assert.Equal(10 + 20, invoker(10));
}

delegate int hostInOut(int a, out int b);

[Fact]
public void BindAsyncHostFunction()
{
var runtime = new WasmRuntime();
runtime.BindHostFunction<HostInOut>(("env","bound_host"), BoundHost);
runtime.BindHostFunction<HostAsyncInRet>(("env","bound_async_host"), BoundAsyncHost);

using var fileStream = new FileStream("../../../engine/binding.wasm", FileMode.Open);
var module = BinaryModuleParser.ParseWasm(fileStream);
var moduleInst = runtime.InstantiateModule(module);
runtime.RegisterModule("binding", moduleInst);

var fa = runtime.GetExportedFunction(("binding", "call_async_host"));

//Host function is async, Wasm function is called synchronously
var invoker = runtime.CreateInvoker<Func<int, int>>(fa);

Assert.Equal(10*2, invoker(10));
}

[Fact]
public async Task BindAsyncWasmFunction()
{
var runtime = new WasmRuntime();
runtime.BindHostFunction<HostInOut>(("env","bound_host"), BoundHost);

runtime.BindHostFunction<HostAsyncInRet>(("env","bound_async_host"), static async a =>
{
await Task.Delay(3_000); // Simulate work
return a*2;
});

using var fileStream = new FileStream("../../../engine/binding.wasm", FileMode.Open);
var module = BinaryModuleParser.ParseWasm(fileStream);
var moduleInst = runtime.InstantiateModule(module);
runtime.RegisterModule("binding", moduleInst);

var fa = runtime.GetExportedFunction(("binding", "call_async_host"));

//Host function is async, Wasm function is called asynchronously
var invoker = runtime.CreateStackInvokerAsync(fa);

var stopwatch = new Stopwatch();

stopwatch.Start();
var results = await invoker(10);
stopwatch.Stop();

Assert.InRange(stopwatch.ElapsedMilliseconds, 3000, 3500);
Assert.Equal(10*2, results[0].Int32);
}

delegate int HostInOut(int a, out int b);

delegate Task<int> HostAsyncInRet(int a);
}
}
4 changes: 2 additions & 2 deletions Spec.Test/Spec.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<OutputType>Library</OutputType>
<LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
<Title>WebAssembly Spec Wast Test suite harness</Title>
<Authors>Kelvin Nishikawa</Authors>
<Company>Kelvin Nishikawa</Company>
<Product>Spec.Test</Product>
<AssemblyVersion>1.0.1</AssemblyVersion>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
Expand Down
5 changes: 3 additions & 2 deletions Spec.Test/WastJson/Argument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Wacs.Core.Runtime;
Expand Down Expand Up @@ -137,7 +138,7 @@ private static float BitBashFloat(string intval)
if (value > int.MaxValue && value <= uint.MaxValue)
{
uint v = (uint)value;
return BitConverter.UInt32BitsToSingle(v);
return MemoryMarshal.Cast<uint, float>(MemoryMarshal.CreateSpan(ref v, 1))[0];
}

return BitConverter.Int32BitsToSingle((int)value);
Expand All @@ -156,7 +157,7 @@ private static double BitBashDouble(string longval)
if (value > long.MaxValue && value <= ulong.MaxValue)
{
ulong v = (ulong)value;
return BitConverter.UInt64BitsToDouble(v);
return MemoryMarshal.Cast<ulong, double>(MemoryMarshal.CreateSpan(ref v, 1))[0];
}

return BitConverter.Int64BitsToDouble((long)value);
Expand Down
2 changes: 1 addition & 1 deletion Spec.Test/WastTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void RunWast(WastJson.WastJson file)
}
}
}

[Theory]
[ClassData(typeof(WastJsonTestData))]
public void RunWastTranspiled(WastJson.WastJson file)
Expand Down
Binary file modified Spec.Test/engine/binding.wasm
Binary file not shown.
Loading
Loading