├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── LICENSE ├── README.md ├── SECURITY.md ├── dotnet-wasi-sdk.sln ├── samples ├── AspNetCoreOnCustomHost │ ├── AspNetCoreOnCustomHost.csproj │ ├── Directory.Build.props │ ├── Pages │ │ ├── MyRazorPage.cshtml │ │ └── MyRazorPage.cshtml.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── wwwroot │ │ └── logo.png ├── AspNetCoreOnNativeWasi │ ├── AspNetCoreOnNativeWasi.csproj │ ├── Pages │ │ ├── MyRazorPage.cshtml │ │ └── MyRazorPage.cshtml.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── wwwroot │ │ └── logo.png ├── ConsoleApp │ ├── .vscode │ │ └── launch.json │ ├── ConsoleApp.csproj │ └── Program.cs ├── Directory.Build.props ├── atmo │ ├── AspNetCoreOnAtmo │ │ ├── AspNetCoreOnAtmo.csproj │ │ ├── Directive.yaml │ │ ├── Pages │ │ │ ├── MyRazorPage.cshtml │ │ │ └── MyRazorPage.cshtml.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ └── wwwroot │ │ │ └── logo.png │ └── docker-compose.yml └── webserver-wasi-host │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── WebserverWasiHost.targets │ └── src │ ├── main.rs │ └── server.rs └── src ├── Wasi.AspNetCore.BundledFiles ├── Wasi.AspNetCore.BundledFiles.csproj ├── WasiBundledFileProvider.cs ├── WasiNativeWebApplicationExtensions.cs ├── build │ ├── Wasi.AspNetCore.BundledFiles.props │ └── Wasi.AspNetCore.BundledFiles.targets └── native │ └── interop.c ├── Wasi.AspNetCore.Server.Atmo ├── AtmoLogger.cs ├── AtmoRequestContext.cs ├── AtmoServer.cs ├── AtmoWebApplicationBuilderExtensions.cs ├── Interop.cs ├── Services │ ├── Cache.cs │ └── IdentAccessor.cs ├── Wasi.AspNetCore.Server.Atmo.csproj ├── build │ ├── Wasi.AspNetCore.Server.Atmo.props │ └── Wasi.AspNetCore.Server.Atmo.targets └── native │ └── host_interop.c ├── Wasi.AspNetCore.Server.CustomHost ├── CustomHostWebApplicationBuilderExtensions.cs ├── Interop.cs ├── Wasi.AspNetCore.Server.CustomHost.csproj ├── WasiCustomHostRequestContext.cs ├── WasiCustomHostServer.cs ├── build │ ├── Wasi.AspNetCore.Server.CustomHost.props │ └── Wasi.AspNetCore.Server.CustomHost.targets └── native │ └── server_interop.c ├── Wasi.AspNetCore.Server.Native ├── DuplexPipe.cs ├── Interop.cs ├── Wasi.AspNetCore.Server.Native.csproj ├── WasiConnectionContext.cs ├── WasiConnectionListener.cs ├── WasiConnectionListenerFactory.cs ├── WasiLoggingProvider.cs ├── WasiNativeServer.cs ├── WasiNativeWebApplicationBuilderExtensions.cs ├── build │ ├── Wasi.AspNetCore.Server.Native.props │ └── Wasi.AspNetCore.Server.Native.targets └── native │ ├── dotnet_method.h │ ├── interop.c │ └── tcp_listener_loop.c ├── Wasi.AspNetCore.Server ├── Wasi.AspNetCore.Server.csproj ├── WasiLoggingProvider.cs ├── WasiServer.cs └── WasiServerRequestContext.cs └── Wasi.Sdk ├── .gitignore ├── Tasks ├── EmitWasmBundleObjectFile.cs └── WasmResolveAssemblyDependencies.cs ├── Wasi.Sdk.csproj ├── build ├── Wasi.Sdk.props └── Wasi.Sdk.targets └── native ├── generated-pinvokes.h ├── main.c └── pinvoke.c /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: BuildAll 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Get package version suffix 16 | run: echo "PACKAGE_VERSION_SUFFIX=preview.$(printf $((${{ github.run_number }}+10000)))" >> $GITHUB_ENV 17 | - name: Install build dependencies 18 | run: sudo apt-get update && sudo apt-get install build-essential cmake ninja-build python2 python3 zlib1g-dev 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: recursive 22 | - name: Install .NET SDK 23 | uses: actions/setup-dotnet@v2 24 | with: 25 | dotnet-version: 7.0.x 26 | include-prerelease: true 27 | 28 | - name: Get runtime commit hash 29 | run: echo "RUNTIME_SUBMODULE_COMMIT=$(git rev-parse HEAD:modules/runtime)" >> $GITHUB_ENV 30 | - name: Cache .NET libraries build output 31 | uses: actions/cache@v2 32 | id: cache-dotnet-libraries-build 33 | with: 34 | path: | 35 | ./modules/runtime/artifacts/bin/microsoft.netcore.app.runtime.browser-wasm 36 | ./modules/runtime/artifacts/obj/wasm 37 | key: ${{ runner.OS }}-cache-dotnet-libraries-build-${{ env.RUNTIME_SUBMODULE_COMMIT }} 38 | - name: Build .NET libraries 39 | run: cd modules/runtime/src/mono/wasm && make provision-wasm && make build-all 40 | if: steps.cache-dotnet-libraries-build.outputs.cache-hit != 'true' 41 | 42 | - name: Build .NET runtime for WASI 43 | run: cd modules/runtime/src/mono/wasi && make 44 | - name: Restore dependencies 45 | run: dotnet restore 46 | - name: Pack 47 | run: dotnet pack dotnet-wasi-sdk.sln -c Release /p:VersionSuffix=${{ env.PACKAGE_VERSION_SUFFIX }} 48 | - name: Upload artifacts 49 | uses: actions/upload-artifact@v3 50 | with: 51 | name: nuget-packages 52 | path: artifacts/*.nupkg 53 | if-no-files-found: error 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vs/ 4 | *.csproj.user 5 | *.suo 6 | *.binlog 7 | artifacts/ 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "modules/runtime"] 2 | path = modules/runtime 3 | url = https://github.com/SteveSandersonMS/dotnet-wasi-runtime.git 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the code of conduct defined by the Contributor Covenant 4 | to clarify expected behavior in our community. 5 | 6 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). 7 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.1.4 5 | dev 6 | $(VersionPrefix)-$(VersionSuffix) 7 | 8 | $(MSBuildThisFileDirectory)artifacts\ 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) .NET Foundation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED # 2 | **This repository is now archived. 3 | Many of the experiments that started in this repository have made their way into .NET runtime during .NET 8. You can read more about these changes in the following blog post: https://devblogs.microsoft.com/dotnet/extending-web-assembly-to-the-cloud/** 4 | 5 | # Experimental WASI SDK for .NET Core 6 | 7 | `Wasi.Sdk` is an experimental package that can build .NET Core projects (including whole ASP.NET Core applications) into standalone WASI-compliant `.wasm` files. These can then be run in standard WASI environments or custom WASI-like hosts. 8 | 9 | ## How to use: Console applications 10 | 11 | ``` 12 | dotnet new console -o MyFirstWasiApp 13 | cd MyFirstWasiApp 14 | dotnet add package Wasi.Sdk --prerelease 15 | dotnet build 16 | ``` 17 | 18 | You'll see from the build output that this produces `bin/Debug/net7.0/MyFirstWasiApp.wasm`. 19 | 20 | To run it, 21 | 22 | * Ensure you've installed [wasmtime](https://github.com/bytecodealliance/wasmtime) and it's available on your system `PATH` 23 | * Run your app via `dotnet run` or, if you're using Visual Studio, press Ctrl+F5 24 | 25 | Alternatively you can invoke runners like `wasmtime` or `wasmer` manually on the command line. For example, 26 | 27 | * For [wasmtime](https://github.com/bytecodealliance/wasmtime), run `wasmtime bin/Debug/net7.0/MyFirstWasiApp.wasm` 28 | * For [wasmer](https://wasmer.io/), run `wasmer bin/Debug/net7.0/MyFirstWasiApp.wasm` 29 | 30 | Other WASI hosts work similarly. 31 | 32 | ## How to use: ASP.NET Core applications 33 | 34 | ``` 35 | dotnet new web -o MyWebApp 36 | cd MyWebApp 37 | dotnet add package Wasi.Sdk --prerelease 38 | dotnet add package Wasi.AspNetCore.Server.Native --prerelease 39 | ``` 40 | 41 | Then: 42 | 43 | * Open your new project in an IDE such as Visual Studio or VS Code 44 | * Open `Program.cs` and change the line `var builder = WebApplication.CreateBuilder(args)` to look like this: 45 | 46 | ```cs 47 | var builder = WebApplication.CreateBuilder(args).UseWasiConnectionListener(); 48 | ``` 49 | 50 | * Open `Properties/launchSettings.json` and edit the `applicationUrl` value to contain only a single HTTP listener, e.g., 51 | 52 | ```json 53 | "applicationUrl": "http://localhost:8080" 54 | ``` 55 | 56 | * Open your `.csproj` file (e.g., in VS, double-click on the project name) and, inside a ``, add this: 57 | 58 | ```xml 59 | --tcplisten localhost:8080 --env ASPNETCORE_URLS=http://localhost:8080 60 | ``` 61 | 62 | Instead of `8080`, you should enter the port number found in `Properties\launchSettings.json`. 63 | 64 | That's it! You can now run it via `dotnet run` (or in VS, use Ctrl+F5) 65 | 66 | Optionally, to add support for bundling `wwwroot` files into the `.wasm` file and serving them: 67 | 68 | * Add the NuGet package `Wasi.AspNetCore.BundledFiles` 69 | * In `Program.cs`, replace `app.UseStaticFiles();` with `app.UseBundledStaticFiles();` 70 | * In your `.csproj` file, add: 71 | 72 | ```xml 73 | 74 | 75 | 76 | ``` 77 | 78 | ## What's in this repo 79 | 80 | * `Wasi.Sdk` - a package that causes your build to produce a WASI-compliant `.wasm` file. This works by: 81 | * Downloading the WASI SDK, if you don't already have it 82 | * When your regular .NET build is done, it takes the resulting assemblies, plus the .NET runtime precompiled to WebAssembly, and uses WASI SDK to bundle them into a single `.wasm` file. You can optionally include other native sources such as `.c` files in the compilation. 83 | * `Wasi.AspNetCore.BundledFiles` - provides `UseBundledStaticFiles`, and alternative to `UseStaticFiles`, that serves static files bundled into your `.wasm` file. This allows you to have single-file deployment even if you have files under `wwwroot` or elsewhere. 84 | * `Wasi.AspNetCore.Server.Native` - a way of running ASP.NET Core on WASI's TCP-level standard networking APIs (e.g., `sock_accept`). These standards are quite recent and are currently only supported in Wasmtime, not other WASI hosts. 85 | 86 | ... and more 87 | 88 | ## Building this repo from source 89 | 90 | First, build the runtime. This can take quite a long time. 91 | 92 | * `git submodule update --init --recursive` 93 | * Do the following steps using Linux or WSL: 94 | * `sudo apt-get install build-essential cmake ninja-build python python3 zlib1g-dev` 95 | * `cd modules/runtime/src/mono/wasm` 96 | * `make provision-wasm` (takes about 2 minutes) 97 | * `make build-all` (takes 10-15 minutes) 98 | * If you get an error about `setlocale: LC_ALL: cannot change locale` then run `sudo apt install language-pack-en`. This only happens on very bare-bones machines. 99 | * `cd ../wasi` 100 | * `make` (takes a few minutes - there are lots of warnings like "System is unknown to cmake" and that's OK) 101 | 102 | 103 | Now you can build the packages and samples in this repo: 104 | 105 | * Prerequisites 106 | * .NET 7 (`dotnet --version` should return `7.0.100-preview.4` or later) 107 | * Rust and the `wasm32-unknown-unknown` target (technically this is only needed for the CustomHost package) 108 | * [Install Rust](https://www.rust-lang.org/tools/install) 109 | * `rustup target add wasm32-unknown-unknown` 110 | * Just use `dotnet build` or `dotnet run` on any of the samples or `src` projects, or open the solution in VS and Ctrl+F5 on any of the sample projects 111 | 112 | # Contributing 113 | 114 | This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org) to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](https://www.dotnetfoundation.org/code-of-conduct). 115 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /dotnet-wasi-sdk.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32104.313 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{03FBB6C8-E4A9-4681-93EB-CC98F01A4DAD}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasi.Sdk", "src\Wasi.Sdk\Wasi.Sdk.csproj", "{3F6AD51F-3CFA-44C2-A8F5-94DF523D0780}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{CB438E7A-1E2B-4F84-9DD8-0C0E8B1BF3B3}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{B99E9F2E-912C-47E7-B99A-0EC29D912C3C}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreOnCustomHost", "samples\AspNetCoreOnCustomHost\AspNetCoreOnCustomHost.csproj", "{02D1DA0B-9822-40C6-A46B-78879757E5CB}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasi.AspNetCore.Server", "src\Wasi.AspNetCore.Server\Wasi.AspNetCore.Server.csproj", "{C696B3BB-CC35-4F92-A2FB-3D8A62D04B00}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasi.AspNetCore.Server.CustomHost", "src\Wasi.AspNetCore.Server.CustomHost\Wasi.AspNetCore.Server.CustomHost.csproj", "{3EDC6C27-2A60-4DCE-A4E5-5901004DD6F7}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasi.AspNetCore.Server.Atmo", "src\Wasi.AspNetCore.Server.Atmo\Wasi.AspNetCore.Server.Atmo.csproj", "{FB2A4419-AD9F-4BE8-A679-986415B2F852}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasi.AspNetCore.Server.Native", "src\Wasi.AspNetCore.Server.Native\Wasi.AspNetCore.Server.Native.csproj", "{B8DB11C4-BC89-49FC-B3D3-7685805C408D}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreOnNativeWasi", "samples\AspNetCoreOnNativeWasi\AspNetCoreOnNativeWasi.csproj", "{FC3A1A34-97DE-417B-8EFF-E2AC29E8B15C}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasi.AspNetCore.BundledFiles", "src\Wasi.AspNetCore.BundledFiles\Wasi.AspNetCore.BundledFiles.csproj", "{FCEF466A-8987-4309-9964-256446764961}" 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "atmo", "atmo", "{874A0D81-B483-46E9-B3B1-C69660B2AD41}" 29 | ProjectSection(SolutionItems) = preProject 30 | samples\atmo\docker-compose.yml = samples\atmo\docker-compose.yml 31 | EndProjectSection 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreOnAtmo", "samples\atmo\AspNetCoreOnAtmo\AspNetCoreOnAtmo.csproj", "{64B1732A-3C97-4745-B74F-F77921CC34F8}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {3F6AD51F-3CFA-44C2-A8F5-94DF523D0780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {3F6AD51F-3CFA-44C2-A8F5-94DF523D0780}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {3F6AD51F-3CFA-44C2-A8F5-94DF523D0780}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {3F6AD51F-3CFA-44C2-A8F5-94DF523D0780}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {B99E9F2E-912C-47E7-B99A-0EC29D912C3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {B99E9F2E-912C-47E7-B99A-0EC29D912C3C}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {B99E9F2E-912C-47E7-B99A-0EC29D912C3C}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {B99E9F2E-912C-47E7-B99A-0EC29D912C3C}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {02D1DA0B-9822-40C6-A46B-78879757E5CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {02D1DA0B-9822-40C6-A46B-78879757E5CB}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {02D1DA0B-9822-40C6-A46B-78879757E5CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {02D1DA0B-9822-40C6-A46B-78879757E5CB}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {C696B3BB-CC35-4F92-A2FB-3D8A62D04B00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {C696B3BB-CC35-4F92-A2FB-3D8A62D04B00}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {C696B3BB-CC35-4F92-A2FB-3D8A62D04B00}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {C696B3BB-CC35-4F92-A2FB-3D8A62D04B00}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {3EDC6C27-2A60-4DCE-A4E5-5901004DD6F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {3EDC6C27-2A60-4DCE-A4E5-5901004DD6F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {3EDC6C27-2A60-4DCE-A4E5-5901004DD6F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {3EDC6C27-2A60-4DCE-A4E5-5901004DD6F7}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {FB2A4419-AD9F-4BE8-A679-986415B2F852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {FB2A4419-AD9F-4BE8-A679-986415B2F852}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {FB2A4419-AD9F-4BE8-A679-986415B2F852}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {FB2A4419-AD9F-4BE8-A679-986415B2F852}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {B8DB11C4-BC89-49FC-B3D3-7685805C408D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {B8DB11C4-BC89-49FC-B3D3-7685805C408D}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {B8DB11C4-BC89-49FC-B3D3-7685805C408D}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {B8DB11C4-BC89-49FC-B3D3-7685805C408D}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {FC3A1A34-97DE-417B-8EFF-E2AC29E8B15C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {FC3A1A34-97DE-417B-8EFF-E2AC29E8B15C}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {FC3A1A34-97DE-417B-8EFF-E2AC29E8B15C}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {FC3A1A34-97DE-417B-8EFF-E2AC29E8B15C}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {FCEF466A-8987-4309-9964-256446764961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {FCEF466A-8987-4309-9964-256446764961}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {FCEF466A-8987-4309-9964-256446764961}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {FCEF466A-8987-4309-9964-256446764961}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {64B1732A-3C97-4745-B74F-F77921CC34F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {64B1732A-3C97-4745-B74F-F77921CC34F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {64B1732A-3C97-4745-B74F-F77921CC34F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {64B1732A-3C97-4745-B74F-F77921CC34F8}.Release|Any CPU.Build.0 = Release|Any CPU 81 | EndGlobalSection 82 | GlobalSection(SolutionProperties) = preSolution 83 | HideSolutionNode = FALSE 84 | EndGlobalSection 85 | GlobalSection(NestedProjects) = preSolution 86 | {3F6AD51F-3CFA-44C2-A8F5-94DF523D0780} = {03FBB6C8-E4A9-4681-93EB-CC98F01A4DAD} 87 | {B99E9F2E-912C-47E7-B99A-0EC29D912C3C} = {CB438E7A-1E2B-4F84-9DD8-0C0E8B1BF3B3} 88 | {02D1DA0B-9822-40C6-A46B-78879757E5CB} = {CB438E7A-1E2B-4F84-9DD8-0C0E8B1BF3B3} 89 | {C696B3BB-CC35-4F92-A2FB-3D8A62D04B00} = {03FBB6C8-E4A9-4681-93EB-CC98F01A4DAD} 90 | {3EDC6C27-2A60-4DCE-A4E5-5901004DD6F7} = {03FBB6C8-E4A9-4681-93EB-CC98F01A4DAD} 91 | {FB2A4419-AD9F-4BE8-A679-986415B2F852} = {03FBB6C8-E4A9-4681-93EB-CC98F01A4DAD} 92 | {B8DB11C4-BC89-49FC-B3D3-7685805C408D} = {03FBB6C8-E4A9-4681-93EB-CC98F01A4DAD} 93 | {FC3A1A34-97DE-417B-8EFF-E2AC29E8B15C} = {CB438E7A-1E2B-4F84-9DD8-0C0E8B1BF3B3} 94 | {FCEF466A-8987-4309-9964-256446764961} = {03FBB6C8-E4A9-4681-93EB-CC98F01A4DAD} 95 | {874A0D81-B483-46E9-B3B1-C69660B2AD41} = {CB438E7A-1E2B-4F84-9DD8-0C0E8B1BF3B3} 96 | {64B1732A-3C97-4745-B74F-F77921CC34F8} = {874A0D81-B483-46E9-B3B1-C69660B2AD41} 97 | EndGlobalSection 98 | GlobalSection(ExtensibilityGlobals) = postSolution 99 | SolutionGuid = {AAE5B3CC-04B7-406F-A2D4-19AA589238D6} 100 | EndGlobalSection 101 | EndGlobal 102 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnCustomHost/AspNetCoreOnCustomHost.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | net7.0 10 | enable 11 | enable 12 | $(WebServerWasiHostExecutable) 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnCustomHost/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnCustomHost/Pages/MyRazorPage.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using System.Runtime.InteropServices 3 | @model AspNetCoreOnCustomHost.Pages.MyRazorPageModel 4 | 5 |

Razor pages

6 | 7 |

8 | Hello World! This is running on @RuntimeInformation.FrameworkDescription, 9 | on @RuntimeInformation.OSArchitecture, 10 | at @DateTime.Now.ToLongTimeString() 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnCustomHost/Pages/MyRazorPage.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace AspNetCoreOnCustomHost.Pages 4 | { 5 | public class MyRazorPageModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnCustomHost/Program.cs: -------------------------------------------------------------------------------- 1 | using Wasi.AspNetCore.Server.CustomHost; 2 | 3 | var builder = WebApplication.CreateBuilder(args).UseWasiCustomHostServer(); 4 | builder.Services.AddRazorPages(); 5 | 6 | var app = builder.Build(); 7 | app.UseStaticFiles(); 8 | app.MapRazorPages(); 9 | 10 | app.MapGet("/", () => "Hello, world! See also: /weatherforecast and /myrazorpage"); 11 | 12 | app.MapGet("/weatherforecast", () => 13 | { 14 | var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 15 | var forecast = Enumerable.Range(1, 5).Select(index => new 16 | { 17 | Date = DateTime.Now.AddDays(index), 18 | TempC = Random.Shared.Next(-20, 55), 19 | Summary = summaries[Random.Shared.Next(summaries.Length)] 20 | }).ToArray(); 21 | return forecast; 22 | }); 23 | 24 | await app.RunAsync(); 25 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnCustomHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "AspNetCoreOnCustomHost": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://localhost:63303" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /samples/AspNetCoreOnCustomHost/wwwroot/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet/dotnet-wasi-sdk/6844eb7c7878b61c75423d960a6b7e4195b97c2d/samples/AspNetCoreOnCustomHost/wwwroot/logo.png -------------------------------------------------------------------------------- /samples/AspNetCoreOnNativeWasi/AspNetCoreOnNativeWasi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | net7.0 10 | enable 11 | enable 12 | --tcplisten localhost:8080 --env ASPNETCORE_URLS=http://localhost:8080 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnNativeWasi/Pages/MyRazorPage.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using System.Runtime.InteropServices 3 | @model AspNetCoreOnNativeWasi.Pages.MyRazorPageModel 4 | 5 |

Razor pages

6 | 7 |

8 | Hello World! This is running on @RuntimeInformation.FrameworkDescription, 9 | on @RuntimeInformation.OSArchitecture, 10 | at @DateTime.Now.ToLongTimeString() 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnNativeWasi/Pages/MyRazorPage.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace AspNetCoreOnNativeWasi.Pages 4 | { 5 | public class MyRazorPageModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnNativeWasi/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args).UseWasiConnectionListener(); 2 | builder.Services.AddRazorPages(); 3 | 4 | var app = builder.Build(); 5 | app.UseBundledStaticFiles(); 6 | app.MapRazorPages(); 7 | 8 | app.MapGet("/", () => "Hello, world! See also: /weatherforecast and /myrazorpage"); 9 | 10 | app.MapGet("/weatherforecast", () => 11 | { 12 | var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 13 | var forecast = Enumerable.Range(1, 5).Select(index => new 14 | { 15 | Date = DateTime.Now.AddDays(index), 16 | TempC = Random.Shared.Next(-20, 55), 17 | Summary = summaries[Random.Shared.Next(summaries.Length)] 18 | }).ToArray(); 19 | return forecast; 20 | }); 21 | 22 | app.Start(); 23 | -------------------------------------------------------------------------------- /samples/AspNetCoreOnNativeWasi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "AspNetCoreOnCustomHost": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://localhost:8080" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /samples/AspNetCoreOnNativeWasi/wwwroot/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet/dotnet-wasi-sdk/6844eb7c7878b61c75423d960a6b7e4195b97c2d/samples/AspNetCoreOnNativeWasi/wwwroot/logo.png -------------------------------------------------------------------------------- /samples/ConsoleApp/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach", 6 | "type": "mono", 7 | "request": "attach", 8 | "address": "127.0.0.1", 9 | "port": 64000 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /samples/ConsoleApp/ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Exe 8 | net7.0 9 | enable 10 | enable 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /samples/ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | Console.WriteLine($"Hello, world at {DateTime.Now.ToLongTimeString()} on {RuntimeInformation.OSArchitecture}!"); 4 | -------------------------------------------------------------------------------- /samples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/atmo/AspNetCoreOnAtmo/AspNetCoreOnAtmo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | net7.0 10 | enable 11 | enable 12 | atmo 13 | 14 | 15 | 16 | 20 | sat 21 | 22 | 23 | 24 | 26 | true 27 | 63305 28 | wsl 29 | -d Ubuntu-for-Docker AspNetBinDir=%24(pwd)/bin/$(Configuration)/net7.0 AtmoPort=$(AtmoPort) docker-compose up 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /samples/atmo/AspNetCoreOnAtmo/Directive.yaml: -------------------------------------------------------------------------------- 1 | identifier: com.suborbital.important-api 2 | appVersion: v0.1.0 3 | atmoVersion: v0.4.3 4 | 5 | handlers: 6 | - type: request 7 | method: GET 8 | resource: / 9 | steps: 10 | - fn: AspNetCoreOnAtmo 11 | - type: request 12 | method: GET 13 | resource: /api/*url 14 | steps: 15 | - fn: AspNetCoreOnAtmo 16 | 17 | connections: 18 | redis: 19 | serverAddress: redis-server:6379 20 | -------------------------------------------------------------------------------- /samples/atmo/AspNetCoreOnAtmo/Pages/MyRazorPage.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using System.Runtime.InteropServices 3 | @model AspNetCoreOnAtmo.Pages.MyRazorPageModel 4 | 5 |

Razor pages

6 | 7 |

8 | Hello World! This is running on @RuntimeInformation.FrameworkDescription, 9 | on @RuntimeInformation.OSArchitecture, 10 | at @DateTime.Now.ToLongTimeString() 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/atmo/AspNetCoreOnAtmo/Pages/MyRazorPage.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace AspNetCoreOnAtmo.Pages 4 | { 5 | public class MyRazorPageModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/atmo/AspNetCoreOnAtmo/Program.cs: -------------------------------------------------------------------------------- 1 | using Wasi.AspNetCore.Server.Atmo; 2 | using AtmoCache = Wasi.AspNetCore.Server.Atmo.Services.Cache; 3 | 4 | AtmoLogger.RedirectConsoleToAtmoLogs(); 5 | var builder = WebApplication.CreateBuilder(args).UseAtmoServer(); 6 | builder.Services.AddRazorPages(); 7 | builder.Services.AddAtmoCache(); 8 | 9 | var app = builder.Build(); 10 | app.UseStaticFiles(); 11 | app.MapRazorPages(); 12 | 13 | app.MapGet("/", () => "Hello, world! See also: /api/getvalue/{key} and /api/setvalue/{key}/{value}"); 14 | 15 | // Demonstrate getting and setting values in Atmo's key-value store, which is backed by Redis 16 | 17 | app.MapGet("/api/getvalue/{key}", (AtmoCache cache, string key) => 18 | { 19 | return cache.GetString(key) is string result 20 | ? Results.Ok(result) 21 | : Results.NotFound(); 22 | }); 23 | 24 | app.MapGet("/api/setvalue/{key}/{value}", (AtmoCache cache, string key, string value) => 25 | { 26 | cache.Set(key, value, TimeSpan.FromMinutes(1)); 27 | return Results.Ok(); 28 | }); 29 | 30 | await app.StartAsync(); 31 | -------------------------------------------------------------------------------- /samples/atmo/AspNetCoreOnAtmo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "AspNetCoreOnCustomHost": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development", 8 | "SAT_HTTP_PORT": "63305" 9 | }, 10 | "applicationUrl": "http://localhost:63305" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /samples/atmo/AspNetCoreOnAtmo/wwwroot/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet/dotnet-wasi-sdk/6844eb7c7878b61c75423d960a6b7e4195b97c2d/samples/atmo/AspNetCoreOnAtmo/wwwroot/logo.png -------------------------------------------------------------------------------- /samples/atmo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | redis-server: 4 | image: redis:latest 5 | container_name: redis-server 6 | ports: 7 | - 6379:6379 8 | atmo-server: 9 | image: suborbital/atmo:v0.4.3 10 | container_name: atmo-server 11 | entrypoint: atmo 12 | environment: 13 | - ATMO_HTTP_PORT=80 14 | ports: 15 | - ${AtmoPort}:80 16 | volumes: 17 | - ${AspNetBinDir}:/home/atmo 18 | -------------------------------------------------------------------------------- /samples/webserver-wasi-host/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /samples/webserver-wasi-host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webserver-wasi-host" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.61" 10 | wasmtime = "0.39.1" 11 | wasmtime-wasi = "0.39.1" 12 | tiny_http = "0.11.0" 13 | -------------------------------------------------------------------------------- /samples/webserver-wasi-host/WebserverWasiHost.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $(WebServerWasiHostRoot)target\release\webserver-wasi-host 15 | $(WebServerWasiHostExecutable).exe 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/webserver-wasi-host/src/main.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | use std::error::Error; 3 | use std::path::Path; 4 | use server::*; 5 | use anyhow::Result; 6 | use wasmtime::*; 7 | use wasmtime_wasi::*; 8 | 9 | fn main() -> Result<(), Box> { 10 | let args: Vec = std::env::args().collect(); 11 | let wasm_file_to_execute = get_wasm_file_to_execute(args)?; 12 | 13 | // Define the WASI functions globally on the `Config`. 14 | let engine = Engine::default(); 15 | let mut linker: Linker = Linker::new(&engine); 16 | wasmtime_wasi::add_to_linker(&mut linker, |s| s.wasi_ctx_mut())?; 17 | DotNetHttpServer::add_to_linker(&mut linker)?; 18 | 19 | // Create a WASI context and put it in a Store; all instances in the store 20 | // share this context. `WasiCtxBuilder` provides a number of ways to 21 | // configure what the target program will have access to. 22 | let wasi = WasiCtxBuilder::new() 23 | .inherit_stdio() 24 | .preopened_dir(sync::Dir::open_ambient_dir(std::env::current_dir()?, sync::ambient_authority())?, ".")? 25 | .inherit_args()? 26 | 27 | // For security reasons, you might not really want to let the WASM-sandboxed code see all the environment 28 | // variables from the native host process. However it is a convenient way to pass through everything from 29 | // launchSettings.json. 30 | .env("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", "")? // Disable watch (otherwise, "Startup assembly Microsoft.AspNetCore.Watch.BrowserRefresh failed to execute") 31 | .inherit_env()? 32 | 33 | .build(); 34 | let mut store = Store::new(&engine, DotNetHttpServerStore::new(wasi)); 35 | 36 | // Instantiate our module with the imports we've created, and run it. 37 | let module = Module::from_file(&engine, wasm_file_to_execute)?; 38 | linker.module(&mut store, "", &module)?; 39 | linker 40 | .get_default(&mut store, "")? 41 | .typed::<(), (), _>(&store)? 42 | .call(&mut store, ())?; 43 | 44 | Ok(()) 45 | } 46 | 47 | fn get_wasm_file_to_execute(args: Vec) -> Result> { 48 | if args.len() < 2 49 | { 50 | return Err(format!("Usage: {} file.wasm", args.get(0).unwrap()).into()); 51 | } 52 | 53 | let wasm_file_to_execute = args.get(1).unwrap(); 54 | if !Path::new(wasm_file_to_execute).exists() 55 | { 56 | return Err(format!("Could not find file {}", wasm_file_to_execute).into()); 57 | } 58 | 59 | Ok(wasm_file_to_execute.to_string()) 60 | } 61 | -------------------------------------------------------------------------------- /samples/webserver-wasi-host/src/server.rs: -------------------------------------------------------------------------------- 1 | use tiny_http::Header; 2 | use wasmtime_wasi::WasiCtx; 3 | use anyhow::*; 4 | use tiny_http::{Server, Request}; 5 | use wasmtime::*; 6 | use std::collections::HashMap; 7 | 8 | pub struct RequestContext { 9 | request: Request, 10 | response_headers: Vec
, 11 | response_chunks: Vec>, 12 | } 13 | 14 | pub struct DotNetHttpServerStore { 15 | wasi_ctx: WasiCtx, 16 | next_request_id: u32, 17 | pending_requests: HashMap::, 18 | } 19 | 20 | impl DotNetHttpServerStore { 21 | pub fn new(wasi_ctx: WasiCtx) -> DotNetHttpServerStore { 22 | DotNetHttpServerStore { wasi_ctx, next_request_id: 1, pending_requests: HashMap::new() } 23 | } 24 | 25 | pub fn wasi_ctx_mut(&mut self) -> &mut WasiCtx { 26 | &mut self.wasi_ctx 27 | } 28 | 29 | pub fn push_response_header(&mut self, request_id: u32, header: Header) { 30 | if let Some(request_context) = self.pending_requests.get_mut(&request_id) { 31 | request_context.response_headers.push(header); 32 | } else { 33 | println!("WARNING: No such pending request {}", request_id); 34 | } 35 | } 36 | 37 | pub fn push_response_chunk(&mut self, request_id: u32, chunk: Vec) { 38 | // tiny_http doesn't seem to support streaming responses, so we have to collect all the chunks in memory 39 | // until we're ready to respond. I should remove tiny_http and switch either to a better high-level HTTP 40 | // framework, or just build directly on epoll-like APIs. 41 | if let Some(request_context) = self.pending_requests.get_mut(&request_id) { 42 | request_context.response_chunks.push(chunk); 43 | } else { 44 | println!("WARNING: No such pending request {}", request_id); 45 | } 46 | } 47 | 48 | pub fn complete_response(&mut self, request_id: u32, status_code: i32) { 49 | if let Some(mut request_context) = self.pending_requests.remove(&request_id) { 50 | let all_chunks = request_context.response_chunks.concat(); 51 | let mut response = tiny_http::Response::from_data(all_chunks).with_status_code(status_code); 52 | 53 | while let Some(h) = request_context.response_headers.pop() { 54 | response.add_header(h); 55 | } 56 | 57 | request_context.request.respond(response).expect("Failed to send response"); 58 | } 59 | } 60 | } 61 | 62 | pub struct DotNetHttpServer<'a> { 63 | caller: wasmtime::Caller<'a, DotNetHttpServerStore>, 64 | wasm_on_incoming_request: TypedFunc<(i32, u32, i32, i32, i32, i32, i32), ()>, 65 | wasm_malloc: TypedFunc, 66 | wasm_memory: wasmtime::Memory, 67 | } 68 | 69 | impl DotNetHttpServer<'_> { 70 | pub fn add_to_linker(linker: &mut Linker) -> Result<()> { 71 | linker.func_wrap("env", "start_http_server", |caller: Caller, dotnet_http_server: i32, port: i32| { 72 | crate::server::launch_server(u16::try_from(port).unwrap(), dotnet_http_server, caller); 73 | })?; 74 | 75 | linker.func_wrap("env", "response_send_chunk", |mut caller: Caller, request_id: u32, buffer_ptr: i32, buffer_len: i32| { 76 | let bytes: Vec = { 77 | let wasm_memory = caller.get_export("memory").expect("Missing export 'memory'").into_memory().unwrap(); 78 | let mut store = caller.as_context_mut(); 79 | let bytes = wasm_memory.data(&mut store).get((buffer_ptr as usize)..((buffer_ptr+buffer_len) as usize)).unwrap(); 80 | bytes.to_vec() 81 | }; 82 | caller.data_mut().push_response_chunk(request_id, bytes); 83 | })?; 84 | 85 | linker.func_wrap("env", "response_add_header", |mut caller: Caller, request_id: u32, name_ptr: i32, name_len: i32, value_ptr: i32, value_len: i32| { 86 | let (name, value) = { 87 | let wasm_memory = caller.get_export("memory").expect("Missing export 'memory'").into_memory().unwrap(); 88 | let mut store = caller.as_context_mut(); 89 | let heap = wasm_memory.data(&mut store); 90 | let name = heap.get((name_ptr as usize)..((name_ptr+name_len) as usize)).unwrap(); 91 | let value = heap.get((value_ptr as usize)..((value_ptr+value_len) as usize)).unwrap(); 92 | (name.to_vec(), value.to_vec()) 93 | }; 94 | caller.data_mut().push_response_header(request_id, tiny_http::Header::from_bytes(name, value).unwrap()); 95 | })?; 96 | 97 | linker.func_wrap("env", "response_complete", |mut caller: Caller, request_id: u32, status_code: i32| { 98 | caller.data_mut().complete_response(request_id, status_code); 99 | })?; 100 | 101 | Ok(()) 102 | } 103 | 104 | fn new(mut caller: wasmtime::Caller) -> DotNetHttpServer { 105 | let wasm_on_incoming_request = caller.get_export("on_incoming_request").expect("Missing export 'on_incoming_request'"); 106 | let wasm_malloc = caller.get_export("malloc").expect("Missing export 'malloc'"); 107 | let wasm_memory = caller.get_export("memory").expect("Missing export 'memory'").into_memory().unwrap(); 108 | 109 | let mut store = caller.as_context_mut(); 110 | 111 | let wasm_on_incoming_request = wasm_on_incoming_request.into_func().unwrap().typed::<(i32, u32, i32, i32, i32, i32, i32), (), _>(&mut store).expect("Type mismatch for 'on_incoming_request'"); 112 | let wasm_malloc = wasm_malloc.into_func().unwrap().typed::(&mut store).expect("Type mismatch for 'malloc'"); 113 | 114 | DotNetHttpServer { 115 | caller, 116 | wasm_on_incoming_request, 117 | wasm_malloc, 118 | wasm_memory, 119 | } 120 | } 121 | 122 | fn store_data_mut(&mut self) -> &mut DotNetHttpServerStore { 123 | self.caller.data_mut() 124 | } 125 | 126 | fn on_incoming_request(&mut self, dotnet_http_server: i32, mut request: Request) -> Result<()> { 127 | let method_str = self.create_wasm_string(request.method().as_str())?; 128 | let url_str = self.create_wasm_string(request.url())?; 129 | 130 | let mut headers_combined = String::new(); 131 | for header in request.headers() { 132 | headers_combined.push_str(header.field.as_str().as_str()); 133 | headers_combined.push_str(":"); 134 | headers_combined.push_str(header.value.as_str()); 135 | headers_combined.push_str("\n"); 136 | } 137 | let headers_combined_str = self.create_wasm_string(headers_combined.as_str())?; 138 | 139 | let mut body_buf = Vec::::new(); 140 | let body_buf_len = request.as_reader().read_to_end(&mut body_buf).unwrap(); 141 | let body_buf_ptr = self.create_wasm_ptr(&body_buf)?; 142 | 143 | let store_data_mut = self.store_data_mut(); 144 | let request_id = store_data_mut.next_request_id; 145 | store_data_mut.next_request_id += 1; 146 | 147 | store_data_mut.pending_requests.insert(request_id, RequestContext { 148 | request, 149 | response_chunks: Vec::>::new(), 150 | response_headers: Vec::
::new(), 151 | }); 152 | 153 | let mut store = self.caller.as_context_mut(); 154 | self.wasm_on_incoming_request.call(&mut store, (dotnet_http_server, request_id, method_str, url_str, headers_combined_str, body_buf_ptr, body_buf_len as i32))?; 155 | Ok(()) 156 | } 157 | 158 | fn create_wasm_string(&mut self, value: &str) -> Result { 159 | let value_utf8 = value.as_bytes(); 160 | self.create_wasm_ptr(value_utf8) 161 | } 162 | 163 | fn create_wasm_ptr(&mut self, value: &[u8]) -> Result { 164 | let value_len = value.len(); 165 | let mut store = self.caller.as_context_mut(); 166 | let wasm_ptr = self.wasm_malloc.call(&mut store, value_len as i32 + 1)?; 167 | self.wasm_memory.write(&mut store, wasm_ptr as usize, value)?; 168 | self.wasm_memory.write(&mut store, (wasm_ptr as usize) + value_len, &[0])?; 169 | Ok(wasm_ptr) 170 | } 171 | } 172 | 173 | pub fn launch_server(port: u16, dotnet_http_server: i32, caller: wasmtime::Caller<'_, DotNetHttpServerStore>) -> () { 174 | run_server(port, dotnet_http_server, caller).ok(); 175 | } 176 | 177 | fn run_server(port: u16, dotnet_http_server: i32, caller: wasmtime::Caller<'_, DotNetHttpServerStore>) -> Result<()> { 178 | let mut dotnet = DotNetHttpServer::new(caller); 179 | 180 | let server = Server::http(format!("0.0.0.0:{}", port)).unwrap(); 181 | 182 | for request in server.incoming_requests() { 183 | dotnet.on_incoming_request(dotnet_http_server, request)?; 184 | } 185 | 186 | Ok(()) 187 | } 188 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.BundledFiles/Wasi.AspNetCore.BundledFiles.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <_PackageFiles Include="build\**" BuildAction="Content" PackagePath="build" /> 17 | <_PackageFiles Include="native\**" BuildAction="Content" PackagePath="native" /> 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.BundledFiles/WasiBundledFileProvider.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.FileProviders; 5 | using Microsoft.Extensions.Primitives; 6 | using System.Collections; 7 | using System.Runtime.CompilerServices; 8 | 9 | namespace Wasi.AspNetCore.BundledFiles; 10 | 11 | public class WasiBundledFileProvider : IFileProvider 12 | { 13 | [MethodImpl(MethodImplOptions.InternalCall)] 14 | private static extern unsafe byte* GetEmbeddedFile(string name, out int length); 15 | 16 | private readonly static DateTime FakeLastModified = new DateTime(2000, 1, 1); 17 | 18 | public IDirectoryContents GetDirectoryContents(string subpath) 19 | { 20 | return new BundledDirectoryContents(this, subpath); 21 | } 22 | 23 | public unsafe IFileInfo GetFileInfo(string subpath) 24 | { 25 | var subpathWithoutLeadingSlash = subpath.AsSpan(1); 26 | var fileBytes = GetEmbeddedFile($"wwwroot/{subpathWithoutLeadingSlash}", out var length); 27 | return fileBytes == null 28 | ? new NotFoundFileInfo(subpath) 29 | : new BundledFileInfo(subpath, length, FakeLastModified, fileBytes); 30 | } 31 | 32 | public IChangeToken Watch(string filter) 33 | { 34 | throw new NotImplementedException(); 35 | } 36 | 37 | unsafe class BundledFileInfo : IFileInfo 38 | { 39 | private byte* _fileBytes; 40 | 41 | public BundledFileInfo(string name, long length, DateTime lastModified, byte* fileBytes) 42 | { 43 | Name = name; 44 | LastModified = lastModified; 45 | Length = length; 46 | _fileBytes = fileBytes; 47 | } 48 | 49 | public bool Exists => true; 50 | 51 | public bool IsDirectory => false; 52 | 53 | public DateTimeOffset LastModified { get; } 54 | 55 | public long Length { get; } 56 | 57 | public string Name { get; } 58 | 59 | public string? PhysicalPath => null; 60 | 61 | public Stream CreateReadStream() 62 | => new UnmanagedMemoryStream(_fileBytes, Length); 63 | } 64 | 65 | class BundledDirectoryContents : IDirectoryContents 66 | { 67 | private readonly IFileProvider _owner; 68 | private readonly string _subpath; 69 | 70 | public BundledDirectoryContents(IFileProvider owner, string subpath) 71 | { 72 | _owner = owner; 73 | _subpath = subpath; 74 | } 75 | 76 | public bool Exists => _subpath == "/"; 77 | 78 | IEnumerator IEnumerable.GetEnumerator() 79 | => GetEnumerator(); 80 | 81 | public IEnumerator GetEnumerator() 82 | { 83 | // TODO: Mechanism for enumerating everything in a bundled directory 84 | // Currently this only recognizes index.html files to support UseDefaultFiles 85 | if (_subpath == "/") 86 | { 87 | var fileInfo = _owner.GetFileInfo("/index.html"); 88 | if (fileInfo.Exists) 89 | { 90 | yield return fileInfo; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.BundledFiles/WasiNativeWebApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Wasi.AspNetCore.BundledFiles; 7 | 8 | namespace Microsoft.AspNetCore.Builder; 9 | 10 | public static class WasiNativeWebApplicationExtensions 11 | { 12 | public static IApplicationBuilder UseBundledStaticFiles(this IApplicationBuilder app, StaticFileOptions? options = null) 13 | { 14 | // Not sure why you'd pass a fileprovider if you're asking to use the bundled files, but just in case, 15 | // only use the WasiBundledFileProvider if no other was specified 16 | if (options?.FileProvider is null) 17 | { 18 | var env = app.ApplicationServices.GetRequiredService(); 19 | env.WebRootFileProvider = new WasiBundledFileProvider(); 20 | } 21 | 22 | if (options is null) 23 | { 24 | app.UseStaticFiles(); 25 | } 26 | else 27 | { 28 | app.UseStaticFiles(options); 29 | } 30 | 31 | return app; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.BundledFiles/build/Wasi.AspNetCore.BundledFiles.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.BundledFiles/build/Wasi.AspNetCore.BundledFiles.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.BundledFiles/native/interop.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const char* dotnet_wasi_getbundledfile(const char* name, int* out_length); 4 | 5 | const char* mono_get_embedded_file(MonoString* name, int* out_length) { 6 | char* name_utf8 = mono_wasm_string_get_utf8(name); 7 | return dotnet_wasi_getbundledfile(name_utf8, out_length); 8 | } 9 | 10 | void bundled_files_attach_internal_calls() { 11 | mono_add_internal_call("Wasi.AspNetCore.BundledFiles.WasiBundledFileProvider::GetEmbeddedFile", mono_get_embedded_file); 12 | } 13 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/AtmoLogger.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Wasi.AspNetCore.Server.Atmo; 4 | 5 | public static class AtmoLogger 6 | { 7 | public static void RedirectConsoleToAtmoLogs() 8 | { 9 | Console.SetError(new StreamWriter(new AtmoLogsWriterStream()) { AutoFlush = true }); 10 | Console.SetOut(new StreamWriter(new AtmoLogsWriterStream()) { AutoFlush = true }); 11 | } 12 | 13 | internal class AtmoLogsWriterStream : Stream 14 | { 15 | private static ConcurrentQueue _pendingMessages = new(); 16 | 17 | public override bool CanRead => false; 18 | 19 | public override bool CanSeek => false; 20 | 21 | public override bool CanWrite => true; 22 | 23 | public override long Length => throw new NotImplementedException(); 24 | 25 | public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 26 | 27 | public override void Flush() 28 | { 29 | } 30 | 31 | public override int Read(byte[] buffer, int offset, int count) 32 | => throw new NotImplementedException(); 33 | 34 | public override long Seek(long offset, SeekOrigin origin) 35 | => throw new NotImplementedException(); 36 | 37 | public override void SetLength(long value) 38 | => throw new NotImplementedException(); 39 | 40 | public override void Write(byte[] buffer, int offset, int count) 41 | { 42 | _pendingMessages.Enqueue(new Span(buffer, offset, count).ToArray()); 43 | } 44 | 45 | internal static unsafe void EmitPendingMessages(uint ident) 46 | { 47 | while (_pendingMessages.TryDequeue(out var message)) 48 | { 49 | fixed (byte* messagePtr = message) 50 | { 51 | Interop.LogMessageRaw(ident, 1, messagePtr, message.Length); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/AtmoRequestContext.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Http.Features; 6 | using static Wasi.AspNetCore.Server.Atmo.AtmoLogger; 7 | 8 | namespace Wasi.AspNetCore.Server.Atmo; 9 | 10 | internal class AtmoRequestContext : WasiServerRequestContext 11 | { 12 | public uint Ident { get; } 13 | 14 | public AtmoRequestContext(uint ident, string httpMethod, string url, HeaderDictionary headers) 15 | : base(httpMethod, url, headers, new MemoryStream()) 16 | { 17 | Ident = ident; 18 | } 19 | 20 | protected override unsafe Task TransmitResponseAsync() 21 | { 22 | AtmoLogsWriterStream.EmitPendingMessages(Ident); 23 | 24 | var response = (IHttpResponseFeature)this; 25 | 26 | foreach (var h in response.Headers) 27 | { 28 | Interop.ResponseAddHeader(Ident, h.Key, h.Value.ToString()); 29 | } 30 | 31 | var ms = new MemoryStream(); 32 | 33 | var responseBody = (IHttpResponseBodyFeature)this; 34 | responseBody.Stream.Position = 0; 35 | responseBody.Stream.CopyTo(ms); 36 | var responseBodyBuffer = ms.GetBuffer(); 37 | 38 | fixed (byte* rawResponseBytesPtr = responseBodyBuffer) 39 | { 40 | Interop.ResponseComplete(Ident, response.StatusCode, rawResponseBytesPtr, (int)ms.Length); 41 | } 42 | 43 | return Task.CompletedTask; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/AtmoServer.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Hosting.Server; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Hosting; 7 | using static Wasi.AspNetCore.Server.Atmo.Interop; 8 | 9 | namespace Wasi.AspNetCore.Server.Atmo; 10 | 11 | internal class AtmoServer : WasiServer 12 | { 13 | // Unfortunately Atmo's ABI doesn't provide a mechanism to enumerate all request headers, so we have to 14 | // ask it for a specific set of well-known headers that may or may not be present 15 | private static readonly string[] HeaderNames = new[] { "Content-Type", "Accept" }; 16 | 17 | public AtmoServer(IHostApplicationLifetime lifetime) : base(lifetime) 18 | { 19 | } 20 | 21 | protected override void Run(IHttpApplication application, int port) 22 | { 23 | var hostApiServerInterop = new Interop(); 24 | hostApiServerInterop.OnIncomingRequest += (sender, ident) => 25 | { 26 | var method = Interop.RequestGetField(FieldType.Meta, "method", ident); 27 | var url = Interop.RequestGetField(FieldType.Meta, "url", ident); 28 | var headers = GetHeaders(ident); 29 | var requestContext = new AtmoRequestContext( 30 | ident, 31 | method, 32 | url, 33 | headers); 34 | 35 | // This isn't meant to throw as it handles its own exceptions and sends errors to the response 36 | _ = HandleRequestAsync(application, requestContext); 37 | }; 38 | 39 | // The underlying native implementation blocks here. If the listening loop was implemented outside the WASM runtime 40 | // then we wouldn't block and would just react to subsequent calls into WASM to handle incoming requests. 41 | Interop.RunHttpServer(hostApiServerInterop, port); 42 | } 43 | 44 | private static HeaderDictionary GetHeaders(uint ident) 45 | { 46 | var result = new HeaderDictionary(); 47 | 48 | for (var headerNameIndex = 0; headerNameIndex < HeaderNames.Length; headerNameIndex++) 49 | { 50 | var headerName = HeaderNames[headerNameIndex]; 51 | result[headerName] = Interop.RequestGetField(FieldType.Header, headerName, ident); 52 | } 53 | 54 | return result; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/AtmoWebApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting.Server; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Http.Features; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Wasi.AspNetCore.Server.Atmo.Services; 11 | 12 | namespace Wasi.AspNetCore.Server.Atmo; 13 | 14 | public static class AtmoWebApplicationBuilderExtensions 15 | { 16 | public static WebApplicationBuilder UseAtmoServer(this WebApplicationBuilder builder) 17 | { 18 | builder.Services.AddSingleton(); 19 | builder.Services.AddScoped(); 20 | builder.Services.AddHttpContextAccessor(); 21 | builder.Logging.AddProvider(new WasiLoggingProvider()); 22 | return builder; 23 | } 24 | 25 | public static void AddAtmoCache(this IServiceCollection services) 26 | { 27 | services.AddScoped(servicesProvider => 28 | { 29 | var contextAccessor = servicesProvider.GetRequiredService(); 30 | var context = contextAccessor.HttpContext!; 31 | var request = (AtmoRequestContext)context.Features.Get()!; 32 | return new Services.Cache(request.Ident); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/Interop.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Runtime.CompilerServices; 5 | using static Wasi.AspNetCore.Server.Atmo.AtmoLogger; 6 | 7 | namespace Wasi.AspNetCore.Server.Atmo; 8 | 9 | internal class Interop 10 | { 11 | [MethodImpl(MethodImplOptions.InternalCall)] 12 | public static extern void RunHttpServer(Interop owner, int port); 13 | 14 | [MethodImpl(MethodImplOptions.InternalCall)] 15 | public static extern void ResponseAddHeader(uint ident, string name, string value); 16 | 17 | [MethodImpl(MethodImplOptions.InternalCall)] 18 | public static unsafe extern void ResponseComplete(uint ident, int statusCode, byte* body, int body_len); 19 | 20 | public event EventHandler? OnIncomingRequest; 21 | 22 | [MethodImpl(MethodImplOptions.InternalCall)] 23 | public static extern void LogMessage(uint ident, int level, string message); 24 | 25 | [MethodImpl(MethodImplOptions.InternalCall)] 26 | public static unsafe extern void LogMessageRaw(uint ident, int level, byte* message, int message_len); 27 | 28 | [MethodImpl(MethodImplOptions.InternalCall)] 29 | public static extern string RequestGetField(FieldType fieldType, string fieldName, uint ident); 30 | 31 | // TODO: Make sure this doesn't get trimmed if AOT compiled 32 | // It's static because we want it to be able to emit pending logs even if 'interop' is null 33 | private static unsafe void HandleIncomingRequest(Interop interop, uint ident) 34 | { 35 | AtmoLogsWriterStream.EmitPendingMessages(ident); 36 | interop.OnIncomingRequest?.Invoke(interop, ident); 37 | } 38 | 39 | public enum FieldType : int 40 | { 41 | Meta = 0, 42 | Body = 1, 43 | Header = 2, 44 | Params = 3, 45 | State = 4, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/Services/Cache.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | 4 | namespace Wasi.AspNetCore.Server.Atmo.Services; 5 | 6 | public class Cache 7 | { 8 | [MethodImpl(MethodImplOptions.InternalCall)] 9 | static unsafe extern byte[]? Get(uint ident, string key); 10 | 11 | [MethodImpl(MethodImplOptions.InternalCall)] 12 | static unsafe extern int Set(uint ident, string key, byte* value, int value_len, int ttl); 13 | 14 | private readonly uint _ident; 15 | 16 | public Cache(uint ident) 17 | { 18 | _ident = ident; 19 | } 20 | 21 | public unsafe byte[]? GetBytes(string key) 22 | { 23 | return Get(_ident, key); 24 | } 25 | 26 | public unsafe string? GetString(string key) 27 | { 28 | var bytes = GetBytes(key); 29 | return bytes is null ? null : Encoding.UTF8.GetString(bytes); 30 | } 31 | 32 | public unsafe void Set(string key, Span value, TimeSpan ttl) 33 | { 34 | fixed (byte* valuePtr = value) 35 | { 36 | Set(_ident, key, valuePtr, value.Length, (int)ttl.TotalSeconds); 37 | } 38 | } 39 | 40 | public unsafe void Set(string key, string value, TimeSpan ttl) 41 | { 42 | Set(key, Encoding.UTF8.GetBytes(value), ttl); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/Services/IdentAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Wasi.AspNetCore.Server.Atmo.Services; 4 | 5 | public class IdentAccessor 6 | { 7 | internal uint Ident { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/Wasi.AspNetCore.Server.Atmo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <_PackageFiles Include="build\**" BuildAction="Content" PackagePath="build" /> 17 | <_PackageFiles Include="native\**" BuildAction="Content" PackagePath="native" /> 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/build/Wasi.AspNetCore.Server.Atmo.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/build/Wasi.AspNetCore.Server.Atmo.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | <_AtmoRunnablesBundleInput Include="$(OutDir)$(AssemblyName).wasm" /> 16 | <_AtmoRunnablesBundleInput Include="Directive.yaml" /> 17 | 18 | 19 | <_AtmoRunnablesBundleOutputFile>$(OutDir)runnables.wasm.zip 20 | 21 | 22 | 23 | 24 | 25 | <_RunnablesTemp>$(IntermediateOutputPath)atmo\ 26 | 27 | 28 | <_RunnableLines Include="%20" /> 29 | <_RunnableLines Include="runnables:" /> 30 | <_RunnableLines Include="- name: $(AssemblyName)" /> 31 | <_RunnableLines Include="%20%20namespace: default" /> 32 | <_RunnableLines Include="%20%20lang: rust" /> 33 | <_RunnableLines Include="%20%20version: """ /> 34 | <_RunnableLines Include="%20%20apiVersion: 0.15.0" /> 35 | <_RunnableLines Include="%20%20fqfn: com.suborbital.$(AssemblyName)#default::$(AssemblyName)@v0.1.0" /> 36 | <_RunnableLines Include="%20%20fqfnUri: """ /> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Atmo/native/host_interop.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | __attribute__((import_name("log_msg"))) 8 | void log_msg(const char* msg, int msg_len, int log_level, int ident); 9 | 10 | __attribute__((import_name("return_result"))) 11 | void return_result(void* buf, int buf_len, int ident); 12 | 13 | __attribute__((import_name("return_error"))) 14 | void return_error(int code, void* buf, int buf_len, int ident); 15 | 16 | __attribute__((import_name("request_get_field"))) 17 | int request_get_field(int field_type, const char* key_ptr, int key_len, int ident); 18 | 19 | __attribute__((import_name("resp_set_header"))) 20 | void resp_set_header(const char* key_ptr, int key_len, const char* val_ptr, int val_len, int ident); 21 | 22 | __attribute__((import_name("get_ffi_result"))) 23 | int get_ffi_result(void* buf, int ident); 24 | 25 | __attribute__((import_name("cache_get"))) 26 | int cache_get(const char* key_ptr, int key_len, int ident); 27 | 28 | __attribute__((import_name("cache_set"))) 29 | int cache_set(const char* key_ptr, int key_len, void* value, int value_len, int ttl, int ident); 30 | 31 | __attribute__((export_name("allocate"))) 32 | void* allocate(int length) { 33 | return malloc(length + 1); 34 | } 35 | 36 | __attribute__((export_name("deallocate"))) 37 | void deallocate(void* ptr, int length) { 38 | free(ptr); 39 | } 40 | 41 | char* request_get_field_str(int field_type, const char* field_name, int ident) { 42 | int ffi_res_len = request_get_field(field_type, field_name, strlen(field_name), ident); 43 | char* ffi_res_buf = (char*)malloc(ffi_res_len + 1); 44 | assert(!get_ffi_result(ffi_res_buf, ident)); 45 | ffi_res_buf[ffi_res_len] = 0; 46 | return ffi_res_buf; 47 | } 48 | 49 | MonoString* request_get_field_mono(int field_type, MonoString* field_name, int ident) { 50 | char* field_name_utf8 = mono_wasm_string_get_utf8(field_name); 51 | char* result_utf8 = request_get_field_str(field_type, field_name_utf8, ident); 52 | 53 | MonoString* result_monostring = mono_wasm_string_from_js(result_utf8); 54 | 55 | free(field_name_utf8); 56 | free(result_utf8); 57 | 58 | return result_monostring; 59 | } 60 | 61 | MonoMethod* method_HandleIncomingRequest; 62 | MonoObject* interop_instance = 0; 63 | 64 | #define FIELD_TYPE_META 0 65 | #define FIELD_TYPE_BODY 1 66 | #define FIELD_TYPE_HEADER 2 67 | #define FIELD_TYPE_PARAMS 3 68 | #define FIELD_TYPE_STATE 4 69 | 70 | __attribute__((export_name("run_e"))) 71 | void run_e(char* buf, int buf_len, int ident) { 72 | if (!method_HandleIncomingRequest) { 73 | method_HandleIncomingRequest = lookup_dotnet_method("Wasi.AspNetCore.Server.Atmo.dll", "Wasi.AspNetCore.Server.Atmo", "Interop", "HandleIncomingRequest", -1); 74 | assert(method_HandleIncomingRequest); 75 | } 76 | 77 | void* method_params[] = { interop_instance, &ident }; 78 | MonoObject* exception; 79 | mono_wasm_invoke_method(method_HandleIncomingRequest, NULL, method_params, &exception); 80 | assert(!exception); 81 | } 82 | 83 | void atmo_log_message(int ident, int level, MonoString* message) { 84 | char* message_utf8 = mono_wasm_string_get_utf8(message); 85 | log_msg(message_utf8, strlen(message_utf8), level, ident); 86 | free(message_utf8); 87 | } 88 | 89 | void atmo_log_message_raw(int ident, int level, char* message, int message_len) { 90 | log_msg(message, message_len, level, ident); 91 | } 92 | 93 | void fake_settimeout(int timeout) { 94 | // Skipping 95 | } 96 | 97 | void run_http_server(MonoObject* interop, int port) { 98 | // Ignoring the port because that's decided by Atmo 99 | interop_instance = interop; 100 | } 101 | 102 | void response_add_header_mono(int ident, MonoString* name, MonoString* value) { 103 | char* name_utf8 = mono_wasm_string_get_utf8(name); 104 | char* value_utf8 = mono_wasm_string_get_utf8(value); 105 | resp_set_header(name_utf8, strlen(name_utf8), value_utf8, strlen(value_utf8), ident); 106 | } 107 | 108 | void response_complete(int ident, int status_code, char* body, int body_len) { 109 | // The ABI only lets us set a custom status code if we're treating the result as an error 110 | if (status_code >= 200 && status_code < 300) { 111 | return_result(body, body_len, ident); 112 | } else { 113 | return_error(status_code, body, body_len, ident); 114 | } 115 | } 116 | 117 | MonoClass* mono_get_byte_class(void); 118 | MonoDomain* mono_get_root_domain(void); 119 | 120 | MonoArray* mono_wasm_typed_array_new(char* arr, int length) { 121 | MonoClass* typeClass = mono_get_byte_class(); 122 | MonoArray* buffer = mono_array_new(mono_get_root_domain(), typeClass, length); 123 | memcpy(mono_array_addr_with_size(buffer, sizeof(char), 0), arr, length); 124 | return buffer; 125 | } 126 | 127 | MonoArray* mono_cache_get(int ident, MonoString* key) { 128 | char* key_utf8 = mono_wasm_string_get_utf8(key); 129 | int ffi_res_len = cache_get(key_utf8, strlen(key_utf8), ident); 130 | if (ffi_res_len < 0) { 131 | // TODO: If it's less than -1, it's the size needed for a string whose value is an error message 132 | // Get this and return it to .NET - see https://github.com/suborbital/reactr/blob/6ab5699f12d947b769368bf8d7f90eb7b7acd950/api/assemblyscript/assembly/ffi.ts#L32 133 | return 0; 134 | } 135 | 136 | char* ffi_res_buf = (char*)malloc(ffi_res_len); 137 | assert(!get_ffi_result(ffi_res_buf, ident)); 138 | MonoArray* dotnet_byte_array = mono_wasm_typed_array_new(ffi_res_buf, ffi_res_len); 139 | free(ffi_res_buf); 140 | 141 | return dotnet_byte_array; 142 | } 143 | 144 | int mono_cache_set(int ident, MonoString* key, char* value, int value_len, int ttl) { 145 | char* key_utf8 = mono_wasm_string_get_utf8(key); 146 | int res = cache_set(key_utf8, strlen(key_utf8), value, value_len, ttl, ident); 147 | free(key_utf8); 148 | return res; 149 | } 150 | 151 | void atmo_attach_internal_calls() { 152 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Interop::LogMessage", atmo_log_message); 153 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Interop::LogMessageRaw", atmo_log_message_raw); 154 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Interop::RunHttpServer", run_http_server); 155 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Interop::RequestGetField", request_get_field_mono); 156 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Interop::ResponseAddHeader", response_add_header_mono); 157 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Interop::ResponseComplete", response_complete); 158 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Services.Cache::Get", mono_cache_get); 159 | mono_add_internal_call("Wasi.AspNetCore.Server.Atmo.Services.Cache::Set", mono_cache_set); 160 | mono_add_internal_call("System.Threading.TimerQueue::SetTimeout", fake_settimeout); 161 | } 162 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/CustomHostWebApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting.Server; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Wasi.AspNetCore.Server.CustomHost; 10 | 11 | public static class CustomHostWebApplicationBuilderExtensions 12 | { 13 | public static WebApplicationBuilder UseWasiCustomHostServer(this WebApplicationBuilder builder) 14 | { 15 | builder.Services.AddSingleton(); 16 | builder.Logging.AddProvider(new WasiLoggingProvider()).AddFilter("Microsoft.AspNetCore.DataProtection", LogLevel.Error); 17 | return builder; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/Interop.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Wasi.AspNetCore.Server.CustomHost; 7 | 8 | internal class Interop 9 | { 10 | [MethodImpl(MethodImplOptions.InternalCall)] 11 | public static extern void RunHttpServer(Interop owner, int port); 12 | 13 | [MethodImpl(MethodImplOptions.InternalCall)] 14 | public static unsafe extern void ResponseAddHeader(uint requestId, string name, string value); 15 | 16 | [MethodImpl(MethodImplOptions.InternalCall)] 17 | public static unsafe extern void ResponseSendChunk(uint requestId, byte* buffer, int buffer_length); 18 | 19 | [MethodImpl(MethodImplOptions.InternalCall)] 20 | public static unsafe extern void ResponseComplete(uint requestId, int statusCode); 21 | 22 | public event EventHandler<(uint RequestId, string Method, string Url, string HeadersCombined, byte[]? Body)>? OnIncomingRequest; 23 | 24 | // TODO: Make sure this doesn't get trimmed if AOT compiled 25 | // The requestId is a uint instead of a long because otherwise the runtime fails with an error like "CANNOT HANDLE INTERP ICALL SIG VLI" 26 | // For a more complete implementation you might want to use a GUID string instead. 27 | private unsafe void HandleIncomingRequest(uint requestId, string method, string url, string headersCombined, byte[]? body) 28 | => OnIncomingRequest?.Invoke(this, (requestId, method, url, headersCombined, body)); 29 | } 30 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/Wasi.AspNetCore.Server.CustomHost.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <_PackageFiles Include="build\**" BuildAction="Content" PackagePath="build" /> 21 | <_PackageFiles Include="native\**" BuildAction="Content" PackagePath="native" /> 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/WasiCustomHostRequestContext.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Http.Features; 6 | 7 | namespace Wasi.AspNetCore.Server.CustomHost; 8 | 9 | internal class WasiCustomHostRequestContext : WasiServerRequestContext 10 | { 11 | public uint RequestId { get; } 12 | 13 | public WasiCustomHostRequestContext(uint requestId, string httpMethod, string url, IHeaderDictionary headers, Stream requestBody) 14 | : base(httpMethod, url, headers, requestBody) 15 | { 16 | RequestId = requestId; 17 | } 18 | 19 | protected override Task TransmitResponseAsync() 20 | { 21 | var response = (IHttpResponseFeature)this; 22 | 23 | foreach (var h in response.Headers) 24 | { 25 | Interop.ResponseAddHeader(RequestId, h.Key, h.Value.ToString()); 26 | } 27 | 28 | TransmitResponseBody(); 29 | 30 | Interop.ResponseComplete(RequestId, response.StatusCode); 31 | return Task.CompletedTask; 32 | } 33 | 34 | private unsafe void TransmitResponseBody() 35 | { 36 | // TODO: Support non-buffered responses 37 | Span chunk = stackalloc byte[4096]; 38 | var responseBody = (IHttpResponseBodyFeature)this; 39 | fixed (byte* rawResponseBytesPtr = chunk) 40 | { 41 | responseBody.Stream.Position = 0; 42 | while (true) 43 | { 44 | var bytesRead = responseBody.Stream.Read(chunk); 45 | if (bytesRead == 0) 46 | { 47 | break; 48 | } 49 | else 50 | { 51 | Interop.ResponseSendChunk(RequestId, rawResponseBytesPtr, bytesRead); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/WasiCustomHostServer.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Hosting.Server; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace Wasi.AspNetCore.Server.CustomHost; 9 | 10 | internal class WasiCustomHostServer : WasiServer 11 | { 12 | public WasiCustomHostServer(IHostApplicationLifetime lifetime) : base(lifetime) 13 | { 14 | } 15 | 16 | protected override void Run(IHttpApplication application, int port) 17 | { 18 | var hostApiServerInterop = new Interop(); 19 | hostApiServerInterop.OnIncomingRequest += (sender, requestArgs) => 20 | { 21 | var requestContext = new WasiCustomHostRequestContext( 22 | requestArgs.RequestId, 23 | requestArgs.Method, 24 | requestArgs.Url, 25 | ParseHeaders(requestArgs.HeadersCombined), 26 | requestArgs.Body is null ? new MemoryStream() : new MemoryStream(requestArgs.Body)); 27 | 28 | // This isn't meant to throw as it handles its own exceptions and sends errors to the response 29 | _ = HandleRequestAsync(application, requestContext); 30 | }; 31 | 32 | // The underlying native implementation blocks here. If the listening loop was implemented outside the WASM runtime 33 | // then we wouldn't block and would just react to subsequent calls into WASM to handle incoming requests. 34 | Interop.RunHttpServer(hostApiServerInterop, port); 35 | } 36 | 37 | private static IHeaderDictionary ParseHeaders(string headersCombined) 38 | { 39 | // It's not great that we're parsing, stringifying, and then reparsing the HTTP headers 40 | // More intricate interop could avoid this 41 | var result = new HeaderDictionary(); 42 | foreach (var headerLine in headersCombined.Split('\n')) 43 | { 44 | var colonPos = headerLine.IndexOf(':'); 45 | if (colonPos > 0) 46 | { 47 | result.Add(headerLine.Substring(0, colonPos), headerLine.Substring(colonPos + 1)); 48 | } 49 | } 50 | 51 | return result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/build/Wasi.AspNetCore.Server.CustomHost.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/build/Wasi.AspNetCore.Server.CustomHost.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.CustomHost/native/server_interop.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | MonoMethod* method_HandleIncomingRequest; 6 | 7 | __attribute__((import_name("start_http_server"))) 8 | void start_http_server (MonoObject* dotnet_http_server, int port); 9 | 10 | __attribute__((import_name("response_send_chunk"))) 11 | void response_send_chunk (int request_id, void* buffer, int buffer_len); 12 | 13 | __attribute__((import_name("response_add_header"))) 14 | void response_add_header (int request_id, char* name, int name_len, char* value, int value_len); 15 | 16 | __attribute__((import_name("response_complete"))) 17 | void response_complete (int request_id, int status_code); 18 | 19 | void run_http_server (MonoObject* dotnet_http_server, int port) { 20 | start_http_server (dotnet_http_server, port); 21 | } 22 | 23 | MonoClass* mono_get_byte_class(void); 24 | MonoDomain* mono_get_root_domain(void); 25 | 26 | MonoArray* mono_wasm_typed_array_new(void* arr, int length) { 27 | MonoClass* typeClass = mono_get_byte_class(); 28 | MonoArray* buffer = mono_array_new(mono_get_root_domain(), typeClass, length); 29 | memcpy(mono_array_addr_with_size(buffer, 1, 0), arr, length); 30 | return buffer; 31 | } 32 | 33 | __attribute__((export_name("on_incoming_request"))) 34 | void on_incoming_request(MonoObject* dotnet_polling_server, int request_id, char* method, char* url, char* headers_combined, void* body_ptr, int body_len) { 35 | if (!method_HandleIncomingRequest) { 36 | method_HandleIncomingRequest = lookup_dotnet_method("Wasi.AspNetCore.Server.CustomHost.dll", "Wasi.AspNetCore.Server.CustomHost", "Interop", "HandleIncomingRequest", -1); 37 | assert(method_HandleIncomingRequest); 38 | } 39 | 40 | MonoArray* body_dotnet_array = body_ptr 41 | ? mono_wasm_typed_array_new(body_ptr, body_len) 42 | : NULL; 43 | 44 | void* method_params[] = { &request_id, mono_wasm_string_from_js(method), mono_wasm_string_from_js(url), mono_wasm_string_from_js(headers_combined), body_dotnet_array }; 45 | MonoObject *exception; 46 | mono_wasm_invoke_method (method_HandleIncomingRequest, dotnet_polling_server, method_params, &exception); 47 | assert (!exception); 48 | free(method); 49 | free(url); 50 | free(headers_combined); 51 | } 52 | 53 | void response_add_header_mono(int request_id, MonoString* name, MonoString* value) { 54 | char* name_utf8 = mono_wasm_string_get_utf8(name); 55 | char* value_utf8 = mono_wasm_string_get_utf8(value); 56 | response_add_header(request_id, name_utf8, strlen(name_utf8), value_utf8, strlen(value_utf8)); 57 | } 58 | 59 | void fake_settimeout(int timeout) { 60 | // Skipping 61 | } 62 | 63 | void http_server_attach_internal_calls() { 64 | mono_add_internal_call ("Wasi.AspNetCore.Server.CustomHost.Interop::RunHttpServer", run_http_server); 65 | mono_add_internal_call ("Wasi.AspNetCore.Server.CustomHost.Interop::ResponseSendChunk", response_send_chunk); 66 | mono_add_internal_call ("Wasi.AspNetCore.Server.CustomHost.Interop::ResponseAddHeader", response_add_header_mono); 67 | mono_add_internal_call ("Wasi.AspNetCore.Server.CustomHost.Interop::ResponseComplete", response_complete); 68 | 69 | mono_add_internal_call ("System.Threading.TimerQueue::SetTimeout", fake_settimeout); 70 | } 71 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/DuplexPipe.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.IO.Pipelines; 5 | 6 | namespace Wasi.AspNetCore.Server.Native; 7 | 8 | internal class DuplexPipe : IDuplexPipe 9 | { 10 | public DuplexPipe(PipeReader reader, PipeWriter writer) 11 | { 12 | Input = reader; 13 | Output = writer; 14 | } 15 | 16 | public PipeReader Input { get; } 17 | 18 | public PipeWriter Output { get; } 19 | 20 | public static (DuplexPipe ApplicationToTransport, DuplexPipe TransportToApplication) CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) 21 | { 22 | var input = new Pipe(inputOptions); // Input from the server's perspective 23 | var output = new Pipe(outputOptions); // Output from the server's perspective 24 | 25 | var transportToApplication = new DuplexPipe(output.Reader, input.Writer); // API for writing server input and reading server output 26 | var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); // API for reading server input and writing server output 27 | 28 | return (applicationToTransport, transportToApplication); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/Interop.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Collections.Concurrent; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace Wasi.AspNetCore.Server.Native; 8 | 9 | internal class Interop 10 | { 11 | [MethodImpl(MethodImplOptions.InternalCall)] 12 | private static extern void RunTcpListenerLoop(Interop self); 13 | 14 | [MethodImpl(MethodImplOptions.InternalCall)] 15 | public static extern unsafe void SendResponseData(uint fileDescriptor, byte* buf, int buf_len); 16 | 17 | private Action? _onConnectionHandler; 18 | private ConcurrentDictionary _liveConnections = new(); 19 | 20 | // TODO: Make sure this doesn't get trimmed if AOT compiled 21 | private void NotifyOpenedConnection(uint fileDescriptor) 22 | { 23 | var connectionInfo = new WasiConnectionContext(fileDescriptor); 24 | _liveConnections[connectionInfo.FileDescriptor] = connectionInfo; 25 | _onConnectionHandler!(connectionInfo); 26 | } 27 | 28 | private void NotifyClosedConnection(uint fileDescriptor) 29 | { 30 | if (_liveConnections.TryRemove(fileDescriptor, out var closedConnection)) 31 | { 32 | _ = closedConnection.NotifyClosedByClientAsync(); 33 | } 34 | } 35 | 36 | private unsafe void NotifyDataReceived(uint fileDescriptor, byte* buf, int buf_len) 37 | { 38 | if (_liveConnections.TryGetValue(fileDescriptor, out var connection)) 39 | { 40 | var data = new ReadOnlySpan(buf, buf_len); 41 | connection.ReceiveDataFromClient(data); 42 | } 43 | } 44 | 45 | public void RunTcpListenerLoop(Action onConnection) 46 | { 47 | _onConnectionHandler = onConnection; 48 | RunTcpListenerLoop(this); 49 | } 50 | 51 | public void StopTcpListenerLoop() 52 | { 53 | throw new NotImplementedException("TODO: Somehow signal the shutdown to the C loop."); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/Wasi.AspNetCore.Server.Native.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <_PackageFiles Include="build\**" BuildAction="Content" PackagePath="build" /> 26 | <_PackageFiles Include="native\**" BuildAction="Content" PackagePath="native" /> 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/WasiConnectionContext.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Connections; 5 | using Microsoft.AspNetCore.Http.Features; 6 | using System.Buffers; 7 | using System.IO.Pipelines; 8 | 9 | namespace Wasi.AspNetCore.Server.Native; 10 | 11 | internal class WasiConnectionContext : ConnectionContext 12 | { 13 | // TODO: Consider configuring some equivalent to the PinnedBlockMemoryPool 14 | private static readonly PipeOptions TransportPipeOptions = new PipeOptions(readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); 15 | 16 | private readonly IDuplexPipe _applicationToTransport; 17 | private readonly IDuplexPipe _transportToApplication; 18 | private readonly string _connectionId; 19 | private readonly CancellationTokenSource _connectionClosedCts = new(); 20 | 21 | public WasiConnectionContext(uint fileDescriptor) 22 | { 23 | FileDescriptor = fileDescriptor; 24 | _connectionId = $"Connection_{fileDescriptor}"; 25 | (_applicationToTransport, _transportToApplication) = DuplexPipe.CreateConnectionPair(TransportPipeOptions, TransportPipeOptions); 26 | 27 | // This shouldn't throw, or at least it needs to do its own error handling 28 | _ = TransmitOutputToClientAsync(_connectionClosedCts.Token); 29 | } 30 | 31 | public uint FileDescriptor { get; } 32 | public override IDuplexPipe Transport { get => _applicationToTransport; set => throw new NotImplementedException(); } 33 | public override string ConnectionId { get => _connectionId; set => throw new NotImplementedException(); } 34 | public override IFeatureCollection Features { get; } = new FeatureCollection(); 35 | public override IDictionary Items { get; set; } = new Dictionary(); 36 | 37 | public void ReceiveDataFromClient(ReadOnlySpan data) 38 | { 39 | _transportToApplication.Output.WriteAsync(data.ToArray()); // TODO: Not this 40 | } 41 | 42 | public async Task NotifyClosedByClientAsync() 43 | { 44 | // TODO: Is this enough to make ASP.NET Core know the client is gone? 45 | await _transportToApplication.Output.FlushAsync(); 46 | await _transportToApplication.Output.CompleteAsync(); 47 | _connectionClosedCts.Cancel(); 48 | } 49 | 50 | private async Task TransmitOutputToClientAsync(CancellationToken cancellationToken) 51 | { 52 | try 53 | { 54 | while (true) 55 | { 56 | var readResult = await _transportToApplication.Input.ReadAsync(cancellationToken); 57 | if (readResult.Buffer.Length > 0) 58 | { 59 | SendBufferAsResponse(readResult.Buffer); 60 | _transportToApplication.Input.AdvanceTo(readResult.Buffer.End); 61 | } 62 | 63 | if (readResult.IsCompleted) 64 | { 65 | break; 66 | } 67 | } 68 | } 69 | catch (OperationCanceledException) 70 | { 71 | // This is fine - it's ReadAsync being cancelled due to the connection being closed 72 | } 73 | catch (Exception ex) 74 | { 75 | Console.WriteLine(ex); 76 | } 77 | } 78 | 79 | private unsafe void SendBufferAsResponse(ReadOnlySequence buffer) 80 | { 81 | foreach (var chunk in buffer) 82 | { 83 | fixed (byte* bufPtr = chunk.Span) 84 | { 85 | Interop.SendResponseData(FileDescriptor, bufPtr, chunk.Length); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/WasiConnectionListener.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Connections; 5 | using System.Net; 6 | using System.Threading.Channels; 7 | 8 | namespace Wasi.AspNetCore.Server.Native; 9 | 10 | internal class WasiConnectionListener : IConnectionListener 11 | { 12 | private Channel _arrivingConnections = Channel.CreateUnbounded(); 13 | 14 | public WasiConnectionListener(EndPoint endpoint) 15 | { 16 | EndPoint = endpoint; 17 | } 18 | 19 | public EndPoint EndPoint { get; } 20 | 21 | public ValueTask AcceptAsync(CancellationToken cancellationToken = default) 22 | { 23 | return _arrivingConnections.Reader.ReadAsync(cancellationToken); 24 | } 25 | 26 | public void ReceiveConnection(ConnectionContext connectionContext) 27 | { 28 | if (!_arrivingConnections.Writer.TryWrite(connectionContext)) 29 | { 30 | throw new InvalidOperationException("Unable to add the incoming connection to the channel"); 31 | } 32 | } 33 | 34 | public ValueTask DisposeAsync() 35 | => ValueTask.CompletedTask; 36 | 37 | public ValueTask UnbindAsync(CancellationToken cancellationToken = default) 38 | => ValueTask.CompletedTask; // Not applicable - preopened listeners can't be closed within the app's lifetime 39 | } 40 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/WasiConnectionListenerFactory.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Net; 5 | using Microsoft.AspNetCore.Connections; 6 | 7 | namespace Wasi.AspNetCore.Server.Native; 8 | 9 | internal class WasiConnectionListenerFactory : IConnectionListenerFactory 10 | { 11 | private bool _hasClaimedPreopenedListener; 12 | 13 | public WasiConnectionListener? BoundConnectionListener { get; private set; } 14 | 15 | public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) 16 | { 17 | if (!_hasClaimedPreopenedListener) 18 | { 19 | // We'll use the first WASI preopened listener and arbitrarily claim it matches the first requested endpoint. 20 | // There's no way to know if it actually matches because WASI doesn't provide any address info. 21 | // If we wanted, we could attach to all the preopened listeners, but it would be very unusual to have more than one. 22 | _hasClaimedPreopenedListener = true; 23 | BoundConnectionListener = new WasiConnectionListener(endpoint); 24 | return ValueTask.FromResult(BoundConnectionListener); 25 | } 26 | else 27 | { 28 | // For subsequent endpoints, act as if we're listening but no traffic arrives 29 | return ValueTask.FromResult(new NullConnectionListener(endpoint)); 30 | } 31 | } 32 | 33 | class NullConnectionListener : IConnectionListener 34 | { 35 | public NullConnectionListener(EndPoint endpoint) 36 | { 37 | EndPoint = endpoint; 38 | } 39 | 40 | public EndPoint EndPoint { get; } 41 | 42 | public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) 43 | { 44 | // Never completes 45 | return await new TaskCompletionSource(cancellationToken).Task; 46 | } 47 | 48 | public ValueTask DisposeAsync() => ValueTask.CompletedTask; 49 | 50 | public ValueTask UnbindAsync(CancellationToken cancellationToken = default) => ValueTask.CompletedTask; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/WasiLoggingProvider.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Wasi.AspNetCore.Server.Native; 7 | 8 | internal class WasiLoggingProvider : ILoggerProvider 9 | { 10 | public ILogger CreateLogger(string categoryName) 11 | { 12 | return new MinimalLogger(categoryName); 13 | } 14 | 15 | public void Dispose() 16 | { 17 | } 18 | 19 | private class MinimalLogger : ILogger 20 | { 21 | private string categoryName; 22 | 23 | public MinimalLogger(string categoryName) 24 | { 25 | this.categoryName = categoryName; 26 | } 27 | 28 | public IDisposable? BeginScope(TState state) where TState : notnull 29 | => new MyScope(); 30 | 31 | public bool IsEnabled(LogLevel logLevel) 32 | { 33 | return logLevel > LogLevel.Debug; 34 | } 35 | 36 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) 37 | { 38 | Console.WriteLine($"{ShortName(logLevel)}: {categoryName}"); 39 | Console.Write(" "); 40 | Console.WriteLine(formatter(state, exception)); 41 | 42 | if (exception is not null) 43 | { 44 | Console.Error.WriteLine(exception); 45 | Console.Error.WriteLine(); 46 | } 47 | } 48 | 49 | private static string ShortName(LogLevel logLevel) => logLevel switch 50 | { 51 | LogLevel.Trace => "trce", 52 | LogLevel.Debug => "dbug", 53 | LogLevel.Information => "info", 54 | LogLevel.Warning => "warn", 55 | LogLevel.Error => "erro", 56 | LogLevel.Critical => "crit", 57 | LogLevel l => l.ToString(), 58 | }; 59 | 60 | private class MyScope : IDisposable 61 | { 62 | public void Dispose() 63 | { 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/WasiNativeServer.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Connections; 5 | using Microsoft.AspNetCore.Hosting.Server; 6 | using Microsoft.AspNetCore.Http.Features; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using System.Reflection; 10 | 11 | namespace Wasi.AspNetCore.Server.Native; 12 | 13 | internal class WasiNativeServer : IServer 14 | { 15 | private readonly IServer _underlyingServer; 16 | private readonly IHostApplicationLifetime _lifetime; 17 | private readonly IServiceProvider _services; 18 | private readonly Interop _interop = new(); 19 | 20 | public WasiNativeServer(IServiceProvider services, IServer underlyingServer) 21 | { 22 | _services = services; 23 | _underlyingServer = underlyingServer; 24 | _lifetime = services.GetRequiredService(); 25 | 26 | // Disable the heartbeat because it tries to run on a background thread 27 | // Instead we'll manually call the heartbeat at the start of each request (which has drawbacks, but is necessary for now) 28 | var serviceContext = underlyingServer.GetType().GetProperty("ServiceContext", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(underlyingServer)!; 29 | var heartbeatProperty = serviceContext.GetType().GetProperty("Heartbeat")!; 30 | var heartbeat = heartbeatProperty.GetValue(serviceContext)!; 31 | var onHeartbeatMethod = heartbeat.GetType().GetMethod("OnHeartbeat", BindingFlags.Instance | BindingFlags.NonPublic)!; 32 | heartbeatProperty.SetValue(serviceContext, null); 33 | onHeartbeatMethod.Invoke(heartbeat, null); 34 | } 35 | 36 | public IFeatureCollection Features => _underlyingServer.Features; 37 | 38 | public void Dispose() => _underlyingServer.Dispose(); 39 | 40 | public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull 41 | { 42 | var result = _underlyingServer.StartAsync(application, cancellationToken); 43 | _lifetime.ApplicationStarted.Register(() => Run(application)); 44 | return result; 45 | } 46 | 47 | private void Run(IHttpApplication application) where TContext : notnull 48 | { 49 | if (_services.GetRequiredService() is not WasiConnectionListenerFactory factory) 50 | { 51 | throw new InvalidOperationException($"{typeof(WasiNativeServer)} requires the {typeof(IConnectionListenerFactory)} to be an instance of {typeof(WasiConnectionListenerFactory)}"); 52 | } 53 | 54 | if (factory.BoundConnectionListener is not { } listener) 55 | { 56 | throw new InvalidOperationException("The WASI connection listener factory was not bound to any endpoint."); 57 | } 58 | 59 | _interop.RunTcpListenerLoop(onConnection: listener.ReceiveConnection); 60 | } 61 | 62 | public Task StopAsync(CancellationToken cancellationToken) 63 | { 64 | _interop.StopTcpListenerLoop(); 65 | return _underlyingServer.StopAsync(cancellationToken); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/WasiNativeWebApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Connections; 5 | using Microsoft.AspNetCore.Hosting.Server; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using Wasi.AspNetCore.Server.Native; 9 | 10 | namespace Microsoft.AspNetCore.Builder; 11 | 12 | public static class WasiNativeWebApplicationBuilderExtensions 13 | { 14 | public static WebApplicationBuilder UseWasiConnectionListener(this WebApplicationBuilder builder) 15 | { 16 | // We want the IServer to be the usual KestrelServerImpl, but we also want to replace its 17 | // StartAsync with our own version that calls the native blocking listener loop. So, get 18 | // the usual instance and pass it through to the WASI-specific wrapper. 19 | var underlyingServerType = FindUnderlyingServerType(builder); 20 | builder.Services.AddSingleton(underlyingServerType); 21 | builder.Services.AddSingleton(serviceProvider => 22 | { 23 | var underlying = serviceProvider.GetRequiredService(underlyingServerType); 24 | return new WasiNativeServer(serviceProvider, (IServer)underlying); 25 | }); 26 | 27 | builder.Services.AddSingleton(); 28 | builder.Logging.AddProvider(new WasiLoggingProvider()).AddFilter("Microsoft.AspNetCore.DataProtection", LogLevel.Error); 29 | 30 | return builder; 31 | } 32 | 33 | private static Type FindUnderlyingServerType(WebApplicationBuilder builder) 34 | { 35 | foreach (var s in builder.Services) 36 | { 37 | if (s.ServiceType == typeof(IServer) && s.ImplementationType is Type type) 38 | { 39 | return type; 40 | } 41 | } 42 | 43 | throw new InvalidOperationException("The service collection doesn't contain an existing IServer implementation type."); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/build/Wasi.AspNetCore.Server.Native.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/build/Wasi.AspNetCore.Server.Native.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/native/dotnet_method.h: -------------------------------------------------------------------------------- 1 | #define DEFINE_DOTNET_METHOD(c_name, assembly_name, namespc, class_name, method_name) \ 2 | MonoMethod* method_##c_name;\ 3 | MonoObject* c_name(MonoObject* target_instance, void* method_params[]) {\ 4 | if (!method_##c_name) {\ 5 | method_##c_name = lookup_dotnet_method(assembly_name, namespc, class_name, method_name, -1);\ 6 | assert(method_##c_name);\ 7 | }\ 8 | \ 9 | MonoObject* exception;\ 10 | MonoObject* res = mono_wasm_invoke_method(method_##c_name, target_instance, method_params, &exception);\ 11 | assert(!exception);\ 12 | return res;\ 13 | } 14 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/native/interop.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "dotnet_method.h" 4 | 5 | void tcp_listener_attach_internal_calls(); 6 | 7 | void noop_settimeout(int timeout) { 8 | // Not implemented 9 | } 10 | 11 | DEFINE_DOTNET_METHOD(invoke_threadpool_callback, "System.Private.CoreLib.dll", "System.Threading", "ThreadPool", "Callback"); 12 | void wasi_queuecallback() { 13 | invoke_threadpool_callback(NULL, NULL); 14 | } 15 | 16 | void native_networking_attach_internal_calls() { 17 | mono_add_internal_call("System.Threading.TimerQueue::SetTimeout", noop_settimeout); 18 | mono_add_internal_call("System.Threading.ThreadPool::QueueCallback", wasi_queuecallback); 19 | tcp_listener_attach_internal_calls(); 20 | } 21 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server.Native/native/tcp_listener_loop.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "dotnet_method.h" 11 | 12 | /* 13 | WARNING: Both 'read' and 'write' appear to be buggy in Wasmtime when operating on preopened sockets on Windows. 14 | If the client has disconnected ungracefully, then: 15 | - On Linux, read returns 0 (not sure about write - didn't check) 16 | - On Windows, read/write doesn't return at all and instead fatally kills the whole wasm runtime, logging a message similar to: 17 | Error: failed to run main module `yourmodule.wasm` 18 | Caused by ... An existing connection was forcibly closed by the remote host. (os error 10054) 19 | To repro, just create an endpoint with Thread.Sleep(5000), use curl to call it, and while it's sleeping use ctrl+c to kill the client. 20 | Or to repro without .NET, use C to make a simple "read"/"write" loop with usleep() calls in the loop, and ctrl+c kill a curl request. 21 | My guess is that Wasmtime needs to know about WSAECONNRESET/WSAENETRESET/WSAECONNABORTED and that it should surface these as error 22 | codes to the wasm code instead of aborting. For prototype purposes, I can ignore this since clients normally do close gracefully. 23 | But it would be a vulnerability if done for real. 24 | */ 25 | 26 | DEFINE_DOTNET_METHOD(notify_opened_connection, "Wasi.AspNetCore.Server.Native.dll", "Wasi.AspNetCore.Server.Native", "Interop", "NotifyOpenedConnection"); 27 | DEFINE_DOTNET_METHOD(notify_closed_connection, "Wasi.AspNetCore.Server.Native.dll", "Wasi.AspNetCore.Server.Native", "Interop", "NotifyClosedConnection"); 28 | DEFINE_DOTNET_METHOD(notify_data_received, "Wasi.AspNetCore.Server.Native.dll", "Wasi.AspNetCore.Server.Native", "Interop", "NotifyDataReceived"); 29 | 30 | // Hold a linked list of active connections for the busy-polling 31 | typedef struct Connection { 32 | int fd; 33 | struct Connection* next; 34 | } Connection; 35 | Connection* first_connection; 36 | 37 | void accept_any_new_connection(int interop_gchandle) { 38 | // It's a bit odd, but WASI preopened listeners have file handles sequentially starting from 3. If the host preopened more than 39 | // one, you could sock_accept with fd=3, then fd=4, etc., until you run out of preopens. 40 | int preopen_fd = getenv("DEBUGGER_FD") ? 4 : 3; 41 | 42 | // libc's accept4 is mapped to WASI's sock_accept with some additional parameter/return mapping at https://github.com/WebAssembly/wasi-libc/blob/63e4489d01ad0262d995c6d9a5f1a1bab719c917/libc-bottom-half/sources/accept.c#L10 43 | struct sockaddr addr_out_ignored; 44 | socklen_t addr_len_out_ignored; 45 | int new_connection_fd = accept4(preopen_fd, &addr_out_ignored, &addr_len_out_ignored, SOCK_NONBLOCK); 46 | if (new_connection_fd > 0) { 47 | Connection* new_connection = (Connection*)malloc(sizeof(Connection)); 48 | new_connection->fd = new_connection_fd; 49 | new_connection->next = first_connection; 50 | first_connection = new_connection; 51 | 52 | notify_opened_connection(mono_gchandle_get_target(interop_gchandle), (void*[]){ &new_connection_fd }); 53 | } else if (errno != __WASI_ERRNO_AGAIN) { // __WASI_ERRNO_AGAIN means "would block so try again later" 54 | printf("Fatal: TCP accept failed with errno %i. This may mean the host isn't listening for connections. Be sure to pass the --tcplisten parameter.\n", errno); 55 | exit(1); 56 | } 57 | } 58 | 59 | void poll_connections(int interop_gchandle, void* read_buffer, int read_buffer_len) { 60 | Connection* prev_connection = NULL; 61 | Connection* connection = first_connection; 62 | while (connection) { 63 | Connection* next_connection = connection->next; 64 | 65 | int bytes_read = read(connection->fd, read_buffer, read_buffer_len); 66 | int has_received_data = bytes_read > 0; 67 | 68 | if (has_received_data || (bytes_read < 0 && errno == EWOULDBLOCK)) { 69 | if (has_received_data) { 70 | notify_data_received(mono_gchandle_get_target(interop_gchandle), (void* []) { &connection->fd, read_buffer, &bytes_read }); 71 | } 72 | 73 | prev_connection = connection; 74 | } 75 | else { 76 | // In this branch, we're definitely closing the connection. First, figure out whether this is an error or not. 77 | if (bytes_read == 0) { 78 | // Client initiating graceful close 79 | //printf("Connection %i closed by client\n", connection->fd); 80 | } else { 81 | // Unexpected error 82 | printf("Connection %i failed with error %i; closing connection.\n", connection->fd, errno); 83 | } 84 | 85 | if (prev_connection) { 86 | prev_connection->next = next_connection; 87 | } else { 88 | first_connection = next_connection; 89 | } 90 | 91 | close(connection->fd); 92 | notify_closed_connection(mono_gchandle_get_target(interop_gchandle), (void* []) { &connection->fd }); 93 | free(connection); 94 | } 95 | 96 | connection = next_connection; 97 | } 98 | } 99 | 100 | void close_all_connections() { 101 | Connection* connection; 102 | while ((connection = first_connection)) { 103 | first_connection = connection->next; 104 | close(connection->fd); 105 | free(connection); 106 | } 107 | } 108 | 109 | void run_polling_listener(int interop_gchandle, int* cancellation_flag) { 110 | int read_buffer_len = 1024*1024; 111 | void* read_buffer = malloc(read_buffer_len); 112 | 113 | // TODO: Stop doing busy-polling. This is the only cross-platform supported option at the moment, but 114 | // on Linux there's a notification mechanism (https://github.com/bytecodealliance/wasmtime/issues/3730). 115 | // Once Wasmtime (etc) implement cross-platform support for notification, this code should use it. 116 | while (*cancellation_flag == 0) { 117 | usleep(10000); 118 | accept_any_new_connection(interop_gchandle); 119 | poll_connections(interop_gchandle, read_buffer, read_buffer_len); 120 | } 121 | 122 | close_all_connections(); 123 | free(read_buffer); 124 | } 125 | 126 | void run_tcp_listener_loop(MonoObject* interop) { 127 | int interop_gchandle = mono_gchandle_new(interop, /* pinned */ 1); 128 | 129 | // TODO: Find some way to let .NET set this cancellation flag 130 | int cancel = 0; 131 | run_polling_listener(interop_gchandle, &cancel); 132 | 133 | mono_gchandle_free(interop_gchandle); 134 | } 135 | 136 | void send_response_data(int fd, char* buf, int buf_len) { 137 | while (1) { 138 | int res = write(fd, buf, buf_len); 139 | 140 | if (res == -1) { 141 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 142 | // Clearly this is not smart, as a single bad client could block us indefinitely. 143 | // We should instead just return back to .NET code telling it the write 144 | // was incomplete, then it should do an async yield before trying to resend 145 | // the rest 146 | continue; 147 | } else { 148 | // TODO: Proper error reporting back into .NET 149 | printf ("Error sending response data. errno: %i\n", errno); 150 | break; 151 | } 152 | } else if (res < buf_len) { 153 | // It's a partial write, so keep going 154 | buf += res; 155 | buf_len -= res; 156 | } else { 157 | break; 158 | } 159 | } 160 | } 161 | 162 | void tcp_listener_attach_internal_calls() { 163 | mono_add_internal_call("Wasi.AspNetCore.Server.Native.Interop::RunTcpListenerLoop", run_tcp_listener_loop); 164 | mono_add_internal_call("Wasi.AspNetCore.Server.Native.Interop::SendResponseData", send_response_data); 165 | } 166 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server/Wasi.AspNetCore.Server.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server/WasiLoggingProvider.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Wasi.AspNetCore.Server; 7 | 8 | public class WasiLoggingProvider : ILoggerProvider 9 | { 10 | public ILogger CreateLogger(string categoryName) 11 | { 12 | return new MinimalLogger(categoryName); 13 | } 14 | 15 | public void Dispose() 16 | { 17 | } 18 | 19 | private class MinimalLogger : ILogger 20 | { 21 | private string categoryName; 22 | 23 | public MinimalLogger(string categoryName) 24 | { 25 | this.categoryName = categoryName; 26 | } 27 | 28 | public IDisposable BeginScope(TState state) 29 | => new MyScope(); 30 | 31 | public bool IsEnabled(LogLevel logLevel) 32 | { 33 | return logLevel > LogLevel.Debug; 34 | } 35 | 36 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) 37 | { 38 | Console.WriteLine($"{ShortName(logLevel)}: {categoryName}"); 39 | Console.Write(" "); 40 | Console.WriteLine(formatter(state, exception)); 41 | 42 | if (exception is not null) 43 | { 44 | Console.Error.WriteLine(exception); 45 | Console.Error.WriteLine(); 46 | } 47 | } 48 | 49 | private static string ShortName(LogLevel logLevel) => logLevel switch 50 | { 51 | LogLevel.Trace => "trce", 52 | LogLevel.Debug => "dbug", 53 | LogLevel.Information => "info", 54 | LogLevel.Warning => "warn", 55 | LogLevel.Error => "erro", 56 | LogLevel.Critical => "crit", 57 | LogLevel l => l.ToString(), 58 | }; 59 | 60 | private class MyScope : IDisposable 61 | { 62 | public void Dispose() 63 | { 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server/WasiServer.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Hosting.Server; 5 | using Microsoft.AspNetCore.Hosting.Server.Features; 6 | using Microsoft.AspNetCore.Http.Features; 7 | using Microsoft.Extensions.Hosting; 8 | using System.Text; 9 | 10 | namespace Wasi.AspNetCore.Server; 11 | 12 | public abstract class WasiServer : IServer, IServerAddressesFeature 13 | { 14 | private readonly IHostApplicationLifetime _lifetime; 15 | 16 | public IFeatureCollection Features { get; } = new FeatureCollection(); 17 | 18 | ICollection IServerAddressesFeature.Addresses { get; } = new List(); 19 | 20 | bool IServerAddressesFeature.PreferHostingUrls { get; set; } 21 | 22 | public WasiServer(IHostApplicationLifetime lifetime) 23 | { 24 | _lifetime = lifetime; 25 | Features[typeof(IServerAddressesFeature)] = this; 26 | } 27 | 28 | protected abstract void Run(IHttpApplication application, int port) where TContext : notnull; 29 | 30 | public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull 31 | { 32 | // Where possible we'll use the port given via IServerAddressesFeature (e.g., via ASPNETCORE_URLS env var) 33 | // but if not fall back on port 5000. We're only binding to one address though. 34 | var addresses = ((IServerAddressesFeature)this).Addresses; 35 | if (addresses.Count == 0) 36 | { 37 | addresses.Add("http://localhost:5000"); 38 | } 39 | var port = Uri.TryCreate(addresses.First().Replace("*", "host"), default, out var uri) 40 | ? uri.Port : 5000; 41 | 42 | _lifetime.ApplicationStarted.Register(() => Run(application, port)); 43 | 44 | return Task.CompletedTask; 45 | } 46 | 47 | protected Task HandleRequestAsync(IHttpApplication application, WasiServerRequestContext requestContext) where TContext : notnull 48 | { 49 | var resultTask = HandleRequestCoreAsync(application, requestContext); 50 | if (resultTask.IsCompleted) 51 | { 52 | // If possible, process it synchronously, as the host doesn't currently support throwing asynchronously 53 | return CompleteResponseWithErrorHandling(); 54 | } 55 | else 56 | { 57 | var tcs = new TaskCompletionSource(); 58 | resultTask.GetAwaiter().OnCompleted(async () => 59 | { 60 | try 61 | { 62 | await CompleteResponseWithErrorHandling(); 63 | tcs.SetResult(); 64 | } 65 | catch (Exception ex) 66 | { 67 | tcs.TrySetException(ex); 68 | } 69 | }); 70 | return tcs.Task; 71 | } 72 | 73 | Task CompleteResponseWithErrorHandling() 74 | { 75 | if (resultTask.IsFaulted) 76 | { 77 | requestContext.StatusCode = 500; 78 | requestContext.Stream.Write( 79 | Encoding.UTF8.GetBytes($"

