1
0
Fork 0
mirror of https://github.com/VSadov/Satori.git synced 2025-06-08 03:27:04 +09:00

[WASI] timers based on wasi:clocks (#105879)

This commit is contained in:
Pavel Savara 2024-08-08 10:59:18 +02:00 committed by GitHub
parent 019d7580a2
commit 7c0364edf7
Signed by: github
GPG key ID: B5690EEEBB952194
19 changed files with 474 additions and 89 deletions

View file

@ -20,8 +20,6 @@
<ILLinkTrimOutputPath>$(IntermediateOutputPath)</ILLinkTrimOutputPath>
<ILLinkDescriptorsXml Condition="'$(ILLinkDescriptorsXml)' == '' and Exists('$(ILLinkDirectory)ILLink.Descriptors.xml')">$(ILLinkDirectory)ILLink.Descriptors.xml</ILLinkDescriptorsXml>
<!-- ILLink.Descriptors.LibraryBuild.xml files are only used during building the library, not an app. They shouldn't be embedded into the assembly. -->
<ILLinkDescriptorsLibraryBuildXml Condition="'$(ILLinkDescriptorsLibraryBuildXml)' == '' and Exists('$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml')">$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml</ILLinkDescriptorsLibraryBuildXml>
<ILLinkDescriptorsXmlIntermediatePath>$(IntermediateOutputPath)ILLink.Descriptors.xml</ILLinkDescriptorsXmlIntermediatePath>
<ILLinkSubstitutionsXmlIntermediatePath>$(IntermediateOutputPath)ILLink.Substitutions.xml</ILLinkSubstitutionsXmlIntermediatePath>
@ -41,6 +39,9 @@
</PropertyGroup>
<ItemGroup>
<!-- ILLink.Descriptors.LibraryBuild.xml files are only used during building the library, not an app. They shouldn't be embedded into the assembly. -->
<ILLinkDescriptorsLibraryBuildXml Include="$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml"
Condition="Exists('$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml')" />
<ILLinkSuppressionsLibraryBuildXml Include="$(ILLinkSuppressionsXmlPrefix).LibraryBuild.xml"
Condition="Exists('$(ILLinkSuppressionsXmlPrefix).LibraryBuild.xml')" />
@ -210,7 +211,7 @@
<ILLinkArgs Condition="'$(ILLinkRewritePDBs)' == 'true' and Exists('$(ILLinkTrimAssemblySymbols)')">$(ILLinkArgs) -b true</ILLinkArgs>
<ILLinkArgs Condition="'$(ILLinkRewritePDBs)' == 'true' and Exists('$(ILLinkTrimAssemblySymbols)') and '$(DeterministicSourcePaths)' == 'true'">$(ILLinkArgs) --preserve-symbol-paths</ILLinkArgs>
<!-- pass the non-embedded descriptors xml file on the command line -->
<ILLinkArgs Condition="'$(ILLinkDescriptorsLibraryBuildXml)' != ''">$(ILLinkArgs) -x "$(ILLinkDescriptorsLibraryBuildXml)"</ILLinkArgs>
<ILLinkArgs Condition="'@(ILLinkDescriptorsLibraryBuildXml)' != ''">$(ILLinkArgs) -x "@(ILLinkDescriptorsLibraryBuildXml->'%(FullPath)', '" -x "')"</ILLinkArgs>
<ILLinkArgs Condition="'$(ILLinkSubstitutionsLibraryBuildXml)' != ''">$(ILLinkArgs) --substitutions "$(ILLinkSubstitutionsLibraryBuildXml)"</ILLinkArgs>
<ILLinkArgs Condition="'@(ILLinkSuppressionsLibraryBuildXml)' != ''">$(ILLinkArgs) --link-attributes "@(ILLinkSuppressionsLibraryBuildXml->'%(FullPath)', '" --link-attributes "')"</ILLinkArgs>
<!-- suppress warnings with the following codes:

View file

@ -3,16 +3,35 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.TestRunners.Common;
using Microsoft.DotNet.XHarness.TestRunners.Xunit;
using System.Runtime.CompilerServices;
public class WasmTestRunner : WasmApplicationEntryPoint
{
protected int MaxParallelThreadsFromArg { get; set; }
protected override int? MaxParallelThreads => RunInParallel ? MaxParallelThreadsFromArg : base.MaxParallelThreads;
public static async Task<int> Main(string[] args)
#if TARGET_WASI
public static int Main(string[] args)
{
return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
static extern int PollWasiEventLoopUntilResolved(Thread t, Task<int> mainTask);
}
#else
public static Task<int> Main(string[] args)
{
return MainAsync(args);
}
#endif
public static async Task<int> MainAsync(string[] args)
{
if (args.Length == 0)
{

View file

@ -2,7 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<TargetFrameworks>$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent)</TargetFrameworks>
</PropertyGroup>
<!-- DesignTimeBuild requires all the TargetFramework Derived Properties to not be present in the first property group. -->
<PropertyGroup>
<TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'browser'">$(DefineConstants);TARGET_BROWSER</DefineConstants>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'wasi'">$(DefineConstants);TARGET_WASI</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="WasmTestRunner.cs" />

View file

@ -314,7 +314,7 @@ namespace System.Net.Http
}
else
{
await WasiEventLoop.RegisterWasiPollable(future.Subscribe()).ConfigureAwait(false);
await RegisterWasiPollable(future.Subscribe()).ConfigureAwait(false);
}
}
}
@ -461,19 +461,22 @@ namespace System.Net.Http
}
}
private static class WasiEventLoop
private static Task RegisterWasiPollable(IPoll.Pollable pollable)
{
internal static Task RegisterWasiPollable(IPoll.Pollable pollable)
{
var handle = pollable.Handle;
pollable.Handle = 0;
return CallRegisterWasiPollable((Thread)null!, handle);
var handle = pollable.Handle;
// this will effectively neutralize Dispose() of the Pollable()
// because in the CoreLib we create another instance, which will dispose it
pollable.Handle = 0;
GC.SuppressFinalize(pollable);
return CallRegisterWasiPollableHandle((Thread)null!, handle);
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "RegisterWasiPollable")]
static extern Task CallRegisterWasiPollable(Thread t, int handle);
}
}
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "RegisterWasiPollableHandle")]
private static extern Task CallRegisterWasiPollableHandle(Thread t, int handle);
private sealed class InputStream : Stream
{
private ITypes.IncomingBody body;
@ -559,8 +562,7 @@ namespace System.Net.Http
var buffer = result;
if (buffer.Length == 0)
{
await WasiEventLoop
.RegisterWasiPollable(stream.Subscribe())
await RegisterWasiPollable(stream.Subscribe())
.ConfigureAwait(false);
}
else
@ -697,7 +699,7 @@ namespace System.Net.Http
var count = (int)stream.CheckWrite();
if (count == 0)
{
await WasiEventLoop.RegisterWasiPollable(stream.Subscribe()).ConfigureAwait(false);
await RegisterWasiPollable(stream.Subscribe()).ConfigureAwait(false);
}
else if (offset == limit)
{

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<linker>
<assembly fullname="System.Private.CoreLib">
<!-- these methods are temporarily accessed via UnsafeAccessor from generated code until we have it in public API, probably in WASI preview3 and promises -->
<type fullname="System.Threading.Thread">
<method name="RegisterWasiPollableHandle" />
<method name="PollWasiEventLoopUntilResolved" />
</type>
</assembly>
</linker>

View file

@ -64,10 +64,9 @@
<ILLinkSubstitutionsXmls Include="$(ILLinkSharedDirectory)ILLink.Substitutions.Browser.xml" Condition="'$(TargetsBrowser)' == 'true'" />
<ILLinkLinkAttributesXmls Include="$(ILLinkSharedDirectory)ILLink.LinkAttributes.Shared.xml" />
<ILLinkSuppressionsLibraryBuildXml Include="$(ILLinkSharedDirectory)ILLink.Suppressions.LibraryBuild.xml" />
<ILLinkDescriptorsLibraryBuildXml Include="$(ILLinkSharedDirectory)ILLink.Descriptors.LibraryBuild.xml" />
<ILLinkDescriptorsLibraryBuildXml Include="$(ILLinkSharedDirectory)ILLink.Descriptors.LibraryBuild.WASI.xml" Condition="'$(TargetsWasi)' == 'true'" />
</ItemGroup>
<PropertyGroup>
<ILLinkDescriptorsLibraryBuildXml>$(ILLinkSharedDirectory)ILLink.Descriptors.LibraryBuild.xml</ILLinkDescriptorsLibraryBuildXml>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Internal\AssemblyAttributes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\Console.cs" />
@ -2804,6 +2803,7 @@
<ItemGroup Condition="'$(TargetsWasi)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiEventLoop.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPoll.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPollWorld.wit.imports.wasi.clocks.v0_2_1.MonotonicClockInterop.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPollWorld.wit.imports.wasi.io.v0_2_1.PollInterop.cs" />
</ItemGroup>

View file

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.Runtime;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.Threading.Tasks;
namespace System.Threading
{
@ -12,15 +13,26 @@ namespace System.Threading
{
// these methods are temporarily accessed via UnsafeAccessor from generated code until we have it in public API, probably in WASI preview3 and promises
#if TARGET_WASI
internal static System.Threading.Tasks.Task RegisterWasiPollable(int handle)
internal static System.Threading.Tasks.Task RegisterWasiPollableHandle(int handle)
{
return WasiEventLoop.RegisterWasiPollable(handle);
return WasiEventLoop.RegisterWasiPollableHandle(handle);
}
internal static void DispatchWasiEventLoop()
internal static int PollWasiEventLoopUntilResolved(Task<int> mainTask)
{
WasiEventLoop.DispatchWasiEventLoop();
while (!mainTask.IsCompleted)
{
WasiEventLoop.DispatchWasiEventLoop();
}
var exception = mainTask.Exception;
if (exception is not null)
{
throw exception;
}
return mainTask.Result;
}
#endif
// the closest analog to Sleep(0) on Unix is sched_yield

View file

@ -9,43 +9,64 @@ namespace System.Threading
{
internal static class WasiEventLoop
{
private static List<(IPoll.Pollable, TaskCompletionSource)> pollables = new();
private static List<WeakReference<TaskCompletionSource>> s_pollables = new();
internal static Task RegisterWasiPollable(int handle)
internal static Task RegisterWasiPollableHandle(int handle)
{
var source = new TaskCompletionSource(TaskCreationOptions.AttachedToParent);
pollables.Add((new IPoll.Pollable(new IPoll.Pollable.THandle(handle)), source));
return source.Task;
// note that this is duplicate of the original Pollable
// the original should be neutralized without disposing the handle
var pollableCpy = new IPoll.Pollable(new IPoll.Pollable.THandle(handle));
return RegisterWasiPollable(pollableCpy);
}
internal static Task RegisterWasiPollable(IPoll.Pollable pollable)
{
var tcs = new TaskCompletionSource(pollable);
var weakRef = new WeakReference<TaskCompletionSource>(tcs);
s_pollables.Add(weakRef);
return tcs.Task;
}
internal static void DispatchWasiEventLoop()
{
ThreadPoolWorkQueue.Dispatch();
if (WasiEventLoop.pollables.Count > 0)
if (s_pollables.Count > 0)
{
var pollables = WasiEventLoop.pollables;
WasiEventLoop.pollables = new();
var arguments = new List<IPoll.Pollable>();
var sources = new List<TaskCompletionSource>();
foreach ((var pollable, var source) in pollables)
var pollables = s_pollables;
s_pollables = new List<WeakReference<TaskCompletionSource>>(pollables.Count);
var arguments = new List<IPoll.Pollable>(pollables.Count);
var indexes = new List<int>(pollables.Count);
for (var i = 0; i < pollables.Count; i++)
{
arguments.Add(pollable);
sources.Add(source);
var weakRef = pollables[i];
if (weakRef.TryGetTarget(out TaskCompletionSource? tcs))
{
var pollable = (IPoll.Pollable)tcs!.Task.AsyncState!;
arguments.Add(pollable);
indexes.Add(i);
}
}
var results = PollInterop.Poll(arguments);
// this is blocking until at least one pollable resolves
var readyIndexes = PollInterop.Poll(arguments);
var ready = new bool[arguments.Count];
foreach (var result in results)
foreach (int readyIndex in readyIndexes)
{
ready[result] = true;
arguments[(int)result].Dispose();
sources[(int)result].SetResult();
ready[readyIndex] = true;
arguments[readyIndex].Dispose();
var weakRef = pollables[indexes[readyIndex]];
if (weakRef.TryGetTarget(out TaskCompletionSource? tcs))
{
tcs!.SetResult();
}
}
for (var i = 0; i < arguments.Count; ++i)
{
if (!ready[i])
{
WasiEventLoop.pollables.Add((arguments[i], sources[i]));
s_pollables.Add(pollables[indexes[i]]);
}
}
}

View file

@ -0,0 +1,81 @@
// Generated by `wit-bindgen` 0.29.0. DO NOT EDIT!
// <auto-generated />
#nullable enable
using System;
using System.Runtime.CompilerServices;
using System.Collections;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace WasiPollWorld.wit.imports.wasi.clocks.v0_2_1
{
internal static class MonotonicClockInterop {
internal static class NowWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "now"), WasmImportLinkage]
internal static extern long wasmImportNow();
}
internal static unsafe ulong Now()
{
var result = NowWasmInterop.wasmImportNow();
return unchecked((ulong)(result));
//TODO: free alloc handle (interopString) if exists
}
internal static class ResolutionWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "resolution"), WasmImportLinkage]
internal static extern long wasmImportResolution();
}
internal static unsafe ulong Resolution()
{
var result = ResolutionWasmInterop.wasmImportResolution();
return unchecked((ulong)(result));
//TODO: free alloc handle (interopString) if exists
}
internal static class SubscribeInstantWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "subscribe-instant"), WasmImportLinkage]
internal static extern int wasmImportSubscribeInstant(long p0);
}
internal static unsafe global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable SubscribeInstant(ulong when)
{
var result = SubscribeInstantWasmInterop.wasmImportSubscribeInstant(unchecked((long)(when)));
var resource = new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable(new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable.THandle(result));
return resource;
//TODO: free alloc handle (interopString) if exists
}
internal static class SubscribeDurationWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "subscribe-duration"), WasmImportLinkage]
internal static extern int wasmImportSubscribeDuration(long p0);
}
internal static unsafe global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable SubscribeDuration(ulong when)
{
var result = SubscribeDurationWasmInterop.wasmImportSubscribeDuration(unchecked((long)(when)));
var resource = new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable(new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable.THandle(result));
return resource;
//TODO: free alloc handle (interopString) if exists
}
}
}

