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

[wasm][wasi] Allow to build wasi in library mode #102806

Merged
merged 16 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions eng/testing/scenarios/BuildWasiAppsJobsList.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Wasi.Build.Tests.SdkMissingTests
Wasi.Build.Tests.RuntimeConfigTests
Wasi.Build.Tests.WasiTemplateTests
Wasi.Build.Tests.PInvokeTableGeneratorTests
Wasi.Build.Tests.WasiLibraryModeTests
1 change: 1 addition & 0 deletions src/mono/browser/runtime/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ MonoAssembly *mono_wasm_assembly_load (const char *name);
MonoClass *mono_wasm_assembly_find_class (MonoAssembly *assembly, const char *namespace, const char *name);
MonoMethod *mono_wasm_assembly_find_method (MonoClass *klass, const char *name, int arguments);
void mono_wasm_marshal_get_managed_wrapper (const char* assemblyName, const char* typeName, const char* methodName, int num_params);
int initialize_runtime ();

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<TargetArchitecture>wasm</TargetArchitecture>
<TargetOS>wasi</TargetOS>
<UseMonoRuntime>true</UseMonoRuntime>
<OutputType>Exe</OutputType>
<OutputType Condition="'$(OutputType)' == ''">Exe</OutputType>
<PublishTrimmed Condition="'$(PublishTrimmed)' == ''">true</PublishTrimmed>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,5 @@ Copyright (c) .NET Foundation. All rights reserved.
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'browser-wasm'">
<_WebAssemblyPropsFile>$(MSBuildThisFileDirectory)\Microsoft.NET.Sdk.WebAssembly.Browser.props</_WebAssemblyPropsFile>
<_WebAssemblyTargetsFile>$(MSBuildThisFileDirectory)\Microsoft.NET.Sdk.WebAssembly.Browser.targets</_WebAssemblyTargetsFile>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
-->
<Project ToolsVersion="14.0"></Project>
<Project ToolsVersion="14.0">
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'browser-wasm'">
<_WebAssemblyTargetsFile>$(MSBuildThisFileDirectory)\Microsoft.NET.Sdk.WebAssembly.Browser.targets</_WebAssemblyTargetsFile>
</PropertyGroup>

<!-- Library Mode defaults-->
<PropertyGroup Condition="'$(OutputType)' == 'Library'">
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions src/mono/nuget/mono-packages.proj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<ItemGroup Condition="'$(TargetsWasi)' == 'true'">
<ProjectReference Include="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk\Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk.pkgproj" />
<ProjectReference Include="..\wasm\templates\Microsoft.NET.Runtime.WebAssembly.Templates.csproj" />
<ProjectReference Include="Microsoft.NET.Sdk.WebAssembly.Pack\Microsoft.NET.Sdk.WebAssembly.Pack.pkgproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetsiOS)' == 'true' or '$(TargetsiOSSimulator)' == 'true'">
Expand Down
70 changes: 70 additions & 0 deletions src/mono/wasi/Wasi.Build.Tests/WasiLibraryModeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 WasiLibraryModeTests : BuildTestBase
{
public WasiLibraryModeTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
{
}

[Fact]
public void ConsoleBuildLibraryMode()
{
string config = "Release";
string id = $"{config}_{GetRandomId()}";
string projectFile = CreateWasmTemplateProject(id, "wasiconsole");
string code =
"""
using System;
using System.Runtime.InteropServices;
public unsafe class Test
{
[UnmanagedCallersOnly(EntryPoint = "MyCallback")]
public static int MyCallback()
{
Console.WriteLine("WASM Library MyCallback is called");
return 100;
}
}
""";
string csprojCode =
"""
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
mkhamoyan marked this conversation as resolved.
Show resolved Hide resolved
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<WasmSingleFileBundle>true</WasmSingleFileBundle>
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
""";
File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), code);
File.WriteAllText(Path.Combine(_projectDir!, $"{id}.csproj"), csprojCode);
string projectName = Path.GetFileNameWithoutExtension(projectFile);
var buildArgs = new BuildArgs(projectName, config, AOT: false, ProjectFileContents: id, ExtraBuildArgs: null);
buildArgs = ExpandBuildArgs(buildArgs);
(_, string output) = BuildProject(buildArgs,
id: id,
new BuildProjectOptions(
DotnetWasmFromRuntimePack: false,
CreateProject: false,
Publish: false,
TargetFramework: BuildTestBase.DefaultTargetFramework
));

