Skip to content

Commit

Permalink
[wasm][wasi] Allow to build wasi in library mode (#102806)
Browse files Browse the repository at this point in the history
Allow to build wasi app in library mode
  • Loading branch information
mkhamoyan authored Jun 7, 2024
1 parent 7393b6e commit 8fdba2b
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 14 deletions.
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>
<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()
{
}

// 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

0 comments on commit 8fdba2b

Please sign in to comment.