View file

@ -17,6 +17,7 @@ tar xzf v0.2.1.tar.gz
cat >wasi-http-0.2.1/wit/world.wit <<EOF
world wasi-poll {
import wasi:io/poll@0.2.1;
import wasi:clocks/monotonic-clock@0.2.1;
}
EOF
wit-bindgen c-sharp -w wasi-poll -r native-aot --internal --skip-support-files wasi-http-0.2.1/wit

View file

@ -581,6 +581,7 @@
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Buffers.Tests\System.Buffers.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Globalization.Tests\Invariant\Invariant.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Globalization.Tests\System.Globalization.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Threading.Timer.Tests\System.Threading.Timer.Tests.csproj" />
</ItemGroup>
<!-- wasi/aot smoke tests -->

View file

@ -9,6 +9,9 @@ using System.Runtime.InteropServices;
namespace System.Threading
{
#if FEATURE_WASM_MANAGED_THREADS
#error when compiled with FEATURE_WASM_MANAGED_THREADS, we use TimerQueue.Portable.cs
#endif
//
// Browser-specific implementation of Timer
// Based on TimerQueue.Portable.cs

View file

@ -3,30 +3,155 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using WasiPollWorld.wit.imports.wasi.clocks.v0_2_1;
namespace System.Threading
{
#if FEATURE_WASM_MANAGED_THREADS
#error when compiled with FEATURE_WASM_MANAGED_THREADS, we will use TimerQueue.Portable.cs
#endif
//
// Wasi-specific implementation of Timer
// Wasi implementation of Timer, single-threaded, on top of pollable and wasi:clocks
// Based on TimerQueue.Portable.cs
// Not thread safe
//
internal partial class TimerQueue
internal sealed partial class TimerQueue
{
private static long TickCount64 => Environment.TickCount64;
private static List<TimerQueue>? s_scheduledTimers;
private static List<TimerQueue>? s_scheduledTimersToFire;
private static long s_shortestDueTimeMs = long.MaxValue;
// this means that it's in the s_scheduledTimers collection, not that it's the one which would run on the next TimeoutCallback
private bool _isScheduled;
private long _scheduledDueTimeMs;
private TimerQueue(int _)
{
throw new PlatformNotSupportedException();
}
#pragma warning disable CA1822 // Mark members as static
private static void TimerHandler(object _)
{
try
{
s_shortestDueTimeMs = long.MaxValue;
long currentTimeMs = TickCount64;
SetNextTimer(PumpTimerQueue(currentTimeMs), currentTimeMs);
}
catch (Exception e)
{
Environment.FailFast("TimerQueue.TimerHandler failed", e);
}
}
// this is called with shortest of timers scheduled on the particular TimerQueue
private bool SetTimer(uint actualDuration)
{
throw new PlatformNotSupportedException();
Debug.Assert((int)actualDuration >= 0);
long currentTimeMs = TickCount64;
if (!_isScheduled)
{
s_scheduledTimers ??= new List<TimerQueue>(Instances.Length);
s_scheduledTimersToFire ??= new List<TimerQueue>(Instances.Length);
s_scheduledTimers.Add(this);
_isScheduled = true;
}
_scheduledDueTimeMs = currentTimeMs + (int)actualDuration;
SetNextTimer(ShortestDueTime(), currentTimeMs);
return true;
}
// shortest time of all TimerQueues
private static unsafe void SetNextTimer(long shortestDueTimeMs, long currentTimeMs)
{
if (shortestDueTimeMs == long.MaxValue)
{
return;
}
// this also covers s_shortestDueTimeMs = long.MaxValue when none is scheduled
if (s_shortestDueTimeMs > shortestDueTimeMs)
{
s_shortestDueTimeMs = shortestDueTimeMs;
ulong shortestWaitMs = (ulong)Math.Max((long)(shortestDueTimeMs - currentTimeMs), 0);
// `SubscribeDuration` expects nanoseconds:
var pollable = MonotonicClockInterop.SubscribeDuration(shortestWaitMs * 1000 * 1000);
Task task = WasiEventLoop.RegisterWasiPollable(pollable);
task.ContinueWith(TimerHandler, TaskScheduler.Default);
}
}
private static long ShortestDueTime()
{
if (s_scheduledTimers == null)
{
return long.MaxValue;
}
long shortestDueTimeMs = long.MaxValue;
var timers = s_scheduledTimers!;
for (int i = timers.Count - 1; i >= 0; --i)
{
TimerQueue timer = timers[i];
if (timer._scheduledDueTimeMs < shortestDueTimeMs)
{
shortestDueTimeMs = timer._scheduledDueTimeMs;
}
}
return shortestDueTimeMs;
}
private static long PumpTimerQueue(long currentTimeMs)
{
if (s_scheduledTimersToFire == null)
{
return ShortestDueTime();
}
List<TimerQueue> timersToFire = s_scheduledTimersToFire!;
List<TimerQueue> timers;
timers = s_scheduledTimers!;
long shortestDueTimeMs = long.MaxValue;
for (int i = timers.Count - 1; i >= 0; --i)
{
TimerQueue timer = timers[i];
long waitDurationMs = timer._scheduledDueTimeMs - currentTimeMs;
if (waitDurationMs <= 0)
{
timer._isScheduled = false;
timersToFire.Add(timer);
int lastIndex = timers.Count - 1;
if (i != lastIndex)
{
timers[i] = timers[lastIndex];
}
timers.RemoveAt(lastIndex);
continue;
}
if (timer._scheduledDueTimeMs < shortestDueTimeMs)
{
shortestDueTimeMs = timer._scheduledDueTimeMs;
}
}
if (timersToFire.Count > 0)
{
foreach (TimerQueue timerToFire in timersToFire)
{
timerToFire.FireNextTimers();
}
timersToFire.Clear();
}
return shortestDueTimeMs;
}
#pragma warning restore CA1822
}
}

View file

@ -346,11 +346,15 @@ JS_ENGINES = [NODE_JS]
<Error Text="Expected version: %(_ExpectedVersionLines.Identity) and actual version: %(_ActualVersionLines.Identity) of WASI SDK does not match. Please delete $(WASI_SDK_PATH) folder to provision a new version."
Condition="'$(ActualWasiSdkVersion)' != '$(ExpectedWasiSdkVersion)'" />
<!-- LLVM in WASI SDK 22 will call wasm-opt when found on the PATH. But it will fail because wasm-opt can't read the WASM components.
After we upgrade to WASI SDK 23.x, we could use no-wasm-opt LLVM option to avoid this issue.
<!-- LLVM in WASI SDK 24 will call wasm-opt when found on the PATH. But it will fail because wasm-opt can't read the WASM components.
After we upgrade to WASI SDK with LLVM 19, we could use no-wasm-opt LLVM option to avoid this issue.
See https://github.com/llvm/llvm-project/pull/98373
See https://github.com/dotnet/runtime/issues/104773
-->
<Exec Command="wasm-opt --version" IgnoreExitCode="true" IgnoreStandardErrorWarningFormat="true" StandardOutputImportance="Low" >
<Exec Command="wasm-opt --version" IgnoreExitCode="true"
IgnoreStandardErrorWarningFormat="true"
StandardErrorImportance="low"
StandardOutputImportance="Low" >
<Output TaskParameter="ExitCode" PropertyName="_WasmOptExitCode"/>
</Exec>
<Error Text="Found wasm-opt tool on the PATH. Please remove it to avoid failures during compilation into wasm32-wasip2 target as WASM components, which is not supported by wasm-opt tool. See https://github.com/llvm/llvm-project/pull/95208#issuecomment-2220400454"

View file

@ -8,50 +8,33 @@ using System.Threading.Tasks;
using System.Threading;
using System.Runtime.CompilerServices;
public class Test
// keep in sync with src\mono\wasi\testassets\Http.cs
public static class WasiMainWrapper
{
public static int Main(string[] args)
{
var task = Work();
while (!task.IsCompleted)
{
WasiEventLoop.DispatchWasiEventLoop();
}
var exception = task.Exception;
if (exception is not null)
{
throw exception;
}
return 0;
}
public static async Task Work()
public static async Task<int> MainAsync(string[] args)
{
using HttpClient client = new();
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
client.DefaultRequestHeaders.Add("User-Agent", "dotnet WASI unit test");
var query="https://api.github.com/orgs/dotnet/repos?per_page=1";
var query="https://corefx-net-http11.azurewebsites.net/Echo.ashx";
var json = await client.GetStringAsync(query);
Console.WriteLine();
Console.WriteLine("GET "+query);
Console.WriteLine();
Console.WriteLine(json);
return 0;
}
private static class WasiEventLoop
public static int Main(string[] args)
{
internal static void DispatchWasiEventLoop()
{
CallDispatchWasiEventLoop((Thread)null!);
return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "DispatchWasiEventLoop")]
static extern void CallDispatchWasiEventLoop(Thread t);
}
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
static extern int PollWasiEventLoopUntilResolved(Thread t, Task<int> mainTask);
}
}