Assert.Contains("Build succeeded.", output);
}
}
1 change: 1 addition & 0 deletions src/mono/wasi/build/WasiApp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
<_WasmCommonCFlags Condition="'$(InvariantGlobalization)' == 'true'" Include="-DINVARIANT_GLOBALIZATION=1" />
<_WasmCommonCFlags Condition="'$(InvariantTimezone)' == 'true'" Include="-DINVARIANT_TIMEZONE=1" />
<_WasmCommonCFlags Condition="'$(WasmLinkIcalls)' == 'true'" Include="-DLINK_ICALLS=1" />
<_WasmCommonCFlags Condition="'$(_IsLibraryMode)' == 'true'" Include="-DWASM_LIBRARY_MODE=1" />
<_WasiClangCFlags Include="@(_WasmCommonCFlags)" />

<_WasiClangCFlags Include="&quot;-I%(_WasmCommonIncludePaths.Identity)&quot;" />
Expand Down
48 changes: 45 additions & 3 deletions src/mono/wasi/runtime/main.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <string.h>
#include <stdbool.h>
#include <string.h>
#include <driver.h>
#include <mono/metadata/assembly.h>

Expand All @@ -10,7 +11,40 @@ const char* dotnet_wasi_getentrypointassemblyname();
WASI_AFTER_RUNTIME_LOADED_DECLARATIONS
#endif