Server error

{resultTask.Exception}
")); 80 | } 81 | 82 | return requestContext.CompleteAsync(); 83 | } 84 | } 85 | 86 | private async Task HandleRequestCoreAsync(IHttpApplication application, WasiServerRequestContext requestContext) where TContext: notnull 87 | { 88 | var requestFeatures = new FeatureCollection(); 89 | requestFeatures[typeof(IHttpRequestFeature)] = requestContext; 90 | requestFeatures[typeof(IHttpResponseFeature)] = requestContext; 91 | requestFeatures[typeof(IHttpResponseBodyFeature)] = requestContext; 92 | 93 | var ctx = application.CreateContext(requestFeatures); 94 | try 95 | { 96 | await application.ProcessRequestAsync(ctx); 97 | application.DisposeContext(ctx, null); 98 | } 99 | catch (Exception ex) 100 | { 101 | application.DisposeContext(ctx, ex); 102 | throw; 103 | } 104 | } 105 | 106 | public Task StopAsync(CancellationToken cancellationToken) 107 | => Task.CompletedTask; 108 | 109 | public void Dispose() 110 | { 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Wasi.AspNetCore.Server/WasiServerRequestContext.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Http.Features; 6 | using System.IO.Pipelines; 7 | 8 | namespace Wasi.AspNetCore.Server; 9 | 10 | public abstract class WasiServerRequestContext : IHttpRequestFeature, IHttpResponseFeature, IHttpResponseBodyFeature 11 | { 12 | private List<(Func, object)> _onStartingCallbacks = new(); 13 | private List<(Func, object)> _onCompletedCallbacks = new(); 14 | 15 | public WasiServerRequestContext(string httpMethod, string url, IHeaderDictionary headers, Stream requestBody) 16 | { 17 | var queryStartPos = url.IndexOf('?'); 18 | var path = queryStartPos < 0 ? url : url.Substring(0, queryStartPos); 19 | var query = queryStartPos < 0 ? string.Empty : url.Substring(queryStartPos); 20 | 21 | Method = httpMethod; 22 | Path = path; 23 | QueryString = query; 24 | ((IHttpRequestFeature)this).Headers = headers; 25 | ((IHttpRequestFeature)this).Body = requestBody; 26 | Stream = new MemoryStream(); 27 | Writer = PipeWriter.Create(Stream); 28 | } 29 | 30 | public string Protocol { get; set; } = "HTTP/1.1"; 31 | public string Scheme { get; set; } = "http"; 32 | public string Method { get; set; } = "GET"; 33 | public string PathBase { get; set; } = string.Empty; 34 | public string Path { get; set; } = "/"; 35 | public string QueryString { get; set; } = string.Empty; 36 | public string RawTarget { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 37 | 38 | public int StatusCode { get; set; } = 200; 39 | public string? ReasonPhrase { get; set; } 40 | 41 | public bool HasStarted { get; } 42 | 43 | public Stream Stream { get; } 44 | 45 | public PipeWriter Writer { get; } 46 | IHeaderDictionary IHttpRequestFeature.Headers { get; set; } = new HeaderDictionary(); 47 | IHeaderDictionary IHttpResponseFeature.Headers { get; set; } = new HeaderDictionary(); 48 | 49 | Stream IHttpRequestFeature.Body { get; set; } = default!; 50 | Stream IHttpResponseFeature.Body { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 51 | 52 | protected abstract Task TransmitResponseAsync(); 53 | 54 | public async Task CompleteAsync() 55 | { 56 | await TransmitResponseAsync(); 57 | 58 | foreach (var c in _onCompletedCallbacks) 59 | { 60 | await c.Item1(c.Item2); 61 | } 62 | } 63 | 64 | public void DisableBuffering() 65 | { 66 | throw new NotImplementedException(); 67 | } 68 | 69 | public void OnCompleted(Func callback, object state) 70 | { 71 | _onCompletedCallbacks.Add((callback, state)); 72 | } 73 | 74 | public void OnStarting(Func callback, object state) 75 | { 76 | _onStartingCallbacks.Add((callback, state)); 77 | } 78 | 79 | public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) 80 | { 81 | using var file = File.OpenRead(path); 82 | file.CopyTo(Stream); // TODO: Respect offset/count 83 | return Task.CompletedTask; 84 | } 85 | 86 | public async Task StartAsync(CancellationToken cancellationToken = default) 87 | { 88 | foreach (var c in _onStartingCallbacks) 89 | { 90 | await c.Item1(c.Item2); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/.gitignore: -------------------------------------------------------------------------------- 1 | packs/ 2 | tools/ 3 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/Tasks/EmitWasmBundleObjectFile.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.Build.Framework; 13 | 14 | namespace Wasi.Sdk.Tasks; 15 | 16 | public class EmitWasmBundleObjectFile : Microsoft.Build.Utilities.Task, ICancelableTask 17 | { 18 | private static Encoding Utf8NoBom = new UTF8Encoding(false); 19 | private static byte[] HexToUtf8Lookup; 20 | private static byte[] NewLineAndIndentation = new[] { (byte)0x0a, (byte)0x20, (byte)0x20 }; 21 | private CancellationTokenSource BuildTaskCancelled { get; } = new(); 22 | 23 | // We only want to emit a single copy of the data for a given content hash, but we have to track all the 24 | // different filenames that may be referencing that content 25 | private ICollection> _filesToBundleByObjectFileName = default!; 26 | 27 | [Required] 28 | public ITaskItem[] FilesToBundle { get; set; } = default!; 29 | 30 | [Required] 31 | public string ClangExecutable { get; set; } = default!; 32 | 33 | [Output] 34 | public string? BundleApiSourceCode { get; set; } 35 | 36 | static EmitWasmBundleObjectFile() 37 | { 38 | // Every 6 bytes in this array represents the output for a different input byte value. 39 | // For example, the input byte 0x1a (26 decimal) corresponds to bytes 156-161 (26*6=156), 40 | // whose values will be ['0', 'x', '1', 'a', ',', ' '], which is the UTF-8 representation 41 | // for "0x1a, ". This is just a faster alternative to calling .ToString("x2") on every 42 | // byte of the input file and then pushing that string through UTF8Encoding. 43 | HexToUtf8Lookup = new byte[256 * 6]; 44 | for (var i = 0; i < 256; i++) 45 | { 46 | string byteAsHex = i.ToString("x2"); 47 | char highOrderChar = BitConverter.IsLittleEndian ? byteAsHex[0] : byteAsHex[1]; 48 | char lowOrderChar = BitConverter.IsLittleEndian ? byteAsHex[1] : byteAsHex[0]; 49 | HexToUtf8Lookup[i * 6 + 0] = (byte)'0'; 50 | HexToUtf8Lookup[i * 6 + 1] = (byte)'x'; 51 | HexToUtf8Lookup[i * 6 + 2] = (byte)highOrderChar; 52 | HexToUtf8Lookup[i * 6 + 3] = (byte)lowOrderChar; 53 | HexToUtf8Lookup[i * 6 + 4] = (byte)','; 54 | HexToUtf8Lookup[i * 6 + 5] = (byte)' '; 55 | } 56 | } 57 | 58 | public override bool Execute() 59 | { 60 | // The ObjectFile (output filename) already includes a content hash. Grouping by this filename therefore 61 | // produces one group per file-content. We only want to emit one copy of each file-content, and one symbol for it. 62 | _filesToBundleByObjectFileName = FilesToBundle.GroupBy(f => f.GetMetadata("ObjectFile")).ToList(); 63 | 64 | // We're handling the incrementalism within this task, because it needs to be based on file content hashes 65 | // and not on timetamps. The output filenames contain a content hash, so if any such file already exists on 66 | // disk with that name, we know it must be up-to-date. 67 | var remainingObjectFilesToBundle = _filesToBundleByObjectFileName.Where(g => !File.Exists(g.Key)).ToArray(); 68 | 69 | // If you're only touching the leaf project, we don't really need to tell you that. 70 | // But if there's more work to do it's valuable to show progress. 71 | var verbose = remainingObjectFilesToBundle.Length > 1; 72 | var verboseCount = 0; 73 | 74 | Parallel.For(0, remainingObjectFilesToBundle.Length, new ParallelOptions { MaxDegreeOfParallelism = 4, CancellationToken = BuildTaskCancelled.Token }, i => 75 | { 76 | var objectFile = remainingObjectFilesToBundle[i]; 77 | 78 | // Since the object filenames include a content hash, we can pick an arbitrary ITaskItem from each group, 79 | // since we know each group's ITaskItems all contain the same binary data 80 | var contentSourceFile = objectFile.First(); 81 | 82 | var outputFile = objectFile.Key; 83 | if (verbose) 84 | { 85 | var count = Interlocked.Increment(ref verboseCount); 86 | Log.LogMessage(MessageImportance.High, "{0}/{1} Bundling {2}...", count, remainingObjectFilesToBundle.Length, Path.GetFileName(contentSourceFile.ItemSpec)); 87 | } 88 | 89 | EmitObjectFile(contentSourceFile, outputFile); 90 | }); 91 | 92 | BundleApiSourceCode = GetBundleFileApiSource(_filesToBundleByObjectFileName); 93 | 94 | return !Log.HasLoggedErrors; 95 | } 96 | 97 | private void EmitObjectFile(ITaskItem fileToBundle, string destinationObjectFile) 98 | { 99 | Log.LogMessage(MessageImportance.Low, "Bundling {0} as {1}", fileToBundle.ItemSpec, destinationObjectFile); 100 | 101 | Directory.CreateDirectory(Path.GetDirectoryName(destinationObjectFile)!); 102 | 103 | var clangProcess = Process.Start(new ProcessStartInfo 104 | { 105 | FileName = ClangExecutable, 106 | Arguments = $"-xc -o \"{destinationObjectFile}\" -c -", 107 | RedirectStandardInput = true, 108 | UseShellExecute = false, 109 | })!; 110 | 111 | BundleFileToCSource(destinationObjectFile, fileToBundle, clangProcess.StandardInput.BaseStream); 112 | clangProcess.WaitForExit(); 113 | } 114 | 115 | private static string GetBundleFileApiSource(ICollection> bundledFilesByObjectFileName) 116 | { 117 | // Emit an object file that uses all the bundle file symbols and supplies an API 118 | // for getting the bundled file data at runtime 119 | var result = new StringBuilder(); 120 | 121 | result.AppendLine("#include "); 122 | result.AppendLine(); 123 | result.AppendLine("int mono_wasm_add_assembly(const char* name, const unsigned char* data, unsigned int size);"); 124 | result.AppendLine(); 125 | 126 | foreach (var objectFileGroup in bundledFilesByObjectFileName) 127 | { 128 | var symbol = ToSafeSymbolName(objectFileGroup.Key); 129 | result.AppendLine($"extern const unsigned char {symbol}[];"); 130 | result.AppendLine($"extern const int {symbol}_len;"); 131 | } 132 | 133 | result.AppendLine(); 134 | result.AppendLine("const unsigned char* dotnet_wasi_getbundledfile(const char* name, int* out_length) {"); 135 | 136 | // TODO: Instead of a naive O(N) search through all bundled files, consider putting them in a 137 | // hashtable or at least generating a series of comparisons equivalent to a binary search 138 | 139 | foreach (var objectFileGroup in bundledFilesByObjectFileName) 140 | { 141 | foreach (var file in objectFileGroup.Where(f => !string.Equals(f.GetMetadata("WasmRole"), "assembly", StringComparison.OrdinalIgnoreCase))) 142 | { 143 | var symbol = ToSafeSymbolName(objectFileGroup.Key); 144 | result.AppendLine($" if (!strcmp (name, \"{file.ItemSpec.Replace("\\", "/")}\")) {{"); 145 | result.AppendLine($" *out_length = {symbol}_len;"); 146 | result.AppendLine($" return {symbol};"); 147 | result.AppendLine(" }"); 148 | result.AppendLine(); 149 | } 150 | } 151 | 152 | result.AppendLine(" return NULL;"); 153 | result.AppendLine("}"); 154 | 155 | result.AppendLine(); 156 | result.AppendLine("void dotnet_wasi_registerbundledassemblies() {"); 157 | 158 | foreach (var objectFileGroup in bundledFilesByObjectFileName) 159 | { 160 | foreach (var file in objectFileGroup.Where(f => string.Equals(f.GetMetadata("WasmRole"), "assembly", StringComparison.OrdinalIgnoreCase))) 161 | { 162 | var symbol = ToSafeSymbolName(objectFileGroup.Key); 163 | result.AppendLine($" mono_wasm_add_assembly (\"{Path.GetFileName(file.ItemSpec)}\", {symbol}, {symbol}_len);"); 164 | } 165 | } 166 | 167 | result.AppendLine("}"); 168 | 169 | return result.ToString(); 170 | } 171 | 172 | private static void BundleFileToCSource(string objectFileName, ITaskItem fileToBundle, Stream outputStream) 173 | { 174 | // Emits a C source file in the same format as "xxd --include". Example: 175 | // 176 | // unsigned char Some_File_dll[] = { 177 | // 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x0a 178 | // }; 179 | // unsigned int Some_File_dll_len = 6; 180 | 181 | using var inputStream = File.OpenRead(fileToBundle.ItemSpec); 182 | using var outputUtf8Writer = new StreamWriter(outputStream, Utf8NoBom); 183 | 184 | var symbolName = ToSafeSymbolName(objectFileName); 185 | outputUtf8Writer.Write($"unsigned char {symbolName}[] = {{"); 186 | outputUtf8Writer.Flush(); 187 | 188 | var buf = new byte[4096]; 189 | var bytesRead = 0; 190 | var generatedArrayLength = 0; 191 | var bytesEmitted = 0; 192 | while ((bytesRead = inputStream.Read(buf, 0, buf.Length)) > 0) 193 | { 194 | for (var i = 0; i < bytesRead; i++) 195 | { 196 | if (bytesEmitted++ % 12 == 0) 197 | { 198 | outputStream.Write(NewLineAndIndentation, 0, NewLineAndIndentation.Length); 199 | } 200 | 201 | var byteValue = buf[i]; 202 | outputStream.Write(HexToUtf8Lookup, byteValue * 6, 6); 203 | } 204 | 205 | generatedArrayLength += bytesRead; 206 | } 207 | 208 | outputStream.Flush(); 209 | outputUtf8Writer.WriteLine("0\n};"); 210 | outputUtf8Writer.WriteLine($"unsigned int {symbolName}_len = {generatedArrayLength};"); 211 | } 212 | 213 | private static string ToSafeSymbolName(string objectFileName) 214 | { 215 | // Since objectFileName includes a content hash, we can safely strip off the directory name 216 | // as the filename is always unique enough. This avoid disclosing information about the build 217 | // file structure in the resulting symbols. 218 | var filename = Path.GetFileName(objectFileName); 219 | 220 | // Equivalent to the logic from "xxd --include" 221 | var sb = new StringBuilder(); 222 | foreach (var c in filename) 223 | { 224 | sb.Append(IsAlphanumeric(c) ? c : '_'); 225 | } 226 | 227 | return sb.ToString(); 228 | } 229 | 230 | // Equivalent to "isalnum" 231 | private static bool IsAlphanumeric(char c) => c 232 | is (>= 'a' and <= 'z') 233 | or (>= 'A' and <= 'Z') 234 | or (>= '0' and <= '9'); 235 | 236 | public void Cancel() 237 | { 238 | BuildTaskCancelled.Cancel(); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/Tasks/WasmResolveAssemblyDependencies.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection.Metadata; 9 | using System.Reflection.PortableExecutable; 10 | using Microsoft.Build.Framework; 11 | using Microsoft.Build.Utilities; 12 | using TaskItem = Microsoft.Build.Utilities.TaskItem; 13 | 14 | namespace Wasi.Sdk.Tasks; 15 | 16 | /// 17 | /// Starting from the entrypoint assembly, walks the graph of referenced assemblies using candidates from the 18 | /// runtime pack (first priority) or application assembly list (second priority). This is a way of reducing the 19 | /// number of bundled assemblies to the minimal set, instead of including every possible assembly from the runtime 20 | /// pack and all framework references. 21 | /// 22 | public class WasmResolveAssemblyDependencies : Microsoft.Build.Utilities.Task 23 | { 24 | [Required] 25 | public string EntryPoint { get; set; } = default!; 26 | 27 | [Required] 28 | public ITaskItem[] ApplicationAssemblies { get; set; } = default!; 29 | 30 | [Required] 31 | public ITaskItem[] WasiRuntimePackAssemblies { get; set; } = default!; 32 | 33 | [Output] 34 | public ITaskItem[]? Dependencies { get; set; } 35 | 36 | public override bool Execute() 37 | { 38 | var paths = ResolveRuntimeDependenciesCore(EntryPoint, ApplicationAssemblies, WasiRuntimePackAssemblies); 39 | Dependencies = paths.Select(p => new TaskItem(p.Path)).ToArray(); 40 | 41 | return true; 42 | } 43 | 44 | private static IEnumerable ResolveRuntimeDependenciesCore( 45 | string entryPointPath, 46 | IEnumerable applicationAssemblies, 47 | IEnumerable runtimePackAssemblies) 48 | { 49 | var entryAssembly = new AssemblyEntry(entryPointPath, GetAssemblyName(entryPointPath), originalTaskItem: null); 50 | var applicationAssemblyEntries = CreateAssemblyLookup(applicationAssemblies); 51 | var runtimePackAssemblyEntries = CreateAssemblyLookup(runtimePackAssemblies); 52 | 53 | var assemblyResolutionContext = new AssemblyResolutionContext( 54 | entryAssembly, 55 | applicationAssemblyEntries, 56 | runtimePackAssemblyEntries); 57 | assemblyResolutionContext.ResolveAssemblies(); 58 | 59 | return assemblyResolutionContext.Results; 60 | } 61 | 62 | private static Dictionary CreateAssemblyLookup(IEnumerable assemblies) 63 | { 64 | var dictionary = new Dictionary(StringComparer.Ordinal); 65 | foreach (var assembly in assemblies) 66 | { 67 | var assemblyName = GetAssemblyName(assembly.ItemSpec); 68 | if (dictionary.TryGetValue(assemblyName, out var previous)) 69 | { 70 | throw new InvalidOperationException($"Multiple assemblies found with the same assembly name '{assemblyName}':" + 71 | Environment.NewLine + string.Join(Environment.NewLine, previous, assembly.ItemSpec)); 72 | } 73 | dictionary[assemblyName] = new AssemblyEntry(assembly.ItemSpec, assemblyName, assembly); 74 | } 75 | 76 | return dictionary; 77 | } 78 | 79 | private static string GetAssemblyName(string assemblyPath) 80 | { 81 | // It would be more correct to return AssemblyName.GetAssemblyName(assemblyPath).Name, but that involves 82 | // actually loading the assembly file and maybe hitting a BadImageFormatException if it's not actually 83 | // something that can be loaded by the active .NET version (e.g., .NET Framework if this task is running 84 | // inside VS). 85 | // Instead we'll rely on the filename matching the assembly name. 86 | return Path.GetFileNameWithoutExtension(assemblyPath); 87 | } 88 | 89 | private class AssemblyResolutionContext 90 | { 91 | public AssemblyResolutionContext( 92 | AssemblyEntry entryAssembly, 93 | Dictionary applicationAssemblies, 94 | Dictionary runtimePackAssemblies) 95 | { 96 | EntryAssembly = entryAssembly; 97 | ApplicationAssemblies = applicationAssemblies; 98 | RuntimePackAssemblies = runtimePackAssemblies; 99 | } 100 | 101 | public AssemblyEntry EntryAssembly { get; } 102 | public Dictionary ApplicationAssemblies { get; } 103 | public Dictionary RuntimePackAssemblies { get; } 104 | 105 | public List Results { get; } = new(); 106 | 107 | public void ResolveAssemblies() 108 | { 109 | var visitedAssemblies = new HashSet(); 110 | var pendingAssemblies = new Stack(); 111 | pendingAssemblies.Push(EntryAssembly.Name); 112 | ResolveAssembliesCore(); 113 | 114 | void ResolveAssembliesCore() 115 | { 116 | while (pendingAssemblies.Count > 0) 117 | { 118 | var current = pendingAssemblies.Pop(); 119 | if (visitedAssemblies.Add(current)) 120 | { 121 | // Not all references will be resolvable within the runtime pack. 122 | // Skipping unresolved assemblies here is equivalent to passing "--skip-unresolved true" to the .NET linker. 123 | if (Resolve(current) is AssemblyEntry resolved) 124 | { 125 | Results.Add(resolved); 126 | var references = GetAssemblyReferences(resolved.Path); 127 | foreach (var reference in references) 128 | { 129 | pendingAssemblies.Push(reference); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | AssemblyEntry? Resolve(string assemblyName) 137 | { 138 | if (string.Equals(assemblyName, EntryAssembly.Name, StringComparison.Ordinal)) 139 | { 140 | return EntryAssembly; 141 | } 142 | 143 | // Resolution logic. For right now, we will prefer the runtime pack version of a given 144 | // assembly if there is a candidate assembly and an equivalent runtime pack assembly. 145 | if (RuntimePackAssemblies.TryGetValue(assemblyName, out var assembly) 146 | || ApplicationAssemblies.TryGetValue(assemblyName, out assembly)) 147 | { 148 | return assembly; 149 | } 150 | 151 | return null; 152 | } 153 | 154 | static IReadOnlyList GetAssemblyReferences(string assemblyPath) 155 | { 156 | try 157 | { 158 | using var peReader = new PEReader(File.OpenRead(assemblyPath)); 159 | if (!peReader.HasMetadata) 160 | { 161 | return Array.Empty(); // not a managed assembly 162 | } 163 | 164 | var metadataReader = peReader.GetMetadataReader(); 165 | 166 | var references = new List(); 167 | foreach (var handle in metadataReader.AssemblyReferences) 168 | { 169 | var reference = metadataReader.GetAssemblyReference(handle); 170 | var referenceName = metadataReader.GetString(reference.Name); 171 | 172 | references.Add(referenceName); 173 | } 174 | 175 | return references; 176 | } 177 | catch (BadImageFormatException) 178 | { 179 | // not a PE file, or invalid metadata 180 | } 181 | 182 | return Array.Empty(); // not a managed assembly 183 | } 184 | } 185 | } 186 | 187 | internal readonly struct AssemblyEntry 188 | { 189 | public AssemblyEntry(string path, string name, ITaskItem? originalTaskItem) 190 | { 191 | Path = path; 192 | Name = name; 193 | _originalTaskItem = originalTaskItem; 194 | } 195 | 196 | private readonly ITaskItem? _originalTaskItem; 197 | public string Path { get; } 198 | public string Name { get; } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/Wasi.Sdk.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0;netstandard2.0 5 | latest 6 | false 7 | true 8 | 9 | 10 | true 11 | 12 | 13 | tools\$(TargetFramework)\ 14 | tools 15 | enable 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 34 | 35 | <_WasiRuntimeArtifactsBin>..\..\modules\runtime\artifacts\bin\ 36 | <_WasiRuntimeArtifactsNativeRoot>$(_WasiRuntimeArtifactsBin)mono\Wasi.Release\ 37 | <_WasiRuntimeArtifactsBrowserWasmRoot>$(_WasiRuntimeArtifactsBin)microsoft.netcore.app.runtime.browser-wasm\Release\runtimes\browser-wasm\ 38 | 39 | 40 | <_WasiPackLibFiles Include="$(_WasiRuntimeArtifactsBrowserWasmRoot)lib\**\*.dll" /> 41 | <_WasiPackNativeFiles Include="$(_WasiRuntimeArtifactsNativeRoot)**" /> 42 | 43 | 44 | 45 | <_WasiPackNativeFiles Include="$(_WasiRuntimeArtifactsBrowserWasmRoot)native\include\mono-2.0\**" Subdir="include\" /> 46 | <_WasiPackNativeFiles Include="$(_WasiRuntimeArtifactsBrowserWasmRoot)native\include\wasm\**" Subdir="include\" /> 47 | 48 | 49 | 50 | <_WasiPackNativeFiles Include="$(_WasiRuntimeArtifactsBrowserWasmRoot)\native\libSystem.Native.a" /> 51 | 52 | 53 | <_WasiPackLibFiles Include="$(_WasiRuntimeArtifactsBrowserWasmRoot)native\System.Private.CoreLib.dll" Subdir="net7.0" /> 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | <_PackageFiles Include="$(OutDir)*\System*.dll"> 64 | tools\%(RecursiveDir) 65 | Content 66 | 67 | 68 | 69 | 70 | 71 | <_PackageFiles Include="build\**" BuildAction="Content" PackagePath="build" /> 72 | <_PackageFiles Include="native\**" BuildAction="Content" PackagePath="native" /> 73 | <_PackageFiles Include="packs\**" BuildAction="Content" PackagePath="packs" /> 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/build/Wasi.Sdk.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/build/Wasi.Sdk.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | netstandard2.0 6 | $(MSBuildThisFileDirectory)..\tools\$(WasiSdkTaskDir)\Wasi.Sdk.dll 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16.0 15 | https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-16/wasi-sdk-16.0-mingw.tar.gz 16 | https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-16/wasi-sdk-16.0-linux.tar.gz 17 | https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-16/wasi-sdk-16.0-macos.tar.gz 18 | 19 | $([System.IO.Path]::Combine("$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))", ".wasi-sdk", "wasi-sdk-$(WasiSdkVersion)")) 20 | $(WasiSdkRoot)\bin\clang 21 | $(WasiClang).exe 22 | $(MSBuildThisFileDirectory)..\packs\wasi-wasm\ 23 | true 24 | 25 | 26 | wasmtime 27 | 28 | 29 | $(CopyWasmToOutputDependsOn); 30 | ObtainWasiSdk; 31 | PrepareBuildWasmInputs; 32 | BuildWasm; 33 | 34 | 35 | 36 | 37 | $(WasiRunner) 38 | $(MSBuildProjectDirectory) 39 | 40 | 41 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).wasm 42 | 43 | 44 | $(RunArguments) --tcplisten 127.0.0.1:$(WasiDebugPort) --env DEBUGGER_FD=3 45 | 46 | $(RunArguments) $(WasiRunnerArgs) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 74 | 75 | <_PathSep>/ 76 | <_PathSep Condition="$([MSBuild]::IsOSPlatform('Windows'))">\ 77 | 78 | 79 | <_WasiResolvedFrameworkReference Include="@(ResolvedFrameworkReference)" Condition="'%(ResolvedFrameworkReference.Identity)' != 'Microsoft.NETCore.App'" /> 80 | <_WasiResolvedFrameworkReference Update="@(_WasiResolvedFrameworkReference)"> 81 | 83 | $([System.String]::Copy('%(TargetingPackPath)').Replace('packs$(_PathSep)%(OriginalItemSpec).Ref', 'shared$(_PathSep)%(OriginalItemSpec)')) 84 | 85 | 86 | 87 | <_WasiResolvedFrameworkReferenceAssembliesPattern>@(_WasiResolvedFrameworkReference->'%(ImplementationPath)\*.dll', ';') 88 | 89 | 90 | <_WasiResolvedFrameworkReferenceAssemblies Include="$(_WasiResolvedFrameworkReferenceAssembliesPattern)" /> 91 | 92 | 93 | 94 | 95 | 96 | 97 | <_WasiRuntimePackAssemblies Include="$(WasiRuntimePackRoot)lib\net7.0\*.dll" /> 98 | <_WasiRuntimePackAssemblies Include="@(_WasiResolvedFrameworkReferenceAssemblies)" /> 99 | <_WasmApplicationAssemblies Include="@(ReferencePath)" Condition="'%(ReferencePath.FrameworkReferenceName)' == ''" /> 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | <_DotNetHostDirectory>$(NetCoreRoot) 110 | <_DotNetHostFileName>dotnet 111 | <_DotNetHostFileName Condition="'$(OS)' == 'Windows_NT'">dotnet.exe 112 | 113 | 127 | 128 | <_WasmTransitivelyReferencedAssemblies Remove="@(_WasmTransitivelyReferencedAssemblies)" /> 129 | <_WasmTransitivelyReferencedAssemblies Include="$(IntermediateOutputPath)dotnet-wasi-sdk\linked\*.dll" /> 130 | 131 | 132 | 133 | 134 | <_WasmTransitivelyReferencedPdbs 135 | Include="@(_WasmTransitivelyReferencedAssemblies -> '%(RelativeDir)%(Filename).pdb')" 136 | Condition="Exists('%(RelativeDir)%(Filename).pdb')" /> 137 | 138 | 139 | 140 | 141 | $(IntermediateOutputPath)dotnet-wasi-sdk\ 142 | $(WasmOutputIntermediateDir)$(AssemblyName).wasm 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | <_WasmBundleObjectsIntermediateDir>$(WasmOutputIntermediateDir)bundle-objects\ 157 | <_GetBundledFileSourcePath>$(_WasmBundleObjectsIntermediateDir)dotnet_wasi_getbundledfile.c 158 | 159 | 160 | 162 | 163 | 164 | 165 | 166 | 167 | $(_WasmBundleObjectsIntermediateDir)%(WasmBundleFilesWithHashes.Filename)%(WasmBundleFilesWithHashes.Extension).$([System.String]::Copy(%(WasmBundleFilesWithHashes.FileHash)).Substring(0, 8)).o 168 | 169 | 170 | 171 | 172 | 173 | 174 | 176 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 196 | 197 | $(WasmOutputIntermediateDir)entrypoint_$(AssemblyName).c 198 | 199 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | @(WasiNativeFileReference, ' ') 213 | $(WasiSdkClangArgs) --sysroot="$(WasiSdkRoot)\share\wasi-sysroot" 214 | $(WasiSdkClangArgs) -I"$(WasiRuntimePackRoot)\native\include" 215 | $(WasiSdkClangArgs) -Wl,--export=malloc,--export=free,--export=__heap_base,--export=__data_end 216 | $(WasiSdkClangArgs) -Wl,-z,stack-size=1048576,--initial-memory=52428800,--max-memory=524288000,-lwasi-emulated-mman 217 | $(WasiSdkClangArgs) -Wl,-s 218 | $(WasiSdkClangArgs) -D WASI_AFTER_RUNTIME_LOADED_DECLARATIONS="@(WasiAfterRuntimeLoadedDeclarations, ' ')" 219 | $(WasiSdkClangArgs) -D WASI_AFTER_RUNTIME_LOADED_CALLS="@(WasiAfterRuntimeLoadedCalls, ' ')" 220 | $(WasiSdkClangArgs) -o "$(WasmOutputIntermediateFile)" 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 234 | 235 | 236 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | $(IntermediateOutputPath)\wasi-sdk-temp 248 | 249 | 250 | 251 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/native/generated-pinvokes.h: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT MODIFY 2 | 3 | int SystemNative_Access (int,int); 4 | int SystemNative_AlignedAlloc (int,int); 5 | void SystemNative_AlignedFree (int); 6 | int SystemNative_AlignedRealloc (int,int,int); 7 | int SystemNative_Calloc (int,int); 8 | int SystemNative_CanGetHiddenFlag (); 9 | int SystemNative_ChDir (int); 10 | int SystemNative_ChMod (int,int); 11 | int SystemNative_Close (int); 12 | int SystemNative_CloseDir (int); 13 | int SystemNative_ConvertErrorPalToPlatform (int); 14 | int SystemNative_ConvertErrorPlatformToPal (int); 15 | int SystemNative_CopyFile (int,int,int64_t); 16 | int SystemNative_Dup (int); 17 | int SystemNative_FAllocate (int,int64_t,int64_t); 18 | int SystemNative_FChflags (int,int); 19 | int SystemNative_FChMod (int,int); 20 | int SystemNative_FLock (int,int); 21 | void SystemNative_Free (int); 22 | void SystemNative_FreeEnviron (int); 23 | int SystemNative_FStat (int,int); 24 | int SystemNative_FSync (int); 25 | int SystemNative_FTruncate (int,int64_t); 26 | int SystemNative_FUTimens (int,int); 27 | int SystemNative_GetCpuUtilization (int); 28 | int SystemNative_GetCryptographicallySecureRandomBytes (int,int); 29 | int SystemNative_GetCwd (int,int); 30 | int SystemNative_GetDefaultSearchOrderPseudoHandle (); 31 | int SystemNative_GetEnv (int); 32 | int SystemNative_GetEnviron (); 33 | int SystemNative_GetErrNo (); 34 | int SystemNative_GetFileSystemType (int); 35 | void SystemNative_GetNonCryptographicallySecureRandomBytes (int,int); 36 | int SystemNative_GetReadDirRBufferSize (); 37 | int64_t SystemNative_GetSystemTimeAsTicks (); 38 | uint64_t SystemNative_GetTimestamp (); 39 | int SystemNative_LChflags (int,int); 40 | int SystemNative_LChflagsCanSetHiddenFlag (); 41 | int SystemNative_Link (int,int); 42 | int SystemNative_LockFileRegion (int,int64_t,int64_t,int); 43 | void SystemNative_Log (int,int); 44 | void SystemNative_LogError (int,int); 45 | void SystemNative_LowLevelMonitor_Acquire (int); 46 | int SystemNative_LowLevelMonitor_Create (); 47 | void SystemNative_LowLevelMonitor_Destroy (int); 48 | void SystemNative_LowLevelMonitor_Release (int); 49 | void SystemNative_LowLevelMonitor_Signal_Release (int); 50 | int SystemNative_LowLevelMonitor_TimedWait (int,int); 51 | void SystemNative_LowLevelMonitor_Wait (int); 52 | int64_t SystemNative_LSeek (int,int64_t,int); 53 | int SystemNative_LStat (int,int); 54 | int SystemNative_Malloc (int); 55 | int SystemNative_MkDir (int,int); 56 | int SystemNative_MksTemps (int,int); 57 | int SystemNative_Open (int,int,int); 58 | int SystemNative_OpenDir (int); 59 | int SystemNative_PosixFAdvise (int,int64_t,int64_t,int); 60 | int SystemNative_PRead (int,int,int,int64_t); 61 | int64_t SystemNative_PReadV (int,int,int,int64_t); 62 | int SystemNative_PWrite (int,int,int,int64_t); 63 | int64_t SystemNative_PWriteV (int,int,int,int64_t); 64 | int SystemNative_Read (int,int,int); 65 | int SystemNative_ReadDirR (int,int,int,int); 66 | int SystemNative_ReadLink (int,int,int); 67 | int SystemNative_Realloc (int,int); 68 | int SystemNative_Rename (int,int); 69 | int SystemNative_RmDir (int); 70 | void SystemNative_SetErrNo (int); 71 | int SystemNative_Stat (int,int); 72 | int SystemNative_StrErrorR (int,int,int); 73 | int SystemNative_SymLink (int,int); 74 | int64_t SystemNative_SysConf (int); 75 | void SystemNative_SysLog (int,int,int); 76 | int SystemNative_Unlink (int); 77 | int SystemNative_UTimensat (int,int); 78 | int SystemNative_Write (int,int,int); 79 | static PinvokeImport libSystem_Native_imports [] = { 80 | {"SystemNative_Access", SystemNative_Access}, // System.Private.CoreLib 81 | {"SystemNative_AlignedAlloc", SystemNative_AlignedAlloc}, // System.Private.CoreLib 82 | {"SystemNative_AlignedFree", SystemNative_AlignedFree}, // System.Private.CoreLib 83 | {"SystemNative_AlignedRealloc", SystemNative_AlignedRealloc}, // System.Private.CoreLib 84 | {"SystemNative_Calloc", SystemNative_Calloc}, // System.Private.CoreLib 85 | {"SystemNative_CanGetHiddenFlag", SystemNative_CanGetHiddenFlag}, // System.Private.CoreLib 86 | {"SystemNative_ChDir", SystemNative_ChDir}, // System.Private.CoreLib 87 | {"SystemNative_ChMod", SystemNative_ChMod}, // System.Private.CoreLib 88 | {"SystemNative_Close", SystemNative_Close}, // System.Private.CoreLib 89 | {"SystemNative_CloseDir", SystemNative_CloseDir}, // System.Private.CoreLib 90 | {"SystemNative_ConvertErrorPalToPlatform", SystemNative_ConvertErrorPalToPlatform}, // System.Console, System.Private.CoreLib 91 | {"SystemNative_ConvertErrorPlatformToPal", SystemNative_ConvertErrorPlatformToPal}, // System.Console, System.Private.CoreLib 92 | {"SystemNative_CopyFile", SystemNative_CopyFile}, // System.Private.CoreLib 93 | {"SystemNative_Dup", SystemNative_Dup}, // System.Console 94 | {"SystemNative_FAllocate", SystemNative_FAllocate}, // System.Private.CoreLib 95 | {"SystemNative_FChflags", SystemNative_FChflags}, // System.Private.CoreLib 96 | {"SystemNative_FChMod", SystemNative_FChMod}, // System.Private.CoreLib 97 | {"SystemNative_FLock", SystemNative_FLock}, // System.Private.CoreLib 98 | {"SystemNative_Free", SystemNative_Free}, // System.Private.CoreLib 99 | {"SystemNative_FreeEnviron", SystemNative_FreeEnviron}, // System.Private.CoreLib 100 | {"SystemNative_FStat", SystemNative_FStat}, // System.Private.CoreLib 101 | {"SystemNative_FSync", SystemNative_FSync}, // System.Private.CoreLib 102 | {"SystemNative_FTruncate", SystemNative_FTruncate}, // System.Private.CoreLib 103 | {"SystemNative_FUTimens", SystemNative_FUTimens}, // System.Private.CoreLib 104 | {"SystemNative_GetCpuUtilization", SystemNative_GetCpuUtilization}, // System.Private.CoreLib 105 | {"SystemNative_GetCryptographicallySecureRandomBytes", SystemNative_GetCryptographicallySecureRandomBytes}, // System.Private.CoreLib 106 | {"SystemNative_GetCwd", SystemNative_GetCwd}, // System.Private.CoreLib 107 | {"SystemNative_GetDefaultSearchOrderPseudoHandle", SystemNative_GetDefaultSearchOrderPseudoHandle}, // System.Private.CoreLib 108 | {"SystemNative_GetEnv", SystemNative_GetEnv}, // System.Private.CoreLib 109 | {"SystemNative_GetEnviron", SystemNative_GetEnviron}, // System.Private.CoreLib 110 | {"SystemNative_GetErrNo", SystemNative_GetErrNo}, // System.Private.CoreLib 111 | {"SystemNative_GetFileSystemType", SystemNative_GetFileSystemType}, // System.Private.CoreLib 112 | {"SystemNative_GetNonCryptographicallySecureRandomBytes", SystemNative_GetNonCryptographicallySecureRandomBytes}, // System.Private.CoreLib 113 | {"SystemNative_GetReadDirRBufferSize", SystemNative_GetReadDirRBufferSize}, // System.Private.CoreLib 114 | {"SystemNative_GetSystemTimeAsTicks", SystemNative_GetSystemTimeAsTicks}, // System.Private.CoreLib 115 | {"SystemNative_GetTimestamp", SystemNative_GetTimestamp}, // System.Private.CoreLib 116 | {"SystemNative_LChflags", SystemNative_LChflags}, // System.Private.CoreLib 117 | {"SystemNative_LChflagsCanSetHiddenFlag", SystemNative_LChflagsCanSetHiddenFlag}, // System.Private.CoreLib 118 | {"SystemNative_Link", SystemNative_Link}, // System.Private.CoreLib 119 | {"SystemNative_LockFileRegion", SystemNative_LockFileRegion}, // System.Private.CoreLib 120 | {"SystemNative_Log", SystemNative_Log}, // System.Private.CoreLib 121 | {"SystemNative_LogError", SystemNative_LogError}, // System.Private.CoreLib 122 | {"SystemNative_LowLevelMonitor_Acquire", SystemNative_LowLevelMonitor_Acquire}, // System.Private.CoreLib 123 | {"SystemNative_LowLevelMonitor_Create", SystemNative_LowLevelMonitor_Create}, // System.Private.CoreLib 124 | {"SystemNative_LowLevelMonitor_Destroy", SystemNative_LowLevelMonitor_Destroy}, // System.Private.CoreLib 125 | {"SystemNative_LowLevelMonitor_Release", SystemNative_LowLevelMonitor_Release}, // System.Private.CoreLib 126 | {"SystemNative_LowLevelMonitor_Signal_Release", SystemNative_LowLevelMonitor_Signal_Release}, // System.Private.CoreLib 127 | {"SystemNative_LowLevelMonitor_TimedWait", SystemNative_LowLevelMonitor_TimedWait}, // System.Private.CoreLib 128 | {"SystemNative_LowLevelMonitor_Wait", SystemNative_LowLevelMonitor_Wait}, // System.Private.CoreLib 129 | {"SystemNative_LSeek", SystemNative_LSeek}, // System.Private.CoreLib 130 | {"SystemNative_LStat", SystemNative_LStat}, // System.Private.CoreLib 131 | {"SystemNative_Malloc", SystemNative_Malloc}, // System.Private.CoreLib 132 | {"SystemNative_MkDir", SystemNative_MkDir}, // System.Private.CoreLib 133 | {"SystemNative_MksTemps", SystemNative_MksTemps}, // System.Private.CoreLib 134 | {"SystemNative_Open", SystemNative_Open}, // System.Private.CoreLib 135 | {"SystemNative_OpenDir", SystemNative_OpenDir}, // System.Private.CoreLib 136 | {"SystemNative_PosixFAdvise", SystemNative_PosixFAdvise}, // System.Private.CoreLib 137 | {"SystemNative_PRead", SystemNative_PRead}, // System.Private.CoreLib 138 | {"SystemNative_PReadV", SystemNative_PReadV}, // System.Private.CoreLib 139 | {"SystemNative_PWrite", SystemNative_PWrite}, // System.Private.CoreLib 140 | {"SystemNative_PWriteV", SystemNative_PWriteV}, // System.Private.CoreLib 141 | {"SystemNative_Read", SystemNative_Read}, // System.Private.CoreLib 142 | {"SystemNative_ReadDirR", SystemNative_ReadDirR}, // System.Private.CoreLib 143 | {"SystemNative_ReadLink", SystemNative_ReadLink}, // System.Private.CoreLib 144 | {"SystemNative_Realloc", SystemNative_Realloc}, // System.Private.CoreLib 145 | {"SystemNative_Rename", SystemNative_Rename}, // System.Private.CoreLib 146 | {"SystemNative_RmDir", SystemNative_RmDir}, // System.Private.CoreLib 147 | {"SystemNative_SetErrNo", SystemNative_SetErrNo}, // System.Private.CoreLib 148 | {"SystemNative_Stat", SystemNative_Stat}, // System.Private.CoreLib 149 | {"SystemNative_StrErrorR", SystemNative_StrErrorR}, // System.Console, System.Private.CoreLib 150 | {"SystemNative_SymLink", SystemNative_SymLink}, // System.Private.CoreLib 151 | {"SystemNative_SysConf", SystemNative_SysConf}, // System.Private.CoreLib 152 | {"SystemNative_SysLog", SystemNative_SysLog}, // System.Private.CoreLib 153 | {"SystemNative_Unlink", SystemNative_Unlink}, // System.Private.CoreLib 154 | {"SystemNative_UTimensat", SystemNative_UTimensat}, // System.Private.CoreLib 155 | {"SystemNative_Write", SystemNative_Write}, // System.Console, System.Private.CoreLib 156 | {NULL, NULL} 157 | }; 158 | static void *pinvoke_tables[] = { libSystem_Native_imports,}; 159 | static char *pinvoke_names[] = { "libSystem.Native",}; 160 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/native/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // This symbol's implementation is generated during the build 6 | const char* dotnet_wasi_getentrypointassemblyname(); 7 | 8 | // These are generated by EmitWasmBundleObjectFile 9 | const char* dotnet_wasi_getbundledfile(const char* name, int* out_length); 10 | void dotnet_wasi_registerbundledassemblies(); 11 | 12 | // TODO: This should actually go in driver.c in the runtime 13 | void mono_marshal_ilgen_init() {} 14 | 15 | #ifdef WASI_AFTER_RUNTIME_LOADED_DECLARATIONS 16 | // This is supplied from the MSBuild itemgroup @(WasiAfterRuntimeLoaded) 17 | WASI_AFTER_RUNTIME_LOADED_DECLARATIONS 18 | #endif 19 | 20 | int main() { 21 | dotnet_wasi_registerbundledassemblies(); 22 | 23 | mono_wasm_load_runtime("", 0); 24 | 25 | #ifdef WASI_AFTER_RUNTIME_LOADED_CALLS 26 | // This is supplied from the MSBuild itemgroup @(WasiAfterRuntimeLoaded) 27 | WASI_AFTER_RUNTIME_LOADED_CALLS 28 | #endif 29 | 30 | // TODO: Consider passing through the args 31 | MonoArray* args = mono_wasm_string_array_new(0); 32 | void* entry_method_params[] = { args }; 33 | 34 | MonoAssembly* assembly = mono_assembly_open(dotnet_wasi_getentrypointassemblyname(), NULL); 35 | MonoMethod* entry_method = mono_wasm_assembly_get_entry_point(assembly); 36 | MonoObject* out_exc = NULL; 37 | mono_wasm_invoke_method(entry_method, NULL, entry_method_params, &out_exc); 38 | 39 | // Note: if the return value isn't an int (e.g., it's async main, returning a Task) 40 | // then the following will result in an obscure error. 41 | // TODO: Handle all entrypoint method types 42 | // return mono_unbox_int(exit_code); 43 | return out_exc ? 1 : 0; 44 | } 45 | -------------------------------------------------------------------------------- /src/Wasi.Sdk/native/pinvoke.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "generated-pinvokes.h" 6 | 7 | void* 8 | wasm_dl_lookup_pinvoke_table (const char *name) 9 | { 10 | for (int i = 0; i < sizeof(pinvoke_tables) / sizeof(void*); ++i) { 11 | if (!strcmp (name, pinvoke_names [i])) 12 | return pinvoke_tables [i]; 13 | } 14 | return NULL; 15 | } 16 | --------------------------------------------------------------------------------