View file

@ -693,10 +693,14 @@ namespace Wasm.Build.Tests
.MultiplyWithSingleArgs(true, false) /*aot*/
.UnwrapItemsAsArrays();
protected CommandResult RunWithoutBuild(string config, string id)
protected CommandResult RunWithoutBuild(string config, string id, bool enableHttp = false)
{
// wasmtime --wasi http is necessary because the default dotnet.wasm (without native rebuild depends on wasi:http world)
string runArgs = $"run --no-build -c {config} --forward-exit-code --extra-host-arg=--wasi --extra-host-arg=http";
string runArgs = $"run --no-build -c {config} --forward-exit-code";
if (enableHttp)
{
runArgs += " --extra-host-arg=--wasi --extra-host-arg=http";
}
runArgs += " x y z";
int expectedExitCode = 42;
CommandResult res = new RunCommand(s_buildEnv, _testOutput, label: id)

View file

@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using Wasm.Build.Tests;
#nullable enable
namespace Wasi.Build.Tests;
public class HttpTests : BuildTestBase
{
public HttpTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
{
}
[Theory]
[MemberData(nameof(TestDataForConsolePublishAndRun))]
public void HttpBuildThenRunThenPublish(string config, bool singleFileBundle, bool aot)
{
string id = $"{config}_{GetRandomId()}";
string projectFile = CreateWasmTemplateProject(id, "wasiconsole");
string projectName = Path.GetFileNameWithoutExtension(projectFile);
File.Copy(Path.Combine(BuildEnvironment.TestAssetsPath, "Http.cs"), Path.Combine(_projectDir!, "Program.cs"), true);
var buildArgs = new BuildArgs(projectName, config, aot, id, null);
buildArgs = ExpandBuildArgs(buildArgs);
string extraProperties = "<PublishTrimmed>true</PublishTrimmed>";
if (aot)
extraProperties += "<RunAOTCompilation>true</RunAOTCompilation><_WasmDevel>false</_WasmDevel>";
if (singleFileBundle)
extraProperties += "<WasmSingleFileBundle>true</WasmSingleFileBundle>";
if (!string.IsNullOrEmpty(extraProperties))
AddItemsPropertiesToProject(projectFile, extraProperties);
BuildProject(buildArgs,
id: id,
new BuildProjectOptions(
DotnetWasmFromRuntimePack: true,
CreateProject: false,
Publish: false,
TargetFramework: BuildTestBase.DefaultTargetFramework));
RunWithoutBuild(config, id, true);
if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product))
throw new XunitException($"Test bug: could not get the build product in the cache");
File.Move(product!.LogFile, Path.ChangeExtension(product.LogFile!, ".first.binlog"));
_testOutput.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}");
BuildProject(buildArgs,
id: id,
new BuildProjectOptions(
DotnetWasmFromRuntimePack: true,
CreateProject: false,
Publish: true,
TargetFramework: BuildTestBase.DefaultTargetFramework,
UseCache: false,
ExpectSuccess: !(config == "Debug" && aot)));
}
}