int main(int argc, char * argv[]) {
#ifdef WASM_LIBRARY_MODE
// _initialize is a function generated by the WASI SDK libc that calls the LLVM synthesized __wasm_call_ctors function for reactor components:
// https://github.com/WebAssembly/wasi-libc/blob/9f51a7102085ec6a6ced5778f0864c9af9f50000/libc-bottom-half/crt/crt1-reactor.c#L7-L27
// We define and call it for WASM_LIBRARY_MODE and TARGET_WASI to call all the global c++ static constructors. This ensures the runtime is initialized
// when calling into WebAssembly Component Model components.
extern void _initialize();

// CustomNativeMain programs are built using the same libbootstrapperdll as WASM_LIBRARY_MODE but wasi-libc will not provide an _initialize implementation,
// so create a dummy one here and make it weak to allow wasi-libc to provide the real implementation for WASI reactor components.
__attribute__((weak)) void _initialize()
{
mkhamoyan marked this conversation as resolved.
Show resolved Hide resolved
}

// Guard the "_initialize" call so that well-behaving hosts do not get affected by this workaround.
static bool g_CalledInitialize = false;

__attribute__((constructor))
void WasiInitializationFlag() {
*(volatile bool*)&g_CalledInitialize = true;
}

static bool runtime_initialized = false;

#endif

int initialize_runtime()
{
#if defined(WASM_LIBRARY_MODE)
if (runtime_initialized)
return 0;
if (!g_CalledInitialize)
_initialize();
runtime_initialized = true;
#endif

#ifndef WASM_SINGLE_FILE
mono_set_assemblies_path("managed");
Expand All @@ -21,7 +55,14 @@ int main(int argc, char * argv[]) {
// This is supplied from the MSBuild itemgroup @(WasiAfterRuntimeLoaded)
WASI_AFTER_RUNTIME_LOADED_CALLS
#endif

return 0;
}

#ifndef WASM_LIBRARY_MODE
int main(int argc, char * argv[]) {
int initval = initialize_runtime();
if (initval != 0)
return initval;
int arg_ofs = 0;
#ifdef WASM_SINGLE_FILE
/*
Expand Down Expand Up @@ -71,3 +112,4 @@ int main(int argc, char * argv[]) {
}
return ret < 0 ? -ret : ret;
}
#endif
3 changes: 2 additions & 1 deletion src/mono/wasi/wasi.proj
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
Assemblies="@(WasmPInvokeAssembly)"
PInvokeModules="@(WasmPInvokeModule)"
PInvokeOutputPath="$(WasmPInvokeTablePath)"
InterpToNativeOutputPath="$(WasmInterpToNativeTablePath)">
InterpToNativeOutputPath="$(WasmInterpToNativeTablePath)"
IsLibraryMode="$(_IsLibraryMode)">
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
</ManagedToNativeGenerator>
</Target>
Expand Down
12 changes: 7 additions & 5 deletions src/mono/wasm/build/WasmApp.Common.targets
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@
</WasmLinkDotNetDependsOn>

<WasmDedup Condition="'$(WasmDedup)' == ''">true</WasmDedup>
<IsWasiProject Condition="'$(IsWasiProject)' == '' and '$(RuntimeIdentifier)' == 'wasi-wasm' and '$(OutputType)' != 'Library'">true</IsWasiProject>
<IsBrowserWasmProject Condition="'$(IsBrowserWasmProject)' == '' and '$(RuntimeIdentifier)' == 'browser-wasm' and '$(OutputType)' != 'Library'">true</IsBrowserWasmProject>
<_IsLibraryMode Condition="'$(OutputType)' == 'Library' and '$(UsingMicrosoftNETSdkWebAssembly)' == 'true'">true</_IsLibraryMode>
<IsWasiProject Condition="'$(IsWasiProject)' == '' and '$(RuntimeIdentifier)' == 'wasi-wasm' and ('$(OutputType)' != 'Library' or '$(_IsLibraryMode)' == 'true')">true</IsWasiProject>
<IsBrowserWasmProject Condition="'$(IsBrowserWasmProject)' == '' and '$(RuntimeIdentifier)' == 'browser-wasm' and ('$(OutputType)' != 'Library' or '$(_IsLibraryMode)' == 'true')">true</IsBrowserWasmProject>
<IsWasmProject Condition="'$(IsWasmProject)' == '' and ('$(IsWasiProject)' == 'true' or '$(IsBrowserWasmProject)' == 'true')">true</IsWasmProject>
<WasmBuildAppAfterThisTarget Condition="'$(WasmBuildAppAfterThisTarget)' == '' and '$(DisableAutoWasmBuildApp)' != 'true'">Build</WasmBuildAppAfterThisTarget>

Expand All @@ -173,7 +174,7 @@
<EnableDefaultWasmAssembliesToBundle Condition="'$(EnableDefaultWasmAssembliesToBundle)' == ''">true</EnableDefaultWasmAssembliesToBundle>
<!-- VS uses DeployOnBuild, and sdk sets _IsPublishing -->
<WasmBuildOnlyAfterPublish Condition="'$(WasmBuildOnlyAfterPublish)' == '' and ('$(DeployOnBuild)' == 'true' or '$(_IsPublishing)' == 'true')">true</WasmBuildOnlyAfterPublish>
<WasmGenerateAppBundle Condition="'$(WasmGenerateAppBundle)' == '' and '$(OutputType)' != 'Library'">true</WasmGenerateAppBundle>
<WasmGenerateAppBundle Condition="'$(WasmGenerateAppBundle)' == '' and ('$(OutputType)' != 'Library' or '$(_IsLibraryMode)' == 'true')">true</WasmGenerateAppBundle>
<WasmGenerateAppBundle Condition="'$(WasmGenerateAppBundle)' == ''">false</WasmGenerateAppBundle>

<!-- FIXME: can't set to true because
Expand Down Expand Up @@ -346,7 +347,7 @@

<Target Name="_WasmGetRuntimeConfigPath">
<PropertyGroup>
<_MainAssemblyPath Condition="'%(WasmAssembliesToBundle.FileName)' == $(AssemblyName) and '%(WasmAssembliesToBundle.Extension)' == '.dll' and $(WasmGenerateAppBundle) == 'true'">%(WasmAssembliesToBundle.Identity)</_MainAssemblyPath>
<_MainAssemblyPath Condition="'%(WasmAssembliesToBundle.FileName)' == '$(AssemblyName)' and '%(WasmAssembliesToBundle.Extension)' == '.dll' and $(WasmGenerateAppBundle) == 'true'">%(WasmAssembliesToBundle.Identity)</_MainAssemblyPath>
<_WasmRuntimeConfigFilePath Condition="'$(_WasmRuntimeConfigFilePath)' == '' and $(_MainAssemblyPath) != ''">$([System.IO.Path]::ChangeExtension($(_MainAssemblyPath), '.runtimeconfig.json'))</_WasmRuntimeConfigFilePath>
<_ParsedRuntimeConfigFilePath Condition="'$(_WasmRuntimeConfigFilePath)' != ''">$([System.IO.Path]::GetDirectoryName($(_WasmRuntimeConfigFilePath)))\runtimeconfig.bin</_ParsedRuntimeConfigFilePath>
</PropertyGroup>
Expand Down Expand Up @@ -799,7 +800,8 @@
PInvokeModules="@(_WasmPInvokeModules)"
PInvokeOutputPath="$(_WasmPInvokeTablePath)"
InterpToNativeOutputPath="$(_WasmInterpToNativeTablePath)"
CacheFilePath="$(_WasmM2NCachePath)">
CacheFilePath="$(_WasmM2NCachePath)"
IsLibraryMode="$(_IsLibraryMode)">
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
</ManagedToNativeGenerator>
</Target>
Expand Down
4 changes: 3 additions & 1 deletion src/tasks/WasmAppBuilder/ManagedToNativeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class ManagedToNativeGenerator : Task
public string? InterpToNativeOutputPath { get; set; }
public string? CacheFilePath { get; set; }

public bool IsLibraryMode { get; set; }

[Output]
public string[]? FileWrites { get; private set; }

Expand Down Expand Up @@ -69,7 +71,7 @@ private void ExecuteInternal(LogAdapter log)
List<string> managedAssemblies = FilterOutUnmanagedBinaries(Assemblies);
if (ShouldRun(managedAssemblies))
{
var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log);
var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log, IsLibraryMode);
var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, log);

var resolver = new PathAssemblyResolver(managedAssemblies);
Expand Down
9 changes: 8 additions & 1 deletion src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ internal sealed class PInvokeTableGenerator
private readonly List<PInvoke> pinvokes = new();
private readonly List<PInvokeCallback> callbacks = new();
private readonly PInvokeCollector _pinvokeCollector;
private readonly bool _isLibraryMode;

public PInvokeTableGenerator(Func<string, string> fixupSymbolName, LogAdapter log)
public PInvokeTableGenerator(Func<string, string> fixupSymbolName, LogAdapter log, bool isLibraryMode = false)
{
Log = log;
_fixupSymbolName = fixupSymbolName;
_pinvokeCollector = new(log);
_isLibraryMode = isLibraryMode;
}

public void ScanAssembly(Assembly asm)
Expand Down Expand Up @@ -379,6 +381,11 @@ private void EmitNativeToInterp(StreamWriter w, List<PInvokeCallback> callbacks)
if (!is_void)
sb.Append($" {MapType(method.ReturnType)} res;\n");

if (_isLibraryMode && HasAttribute(method, "System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute"))
{
sb.Append($" initialize_runtime(); \n");
}

// In case when null force interpreter to initialize the pointers
sb.Append($" if (!(WasmInterpEntrySig_{cb_index})wasm_native_to_interp_ftndescs [{cb_index}].func) {{\n");
var assemblyFullName = cb.Method.DeclaringType == null ? "" : cb.Method.DeclaringType.Assembly.FullName;
Expand Down
Loading