├── .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 |
8 | Hello World! This is running on @RuntimeInformation.FrameworkDescription,
9 | on @RuntimeInformation.OSArchitecture,
10 | at @DateTime.Now.ToLongTimeString()
11 |
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