View file

@ -50,7 +50,7 @@ public class WasiTemplateTests : BuildTestBase
CreateProject: false,
Publish: false,
TargetFramework: BuildTestBase.DefaultTargetFramework));
RunWithoutBuild(config, id);
RunWithoutBuild(config, id, true);
}
[Theory]
@ -80,7 +80,7 @@ public class WasiTemplateTests : BuildTestBase
CreateProject: false,
Publish: false,
TargetFramework: BuildTestBase.DefaultTargetFramework));
RunWithoutBuild(config, id);
RunWithoutBuild(config, id, true);
if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product))
throw new XunitException($"Test bug: could not get the build product in the cache");

View file

@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.CompilerServices;
// keep in sync with src\mono\sample\wasi\http-p2\Program.cs
public static class WasiMainWrapper
{
public static async Task<int> MainAsync(string[] args)
{
Console.WriteLine("Hello, Wasi Console!");
for (int i = 0; i < args.Length; i ++)
Console.WriteLine($"args[{i}] = {args[i]}");
using HttpClient client = new();
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Add("User-Agent", "dotnet WASI unit test");
var query="https://corefx-net-http11.azurewebsites.net/Echo.ashx";
var json = await client.GetStringAsync(query);
Console.WriteLine();
Console.WriteLine("GET "+query);
Console.WriteLine();
Console.WriteLine(json);
return 42;
}
public static int Main(string[] args)
{
return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
static extern int PollWasiEventLoopUntilResolved(Thread t, Task<int> mainTask);
}
}