"]
3 | description = "Add a new pet"
4 | name = "Fermyon.PetStore.NewPet"
5 | version = "1.0.0"
6 | trigger = { type = "http", base = "/" }
7 |
--------------------------------------------------------------------------------
/wit/ephemeral/pg-types.wit:
--------------------------------------------------------------------------------
1 | // General purpose error.
2 | variant pg-error {
3 | success,
4 | connection-failed(string),
5 | bad-parameter(string),
6 | query-failed(string),
7 | value-conversion-failed(string),
8 | other-error(string)
9 | }
10 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewPet/assets/NewPetMissingField.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | New Pet
5 |
6 |
7 |
8 | New Pet - Error!
9 |
10 | Both the name and picture are required.
11 |
12 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Common/Configuration.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | namespace Fermyon.PetStore.Common;
4 |
5 | public static class Configuration
6 | {
7 | public static string DbConnectionString()
8 | {
9 | return SpinConfig.Get("pg_conn_str");
10 | }
11 | }
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Pet/assets/PetNotFound.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Pet not found!
5 |
6 |
7 |
8 | That pet was not found.
9 | View all pets
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Toy/assets/ToyNotFound.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Toy not found!
5 |
6 |
7 |
8 | That toy was not found.
9 | View all toys
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewToy/assets/NewToyMissingField.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | New Toy
5 |
6 |
7 |
8 | New Toy - Error!
9 |
10 | The description, owner and picture are all required.
11 |
12 |
--------------------------------------------------------------------------------
/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | # MAINTAINERS
2 |
3 | ## Current Maintainers
4 |
5 | _Listed in alphabetical order by first name_
6 |
7 | | Name | GitHub Username |
8 | | --- | --- |
9 | | Ivan Towlson | itowlson |
10 | | Joel Dice | dicej |
11 | | Radu Matei | radu-matei |
12 | | Vaughn Dice | vdice |
13 |
14 | ## Emeritus Maintainers
15 |
16 | None
17 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Common/HttpRequestExtensions.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | namespace Fermyon.PetStore.Common;
4 |
5 | public static class HttpRequestExtensions
6 | {
7 | public static bool IsRuntime(this HttpRequest request)
8 | {
9 | return request.Url != Warmup.DefaultWarmupUrl;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewToy/assets/NewToyNoPets.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | New Toy
5 |
6 |
7 |
8 | Please record a pet first
9 |
10 | Every toy must have an owner!
11 | Add a new pet now
12 |
13 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Pets/assets/PetsTemplate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Pets
5 |
6 |
7 |
8 | Pets
9 |
10 |
11 | {{ db-table }}
12 |
13 |
14 | Add a new pet
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Toys/assets/ToysTemplate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Toys
5 |
6 |
7 |
8 | Toys
9 |
10 |
11 | {{ db-table }}
12 |
13 |
14 | Add a new toy
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/sql/schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE pets (
2 | id integer PRIMARY KEY,
3 | name varchar(80) not null,
4 | picture bytea
5 | );
6 |
7 | CREATE TABLE toys (
8 | id integer PRIMARY KEY,
9 | owner_id integer REFERENCES pets (id),
10 | count integer,
11 | description varchar(200) not null,
12 | picture bytea
13 | );
14 |
15 |
--------------------------------------------------------------------------------
/wit/ephemeral/spin-config.wit:
--------------------------------------------------------------------------------
1 | // Get a configuration value for the current component.
2 | // The config key must match one defined in in the component manifest.
3 | get-config: func(key: string) -> expected
4 |
5 | variant error {
6 | provider(string),
7 | invalid-key(string),
8 | invalid-schema(string),
9 | other(string),
10 | }
--------------------------------------------------------------------------------
/templates/http-csharp/metadata/spin-template.toml:
--------------------------------------------------------------------------------
1 | manifest_version = "1"
2 | id = "http-csharp"
3 | description = "HTTP request handler using C# (EXPERIMENTAL)"
4 |
5 | [parameters]
6 | project-description = { type = "string", prompt = "Project description", default = "" }
7 | http-path = { type = "string", prompt = "HTTP path", default = "/...", pattern = "^/\\S*$" }
8 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Common/GenericResponse.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | namespace Fermyon.PetStore.Common;
4 |
5 | public static class GenericResponse
6 | {
7 | public static HttpResponse NotFound()
8 | {
9 | return new HttpResponse
10 | {
11 | StatusCode = System.Net.HttpStatusCode.NotFound,
12 | };
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Pet/assets/PetTemplate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ name }}
5 |
6 |
7 |
8 | {{ name }}
9 |
10 |
11 |

12 |
13 |
14 | Add a new pet
15 |
16 |
17 |
--------------------------------------------------------------------------------
/wit/ephemeral/outbound-pg.wit:
--------------------------------------------------------------------------------
1 | use * from pg-types
2 | use * from rdbms-types
3 |
4 | // query the database: select
5 | query: func(address: string, statement: string, params: list) -> expected
6 |
7 | // execute command to the database: insert, update, delete
8 | execute: func(address: string, statement: string, params: list) -> expected
9 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewPet/assets/NewPetCreated.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | New Pet
5 |
6 |
7 |
8 | New Pet - Success!
9 |
10 | Your pet has been recorded.
11 |
12 | View and add pets
13 | View and add toys
14 |
15 |
16 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewToy/assets/NewToyCreated.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | New Toy
5 |
6 |
7 |
8 | New Toy - Success!
9 |
10 | Your pet's toy has been recorded.
11 |
12 | View and add pets
13 | View and add toys
14 |
15 |
16 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Home/assets/Home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Spin .NET Pet Store by Fermyon
5 |
6 |
7 |
8 | Welcome to the .NET Pet Store, powered by Spin
9 | Where pets keep their toys on the Internet
10 |
11 | View and add pets
12 | View and add toys
13 |
14 |
15 |
--------------------------------------------------------------------------------
/wit/ephemeral/outbound-redis.wit:
--------------------------------------------------------------------------------
1 | use * from redis-types
2 |
3 | // Publish a Redis message to the specified channel and return an error, if any.
4 | publish: func(address: string, channel: string, payload: payload) -> expected
5 |
6 | // Get the value of a key.
7 | get: func(address: string, key: string) -> expected
8 |
9 | // Set key to value. If key already holds a value, it is overwritten.
10 | set: func(address: string, key: string, value: payload) -> expected
11 |
--------------------------------------------------------------------------------
/templates/http-csharp/content/spin.toml:
--------------------------------------------------------------------------------
1 | spin_manifest_version = 2
2 |
3 | [application]
4 | authors = ["{{authors}}"]
5 | description = "{{project-description}}"
6 | name = "{{project-name}}"
7 | version = "1.0.0"
8 |
9 | [[trigger.http]]
10 | route = "{{http-path}}"
11 | component = "{{project-name | kebab_case}}"
12 |
13 | [component.{{project-name | kebab_case}}]
14 | source = "bin/Release/net8.0/{{project-name | dotted_pascal_case}}.wasm"
15 | [component.{{project-name | kebab_case}}.build]
16 | command = "dotnet build -c Release"
17 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Toy/assets/ToyTemplate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ description }}
5 |
6 |
7 |
8 | {{ description }}
9 |
10 | {{ owner_name }} has {{ count }} of these.
11 |
12 |

loves

13 |
14 |
15 | Add a new toy
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Common/Fermyon.PetStore.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | net8.0
13 | enable
14 | enable
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewPet/assets/NewPet.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | New Pet
5 |
6 |
7 |
8 | New Pet
9 |
10 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/templates/http-csharp/content/Handler.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | namespace {{project-name | dotted_pascal_case}};
4 |
5 | public static class Handler
6 | {
7 | [HttpHandler]
8 | public static HttpResponse HandleHttpRequest(HttpRequest request)
9 | {
10 | return new HttpResponse
11 | {
12 | StatusCode = System.Net.HttpStatusCode.OK,
13 | Headers = new Dictionary
14 | {
15 | { "Content-Type", "text/plain" },
16 | },
17 | BodyAsString = "Hello from .NET\n",
18 | };
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/templates/http-csharp/content/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | net8.0
13 | {{project-name | dotted_pascal_case}}
14 | enable
15 | enable
16 |
17 |
18 | true
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/RedisInterop.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | namespace Fermyon.Spin.Sdk;
4 |
5 | internal static class OutboundRedisInterop
6 | {
7 | [MethodImpl(MethodImplOptions.InternalCall)]
8 | internal static extern unsafe byte outbound_redis_get(ref InteropString address, ref InteropString key, ref Buffer ret0);
9 |
10 | [MethodImpl(MethodImplOptions.InternalCall)]
11 | internal static extern unsafe byte outbound_redis_set(ref InteropString address, ref InteropString key, ref Buffer value);
12 |
13 | [MethodImpl(MethodImplOptions.InternalCall)]
14 | internal static extern unsafe byte outbound_redis_publish(ref InteropString address, ref InteropString channel, ref Buffer value);
15 | }
16 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: generate
2 | generate:
3 | # wit-bindgen c --export wit/ephemeral/spin-http.wit --out-dir ./src/native/
4 | # wit-bindgen c --import wit/ephemeral/wasi-outbound-http.wit --out-dir ./src/native/
5 | # wit-bindgen c --import wit/ephemeral/outbound-redis.wit --out-dir ./src/native
6 | # wit-bindgen c --import wit/ephemeral/outbound-pg.wit --out-dir ./src/native
7 | # wit-bindgen c --import wit/ephemeral/spin-config.wit --out-dir ./src/native
8 |
9 | .PHONY: bootstrap
10 | bootstrap:
11 | # install the WIT Bindgen version we are currently using in Spin e06c6b1
12 | cargo install wit-bindgen-cli --git https://github.com/bytecodealliance/wit-bindgen --rev dde4694aaa6acf9370206527a798ac4ba6a8c5b8 --force
13 | cargo install wizer --all-features
14 |
--------------------------------------------------------------------------------
/samples/hello-world/HelloWorld.Spin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Library
8 | net8.0
9 | hello_world
10 | enable
11 | enable
12 | true
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/README.md:
--------------------------------------------------------------------------------
1 | # Spin .NET PostgreSQL sample
2 |
3 | This directory contains a work-in-progress sample of building a database-backed
4 | application using Spin and PostgreSQL.
5 |
6 | If you want to run this sample, you'll need to:
7 |
8 | 1. Set up a PostgreSQL database using the tables in the `sql` directory.
9 | 1. Set the `SPIN_VARIABLE_PG_CONN_STR` environment variable to the connection string for the database.
10 | The connection string is in space-separated format e.g. `user=foo password=bar dbname=test host=127.0.0.1`
11 |
12 | We will provide more detailed information in a subsequent update. In the
13 | meantime, if you run into problems, please [raise an issue](https://github.com/fermyon/spin-dotnet-sdk/issues)
14 | or [contact us on Discord](https://discord.gg/AAFNfS7NGf).
15 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Home/Handler.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Fermyon.Spin.Sdk;
3 |
4 | using Fermyon.PetStore.Common;
5 |
6 | namespace Fermyon.PetStore.Home;
7 |
8 | public static class Handler
9 | {
10 | [HttpHandler]
11 | public static HttpResponse HandleHttpRequest(HttpRequest request)
12 | {
13 | var html = request.IsRuntime() ?
14 | File.ReadAllText("/assets/Home.html") :
15 | String.Empty;
16 |
17 | return new HttpResponse
18 | {
19 | StatusCode = System.Net.HttpStatusCode.OK,
20 | Headers = new Dictionary
21 | {
22 | { "Content-Type", "text/html" },
23 | },
24 | BodyAsString = html,
25 | };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/hello-world/spin.toml:
--------------------------------------------------------------------------------
1 | spin_manifest_version = 2
2 |
3 | [application]
4 | name = "dotnet-hello"
5 | version = "1.0.0"
6 |
7 | [variables.defaulted]
8 | default = "test value"
9 |
10 | [variables.required]
11 | required = true
12 |
13 | [[trigger.http]]
14 | route = "/..."
15 | component = "hello"
16 |
17 | [component.hello]
18 | source = "bin/Release/net8.0/HelloWorld.Spin.wasm"
19 | allowed_outbound_hosts = [
20 | # http requests to this same app
21 | "http://127.0.0.1:3000",
22 | # requests to postgres
23 | "postgres://127.0.0.1:5432",
24 | # requests to redis
25 | "redis://127.0.0.1:6379"
26 | ]
27 | files = [{ source = "assets", destination = "/assets" }]
28 | [component.hello.variables]
29 | defaulted = "{{ defaulted }}"
30 | required = "{{ required }}"
31 | [component.hello.build]
32 | command = "dotnet build -c Release"
33 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Home/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | net8.0
17 | Fermyon.PetStore.Home
18 | enable
19 | enable
20 |
21 |
22 | true
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Toys/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | net8.0
17 | Fermyon.PetStore.Toys
18 | enable
19 | enable
20 |
21 |
22 | true
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Pets/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | net8.0
17 | Fermyon.PetStore.Pets
18 | enable
19 | enable
20 |
21 |
22 |
23 | true
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/HttpTrigger.cs:
--------------------------------------------------------------------------------
1 | namespace Fermyon.Spin.Sdk;
2 |
3 | ///
4 | /// Marks a method as being the handler for the Spin HTTP trigger.
5 | ///
6 | [AttributeUsage(AttributeTargets.Method)]
7 | public sealed class HttpHandlerAttribute : Attribute
8 | {
9 | ///
10 | /// Gets or sets the URL passed to the HTTP handler to indicate that the request is occurring during warmup.
11 | /// The default is Warmup.DefaultWarmupUrl.
12 | ///
13 | public string WarmupUrl { get; set; } = Warmup.DefaultWarmupUrl;
14 | }
15 |
16 | ///
17 | /// Constants that can be used to check if a call is occurring during warmup.
18 | ///
19 | public static class Warmup
20 | {
21 | ///
22 | /// The default URL passed to a HTTP handler to indicate that the request is occurring during warmup.
23 | ///
24 | public const string DefaultWarmupUrl = "/warmupz";
25 | }
26 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewToy/assets/NewToy.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | New Toy
5 |
6 |
7 |
8 | New Toy
9 |
10 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/HttpOutbound.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 |
4 | namespace Fermyon.Spin.Sdk;
5 |
6 | ///
7 | /// Sends HTTP requests.
8 | ///
9 | public static class HttpOutbound
10 | {
11 | // TODO: wonder if we can use a HttpClient with custom HttpMessageHandler,
12 | // or HttpMessageInvoker? Trouble is those are async and our handler can't
13 | // be async...
14 | ///
15 | /// Sends the specified HTTP request, and returns the response.
16 | ///
17 | public static HttpResponse Send(HttpRequest request)
18 | {
19 | var resp = new HttpResponse();
20 | var err = OutboundHttpInterop.wasi_outbound_http_request(ref request, ref resp);
21 |
22 | if (err == 0 || err == 255)
23 | {
24 | return resp;
25 | }
26 | else
27 | {
28 | throw new HttpRequestException($"Outbound error {err}");
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Fermyon.Spin.Sdk.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | <_PackageFiles Include="build\**" BuildAction="Content" PackagePath="build" />
20 | <_PackageFiles Include="native\**" BuildAction="Content" PackagePath="native" />
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Pet/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | net8.0
17 | preview
18 | Fermyon.PetStore.Pet
19 | enable
20 | enable
21 |
22 |
23 |
24 | false
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Toy/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | net8.0
17 | preview
18 | Fermyon.PetStore.Toy
19 | enable
20 | enable
21 |
22 |
23 |
24 | false
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewPet/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | net8.0
21 | Fermyon.PetStore.NewPet
22 | enable
23 | enable
24 |
25 |
26 |
27 | false
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewToy/Project.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | net8.0
21 | Fermyon.PetStore.NewToy
22 | enable
23 | enable
24 |
25 |
26 |
27 | false
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/wit/ephemeral/rdbms-types.wit:
--------------------------------------------------------------------------------
1 | enum db-data-type {
2 | boolean,
3 | int8,
4 | int16,
5 | int32,
6 | int64,
7 | uint8,
8 | uint16,
9 | uint32,
10 | uint64,
11 | floating32,
12 | floating64,
13 | str,
14 | binary,
15 | other,
16 | }
17 |
18 | variant db-value {
19 | boolean(bool),
20 | int8(s8),
21 | int16(s16),
22 | int32(s32),
23 | int64(s64),
24 | uint8(u8),
25 | uint16(u16),
26 | uint32(u32),
27 | uint64(u64),
28 | floating32(float32),
29 | floating64(float64),
30 | str(string),
31 | binary(list),
32 | db-null,
33 | unsupported,
34 | }
35 |
36 | variant parameter-value {
37 | boolean(bool),
38 | int8(s8),
39 | int16(s16),
40 | int32(s32),
41 | int64(s64),
42 | uint8(u8),
43 | uint16(u16),
44 | uint32(u32),
45 | uint64(u64),
46 | floating32(float32),
47 | floating64(float64),
48 | str(string),
49 | binary(list),
50 | db-null,
51 | }
52 |
53 | record column {
54 | name: string,
55 | data-type: db-data-type,
56 | }
57 |
58 | type row = list
59 |
60 | record row-set {
61 | columns: list,
62 | rows: list,
63 | }
64 |
--------------------------------------------------------------------------------
/src/Config.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace Fermyon.Spin.Sdk;
5 |
6 | [StructLayout(LayoutKind.Sequential)]
7 | internal unsafe readonly struct ConfigError {
8 | internal const byte SPIN_CONFIG_ERROR_PROVIDER = 0;
9 | internal const byte SPIN_CONFIG_ERROR_INVALID_KEY = 1;
10 | internal const byte SPIN_CONFIG_ERROR_INVALID_SCHEMA = 2;
11 | internal const byte SPIN_CONFIG_ERROR_OTHER = 3;
12 |
13 | internal readonly byte tag;
14 | internal readonly InteropString message;
15 | }
16 |
17 | [StructLayout(LayoutKind.Explicit)]
18 | internal unsafe readonly struct ConfigResultValue {
19 | [FieldOffset(0)]
20 | internal readonly InteropString ok;
21 | [FieldOffset(0)]
22 | internal readonly ConfigError err;
23 | }
24 |
25 | [StructLayout(LayoutKind.Sequential)]
26 | internal unsafe readonly struct ConfigResult {
27 | internal readonly byte is_err;
28 | internal readonly ConfigResultValue val;
29 | }
30 |
31 | internal static class SpinConfigNative
32 | {
33 | [MethodImpl(MethodImplOptions.InternalCall)]
34 | internal static extern unsafe void spin_config_get_config(ref InteropString key, out ConfigResult ret0);
35 | }
36 |
--------------------------------------------------------------------------------
/src/native/outbound-redis.h:
--------------------------------------------------------------------------------
1 | #ifndef __BINDINGS_OUTBOUND_REDIS_H
2 | #define __BINDINGS_OUTBOUND_REDIS_H
3 | #ifdef __cplusplus
4 | extern "C"
5 | {
6 | #endif
7 |
8 | #include
9 | #include
10 |
11 | typedef struct {
12 | char *ptr;
13 | size_t len;
14 | } outbound_redis_string_t;
15 |
16 | void outbound_redis_string_set(outbound_redis_string_t *ret, const char *s);
17 | void outbound_redis_string_dup(outbound_redis_string_t *ret, const char *s);
18 | void outbound_redis_string_free(outbound_redis_string_t *ret);
19 | typedef uint8_t outbound_redis_error_t;
20 | #define OUTBOUND_REDIS_ERROR_SUCCESS 0
21 | #define OUTBOUND_REDIS_ERROR_ERROR 1
22 | typedef struct {
23 | uint8_t *ptr;
24 | size_t len;
25 | } outbound_redis_payload_t;
26 | void outbound_redis_payload_free(outbound_redis_payload_t *ptr);
27 | outbound_redis_error_t outbound_redis_publish(outbound_redis_string_t *address, outbound_redis_string_t *channel, outbound_redis_payload_t *payload);
28 | outbound_redis_error_t outbound_redis_get(outbound_redis_string_t *address, outbound_redis_string_t *key, outbound_redis_payload_t *ret0);
29 | outbound_redis_error_t outbound_redis_set(outbound_redis_string_t *address, outbound_redis_string_t *key, outbound_redis_payload_t *value);
30 | #ifdef __cplusplus
31 | }
32 | #endif
33 | #endif
34 |
--------------------------------------------------------------------------------
/src/native/util.h:
--------------------------------------------------------------------------------
1 | #ifndef __UTIL_H
2 | #define __UTIL_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | typedef uint8_t get_member_err_t;
16 |
17 | #define GET_MEMBER_ERR_OK 0
18 | #define GET_MEMBER_ERR_NOT_FOUND 1
19 | #define GET_MEMBER_ERR_EXCEPTION 2
20 | #define GET_MEMBER_ERR_WRITE_ONLY 3
21 |
22 | get_member_err_t get_property(MonoObject* instance, const char* name, MonoObject** result);
23 |
24 | typedef uint8_t resolve_err_t;
25 |
26 | #define RESOLVE_ERR_OK 0
27 | #define RESOLVE_ERR_NO_MATCH 1
28 | #define RESOLVE_ERR_IMAGE_NOT_RESOLVED 2
29 | #define RESOLVE_ERR_TYPEDEF_TABLE_NOT_RESOLVED 3
30 |
31 | resolve_err_t find_decorated_method(MonoAssembly* assembly, const char* attr_name, MonoObject** attr_obj, MonoMethod** decorated_method);
32 |
33 | typedef uint8_t entry_points_err_t;
34 |
35 | #define EP_ERR_OK 0
36 | #define EP_ERR_NO_ENTRY_ASSEMBLY 1
37 | #define EP_ERR_NO_HANDLER_METHOD 2
38 |
39 | entry_points_err_t find_entry_points(const char* attr_name, MonoObject** attr_obj, MonoMethod** handler);
40 |
41 | #endif
42 |
--------------------------------------------------------------------------------
/src/ConfigInterop.cs:
--------------------------------------------------------------------------------
1 | namespace Fermyon.Spin.Sdk;
2 |
3 | ///
4 | /// Gets component configuration values from Spin.
5 | ///
6 | public static class SpinConfig
7 | {
8 | ///
9 | /// Gets the component configuration value identified by the key.
10 | ///
11 | public static string Get(string key)
12 | {
13 | var interopKey = InteropString.FromString(key);
14 | SpinConfigNative.spin_config_get_config(ref interopKey, out var result);
15 |
16 | if (result.is_err == 0)
17 | {
18 | return result.val.ok.ToString();
19 | }
20 | else
21 | {
22 | var message = result.val.err.message.ToString();
23 | Exception ex = result.val.err.tag switch {
24 | ConfigError.SPIN_CONFIG_ERROR_PROVIDER => new InvalidOperationException(message),
25 | ConfigError.SPIN_CONFIG_ERROR_INVALID_KEY => new ArgumentException($"Incorrect key syntax: {message}", nameof(key)),
26 | ConfigError.SPIN_CONFIG_ERROR_INVALID_SCHEMA => new InvalidOperationException($"Incorrect app configuration: {message}"),
27 | ConfigError.SPIN_CONFIG_ERROR_OTHER => new InvalidOperationException(message),
28 | _ => new Exception("Unknown error from Spin configuration service"),
29 | };
30 | throw ex;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Common/PostgresExtensions.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | namespace Fermyon.PetStore.Common;
4 |
5 | public static class PostgresExtensions
6 | {
7 | public static int AsInt32(this DbValue value)
8 | {
9 | return value.Value() switch
10 | {
11 | null => throw new InvalidOperationException("value is null"),
12 | var v => (int)v,
13 | };
14 | }
15 |
16 | public static int? AsNullableInt32(this DbValue value)
17 | {
18 | return value.Value() switch
19 | {
20 | null => null,
21 | var v => (int)v,
22 | };
23 | }
24 |
25 | public static string AsString(this DbValue value)
26 | {
27 | return value.Value() switch
28 | {
29 | null => throw new InvalidOperationException("value is null"),
30 | var v => (string)v,
31 | };
32 | }
33 |
34 | public static Buffer AsBuffer(this DbValue value)
35 | {
36 | return value.Value() switch
37 | {
38 | null => throw new InvalidOperationException("value is null"),
39 | var v => (Buffer)v,
40 | };
41 | }
42 |
43 | public static Buffer? AsNullableBuffer(this DbValue value)
44 | {
45 | return value.Value() switch
46 | {
47 | null => null,
48 | var v => (Buffer)v,
49 | };
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/native/host-components.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #include "host-components.h"
15 | #include "wasi-outbound-http.h"
16 | #include "outbound-redis.h"
17 | #include "outbound-pg.h"
18 | #include "spin-config.h"
19 |
20 | void spin_attach_internal_calls()
21 | {
22 | mono_add_internal_call("Fermyon.Spin.Sdk.OutboundHttpInterop::wasi_outbound_http_request", wasi_outbound_http_request);
23 |
24 | mono_add_internal_call("Fermyon.Spin.Sdk.OutboundRedisInterop::outbound_redis_get", outbound_redis_get);
25 | mono_add_internal_call("Fermyon.Spin.Sdk.OutboundRedisInterop::outbound_redis_set", outbound_redis_set);
26 | mono_add_internal_call("Fermyon.Spin.Sdk.OutboundRedisInterop::outbound_redis_publish", outbound_redis_publish);
27 |
28 | mono_add_internal_call("Fermyon.Spin.Sdk.OutboundPgInterop::outbound_pg_query", outbound_pg_query);
29 | mono_add_internal_call("Fermyon.Spin.Sdk.OutboundPgInterop::outbound_pg_execute", outbound_pg_execute);
30 |
31 | mono_add_internal_call("Fermyon.Spin.Sdk.SpinConfigNative::spin_config_get_config", spin_config_get_config);
32 | }
33 |
--------------------------------------------------------------------------------
/src/native/spin-config.h:
--------------------------------------------------------------------------------
1 | #ifndef __BINDINGS_SPIN_CONFIG_H
2 | #define __BINDINGS_SPIN_CONFIG_H
3 | #ifdef __cplusplus
4 | extern "C"
5 | {
6 | #endif
7 |
8 | #include
9 | #include
10 |
11 | typedef struct {
12 | char *ptr;
13 | size_t len;
14 | } spin_config_string_t;
15 |
16 | void spin_config_string_set(spin_config_string_t *ret, const char *s);
17 | void spin_config_string_dup(spin_config_string_t *ret, const char *s);
18 | void spin_config_string_free(spin_config_string_t *ret);
19 | typedef struct {
20 | uint8_t tag;
21 | union {
22 | spin_config_string_t provider;
23 | spin_config_string_t invalid_key;
24 | spin_config_string_t invalid_schema;
25 | spin_config_string_t other;
26 | } val;
27 | } spin_config_error_t;
28 | #define SPIN_CONFIG_ERROR_PROVIDER 0
29 | #define SPIN_CONFIG_ERROR_INVALID_KEY 1
30 | #define SPIN_CONFIG_ERROR_INVALID_SCHEMA 2
31 | #define SPIN_CONFIG_ERROR_OTHER 3
32 | void spin_config_error_free(spin_config_error_t *ptr);
33 | typedef struct {
34 | bool is_err;
35 | union {
36 | spin_config_string_t ok;
37 | spin_config_error_t err;
38 | } val;
39 | } spin_config_expected_string_error_t;
40 | void spin_config_expected_string_error_free(spin_config_expected_string_error_t *ptr);
41 | void spin_config_get_config(spin_config_string_t *key, spin_config_expected_string_error_t *ret0);
42 | #ifdef __cplusplus
43 | }
44 | #endif
45 | #endif
46 |
--------------------------------------------------------------------------------
/wit/ephemeral/http-types.wit:
--------------------------------------------------------------------------------
1 | // This is a temporary workaround very similar to https://github.com/deislabs/wasi-experimental-http.
2 | // Once asynchronous functions, streams, and the upstream HTTP API are available, this should be removed.
3 |
4 | // The HTTP status code.
5 | // This is currently an unsigned 16-bit integer,
6 | // but it could be represented as an enum containing
7 | // all possible HTTP status codes.
8 | type http-status = u16
9 |
10 | // The HTTP body.
11 | // Currently, this is a synchonous byte array, but it should be
12 | // possible to have a stream for both request and response bodies.
13 | type body = list
14 |
15 | // The HTTP headers represented as a list of (name, value) pairs.
16 | type headers = list>
17 |
18 | // The HTTP parameter queries, represented as a list of (name, value) pairs.
19 | type params = list>
20 |
21 | // The HTTP URI of the current request.
22 | type uri = string
23 |
24 | // The HTTP method.
25 | enum method {
26 | get,
27 | post,
28 | put,
29 | delete,
30 | patch,
31 | head,
32 | options,
33 | }
34 |
35 | // An HTTP request.
36 | record request {
37 | method: method,
38 | uri: uri,
39 | headers: headers,
40 | params: params,
41 | body: option,
42 | }
43 |
44 | // An HTTP response.
45 | record response {
46 | status: http-status,
47 | headers: option,
48 | body: option,
49 | }
50 |
51 | // HTTP errors returned by the runtime.
52 | enum http-error {
53 | success,
54 | destination-not-allowed,
55 | invalid-url,
56 | request-error,
57 | runtime-error,
58 | }
59 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Pets/Handler.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | using Fermyon.PetStore.Common;
4 |
5 | namespace Fermyon.PetStore.Pets;
6 |
7 | public static class Handler
8 | {
9 | [HttpHandler]
10 | public static HttpResponse HandleHttpRequest(HttpRequest request)
11 | {
12 | var html = request.IsRuntime() ?
13 | PageContent() :
14 | String.Empty;
15 |
16 | return new HttpResponse
17 | {
18 | StatusCode = System.Net.HttpStatusCode.OK,
19 | Headers = new Dictionary
20 | {
21 | { "Content-Type", "text/html" },
22 | },
23 | BodyAsString = html,
24 | };
25 | }
26 |
27 | private static string PageContent()
28 | {
29 | var template = File.ReadAllText("/assets/PetsTemplate.html");
30 | return template.Replace("{{ db-table }}", DatabaseTableHtml());
31 | }
32 |
33 | private static string DatabaseTableHtml()
34 | {
35 | var connectionString = Configuration.DbConnectionString();
36 |
37 | var rows = PostgresOutbound.Query(connectionString, "SELECT id, name FROM pets ORDER BY name").Rows;
38 |
39 | if (rows.Count == 0)
40 | {
41 | return "No pets registered.
$"| {r[1].AsString()} |
");
46 | var tbody = String.Join("\n", trs);
47 | var table = $"";
48 | return table;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/build/Fermyon.Spin.Sdk.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Toys/Handler.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | using Fermyon.PetStore.Common;
4 |
5 | namespace Fermyon.PetStore.Toys;
6 |
7 | public static class Handler
8 | {
9 | [HttpHandler]
10 | public static HttpResponse HandleHttpRequest(HttpRequest request)
11 | {
12 | var html = request.IsRuntime() ?
13 | PageContent() :
14 | String.Empty;
15 |
16 | return new HttpResponse
17 | {
18 | StatusCode = System.Net.HttpStatusCode.OK,
19 | Headers = new Dictionary
20 | {
21 | { "Content-Type", "text/html" },
22 | },
23 | BodyAsString = html,
24 | };
25 | }
26 |
27 | private static string PageContent()
28 | {
29 | var template = File.ReadAllText("/assets/ToysTemplate.html");
30 | return template.Replace("{{ db-table }}", DatabaseTableHtml());
31 | }
32 |
33 | private static string DatabaseTableHtml()
34 | {
35 | var connectionString = Configuration.DbConnectionString();
36 |
37 | var rows = PostgresOutbound.Query(connectionString, "SELECT toys.id, toys.description, toys.count, toys.owner_id, pets.name FROM toys INNER JOIN pets ON pets.id = toys.owner_id ORDER BY toys.description").Rows;
38 |
39 | if (rows.Count == 0)
40 | {
41 | return "No toys registered.
$"| {r[1].AsString()} | {CountText(r[2].AsNullableInt32())} | {r[4].AsString()} |
");
46 | var tbody = String.Join("\n", trs);
47 | var table = $"\n| Toy | Count | Owner | \n{tbody}\n
";
48 | return table;
49 | }
50 | }
51 |
52 | private static string CountText(int? count)
53 | {
54 | return count switch {
55 | null => "Unknown",
56 | int n => n.ToString(),
57 | };
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/RedisOutbound.cs:
--------------------------------------------------------------------------------
1 | namespace Fermyon.Spin.Sdk;
2 |
3 | ///
4 | /// Performs operations on a Redis store.
5 | ///
6 | public static class RedisOutbound
7 | {
8 | ///
9 | /// Gets the value of a key.
10 | ///
11 | public static Buffer Get(string address, string key)
12 | {
13 | var res = new Buffer();
14 | var redisAddress = InteropString.FromString(address);
15 | var redisKey = InteropString.FromString(key);
16 |
17 | var err = OutboundRedisInterop.outbound_redis_get(ref redisAddress, ref redisKey, ref res);
18 | if (err == 0 || err == 255)
19 | {
20 | return res;
21 | }
22 | else
23 | {
24 | throw new Exception("Redis outbound error: cannot GET.");
25 | }
26 | }
27 |
28 | ///
29 | /// Sets the value of a key. If the key already holds a value, it is overwritten.
30 | ///
31 | public static void Set(string address, string key, Buffer payload)
32 | {
33 | var redisAddress = InteropString.FromString(address);
34 | var redisKey = InteropString.FromString(key);
35 |
36 | var err = OutboundRedisInterop.outbound_redis_set(ref redisAddress, ref redisKey, ref payload);
37 | if (err == 0 || err == 255)
38 | {
39 | return;
40 | }
41 | else
42 | {
43 | throw new Exception("Redis outbound error: cannot SET.");
44 | }
45 | }
46 |
47 | ///
48 | /// Publishes a Redis message to the specified channel.
49 | ///
50 | public static void Publish(string address, string channel, Buffer payload)
51 | {
52 | var redisAddress = InteropString.FromString(address);
53 | var redisChannel = InteropString.FromString(channel);
54 |
55 | var err = OutboundRedisInterop.outbound_redis_publish(ref redisAddress, ref redisChannel, ref payload);
56 | if (err == 0 || err == 255)
57 | {
58 | return;
59 | }
60 | else
61 | {
62 | throw new Exception("Redis outbound error: cannot PUBLISH.");
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/PostgresOutbound.cs:
--------------------------------------------------------------------------------
1 | namespace Fermyon.Spin.Sdk;
2 |
3 | ///
4 | /// Performs operations on a Postgres database.
5 | ///
6 | public static class PostgresOutbound
7 | {
8 | ///
9 | /// Performs a query against a Postgres database.
10 | ///
11 | // TODO: less foul return type like an IDataReader or something
12 | public static PgRowSet Query(string connectionString, string sql, params object?[] parameters)
13 | {
14 | var conn = InteropString.FromString(connectionString);
15 | var stmt = InteropString.FromString(sql);
16 | var parms = InteropList.From(parameters.Select(p => ParameterValue.From(p)).ToArray());
17 | var result = new PgRowSetOrError();
18 |
19 | OutboundPgInterop.outbound_pg_query(ref conn, ref stmt, ref parms, ref result);
20 |
21 | if (result.is_err == 0)
22 | {
23 | return result.value;
24 | }
25 | else
26 | {
27 | var err = result.err;
28 | throw new Exception($"Postgres query error: interop error {err.tag}: {err.message.ToString()}");
29 | }
30 | }
31 |
32 | ///
33 | /// Executes a SQL statement against a Postgres database, and returns the number of rows changed. This
34 | /// is for statements that do not return a result set.
35 | ///
36 | public static long Execute(string connectionString, string sql, params object?[] parameters)
37 | {
38 | var conn = InteropString.FromString(connectionString);
39 | var stmt = InteropString.FromString(sql);
40 | var parms = InteropList.From(parameters.Select(p => ParameterValue.From(p)).ToArray());
41 | var result = new PgU64OrError();
42 |
43 | OutboundPgInterop.outbound_pg_execute(ref conn, ref stmt, ref parms, ref result);
44 |
45 | if (result.is_err == 0)
46 | {
47 | return (long)(result.value);
48 | }
49 | else
50 | {
51 | var err = result.err;
52 | throw new Exception($"Postgres execute error: interop error {err.tag}: {err.message.ToString()}");
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Fermyon.Spin.Sdk.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32611.2
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fermyon.Spin.Sdk", "src\Fermyon.Spin.Sdk.csproj", "{004286A2-C8EE-415E-AA1C-3C7163DFC74B}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloWorld.Spin", "samples\hello-world\HelloWorld.Spin.csproj", "{98E19421-DCD0-4AB5-88FB-9009B80EEE7D}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{FD170508-0B77-4CEC-9578-A526D529D5B7}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{404EC7BB-F8A8-4766-B273-30BCA615BB40}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {004286A2-C8EE-415E-AA1C-3C7163DFC74B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {004286A2-C8EE-415E-AA1C-3C7163DFC74B}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {004286A2-C8EE-415E-AA1C-3C7163DFC74B}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {004286A2-C8EE-415E-AA1C-3C7163DFC74B}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {98E19421-DCD0-4AB5-88FB-9009B80EEE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {98E19421-DCD0-4AB5-88FB-9009B80EEE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {98E19421-DCD0-4AB5-88FB-9009B80EEE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {98E19421-DCD0-4AB5-88FB-9009B80EEE7D}.Release|Any CPU.Build.0 = Release|Any CPU
28 | EndGlobalSection
29 | GlobalSection(SolutionProperties) = preSolution
30 | HideSolutionNode = FALSE
31 | EndGlobalSection
32 | GlobalSection(NestedProjects) = preSolution
33 | {004286A2-C8EE-415E-AA1C-3C7163DFC74B} = {404EC7BB-F8A8-4766-B273-30BCA615BB40}
34 | {98E19421-DCD0-4AB5-88FB-9009B80EEE7D} = {FD170508-0B77-4CEC-9578-A526D529D5B7}
35 | EndGlobalSection
36 | GlobalSection(ExtensibilityGlobals) = postSolution
37 | SolutionGuid = {D0187C07-244A-471B-B972-9E7F0389CB6E}
38 | EndGlobalSection
39 | EndGlobal
40 |
--------------------------------------------------------------------------------
/src/native/spin-http.h:
--------------------------------------------------------------------------------
1 | #ifndef __BINDINGS_SPIN_HTTP_H
2 | #define __BINDINGS_SPIN_HTTP_H
3 | #ifdef __cplusplus
4 | extern "C"
5 | {
6 | #endif
7 |
8 | #include
9 | #include
10 |
11 | typedef struct {
12 | char *ptr;
13 | size_t len;
14 | } spin_http_string_t;
15 |
16 | void spin_http_string_set(spin_http_string_t *ret, const char *s);
17 | void spin_http_string_dup(spin_http_string_t *ret, const char *s);
18 | void spin_http_string_free(spin_http_string_t *ret);
19 | typedef struct {
20 | uint8_t *ptr;
21 | size_t len;
22 | } spin_http_body_t;
23 | void spin_http_body_free(spin_http_body_t *ptr);
24 | typedef struct {
25 | spin_http_string_t f0;
26 | spin_http_string_t f1;
27 | } spin_http_tuple2_string_string_t;
28 | void spin_http_tuple2_string_string_free(spin_http_tuple2_string_string_t *ptr);
29 | typedef struct {
30 | spin_http_tuple2_string_string_t *ptr;
31 | size_t len;
32 | } spin_http_headers_t;
33 | void spin_http_headers_free(spin_http_headers_t *ptr);
34 | typedef uint8_t spin_http_http_error_t;
35 | #define SPIN_HTTP_HTTP_ERROR_SUCCESS 0
36 | #define SPIN_HTTP_HTTP_ERROR_DESTINATION_NOT_ALLOWED 1
37 | #define SPIN_HTTP_HTTP_ERROR_INVALID_URL 2
38 | #define SPIN_HTTP_HTTP_ERROR_REQUEST_ERROR 3
39 | #define SPIN_HTTP_HTTP_ERROR_RUNTIME_ERROR 4
40 | typedef uint16_t spin_http_http_status_t;
41 | typedef uint8_t spin_http_method_t;
42 | #define SPIN_HTTP_METHOD_GET 0
43 | #define SPIN_HTTP_METHOD_POST 1
44 | #define SPIN_HTTP_METHOD_PUT 2
45 | #define SPIN_HTTP_METHOD_DELETE 3
46 | #define SPIN_HTTP_METHOD_PATCH 4
47 | #define SPIN_HTTP_METHOD_HEAD 5
48 | #define SPIN_HTTP_METHOD_OPTIONS 6
49 | typedef struct {
50 | spin_http_tuple2_string_string_t *ptr;
51 | size_t len;
52 | } spin_http_params_t;
53 | void spin_http_params_free(spin_http_params_t *ptr);
54 | typedef spin_http_string_t spin_http_uri_t;
55 | void spin_http_uri_free(spin_http_uri_t *ptr);
56 | typedef struct {
57 | bool is_some;
58 | spin_http_body_t val;
59 | } spin_http_option_body_t;
60 | void spin_http_option_body_free(spin_http_option_body_t *ptr);
61 | typedef struct {
62 | spin_http_method_t method;
63 | spin_http_uri_t uri;
64 | spin_http_headers_t headers;
65 | spin_http_params_t params;
66 | spin_http_option_body_t body;
67 | } spin_http_request_t;
68 | void spin_http_request_free(spin_http_request_t *ptr);
69 | typedef struct {
70 | bool is_some;
71 | spin_http_headers_t val;
72 | } spin_http_option_headers_t;
73 | void spin_http_option_headers_free(spin_http_option_headers_t *ptr);
74 | typedef struct {
75 | spin_http_http_status_t status;
76 | spin_http_option_headers_t headers;
77 | spin_http_option_body_t body;
78 | } spin_http_response_t;
79 | void spin_http_response_free(spin_http_response_t *ptr);
80 | void spin_http_handle_http_request(spin_http_request_t *req, spin_http_response_t *ret0);
81 | #ifdef __cplusplus
82 | }
83 | #endif
84 | #endif
85 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewPet/Handler.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Headers;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Http.Features;
4 |
5 | using Fermyon.Spin.Sdk;
6 | using HttpRequest = Fermyon.Spin.Sdk.HttpRequest;
7 | using HttpResponse = Fermyon.Spin.Sdk.HttpResponse;
8 |
9 | using Fermyon.PetStore.Common;
10 |
11 | namespace Fermyon.PetStore.NewPet;
12 |
13 | public static class Handler
14 | {
15 | [HttpHandler]
16 | public static HttpResponse HandleHttpRequest(HttpRequest request)
17 | {
18 | return request.Method switch {
19 | Fermyon.Spin.Sdk.HttpMethod.Get => ShowNewPetForm(request),
20 | Fermyon.Spin.Sdk.HttpMethod.Post => HandleNewPetForm(request),
21 | _ => new HttpResponse { StatusCode = System.Net.HttpStatusCode.MethodNotAllowed },
22 | };
23 | }
24 |
25 | private static HttpResponse ShowNewPetForm(HttpRequest request)
26 | {
27 | var html = request.IsRuntime() ?
28 | File.ReadAllText("/assets/NewPet.html") :
29 | String.Empty;
30 |
31 | return new HttpResponse
32 | {
33 | StatusCode = System.Net.HttpStatusCode.OK,
34 | Headers = new Dictionary
35 | {
36 | { "Content-Type", "text/html" },
37 | },
38 | BodyAsString = html,
39 | };
40 | }
41 |
42 | private static HttpResponse HandleNewPetForm(HttpRequest request)
43 | {
44 | var ctx = FormParser.ParsePostedForm(request);
45 |
46 | var name = ctx.Request.Form["submitted-name"].ToString();
47 | var pictureFile = ctx.Request.Form.Files["submitted-picture"];
48 |
49 | if (string.IsNullOrWhiteSpace(name) || pictureFile == null || pictureFile.Length == 0)
50 | {
51 | return new HttpResponse
52 | {
53 | StatusCode = System.Net.HttpStatusCode.OK,
54 | Headers = new Dictionary
55 | {
56 | { "Content-Type", "text/html" },
57 | },
58 | BodyAsString = File.ReadAllText("/assets/NewPetMissingField.html"),
59 | };
60 | }
61 |
62 | using var memStream = new MemoryStream((int)(pictureFile.Length));
63 | pictureFile.CopyTo(memStream);
64 | var pictureData = memStream.ToArray();
65 |
66 | var connectionString = Configuration.DbConnectionString();
67 |
68 | var maxIdResults = PostgresOutbound.Query(connectionString, "SELECT MAX(id) FROM pets");
69 | var maxId = maxIdResults.Rows.First()[0].AsNullableInt32();
70 |
71 | var id = (maxId ?? 0) + 1;
72 | PostgresOutbound.Execute(
73 | connectionString,
74 | "INSERT INTO pets (id, name, picture) VALUES ($1, $2, $3)",
75 | id,
76 | name,
77 | pictureData
78 | );
79 |
80 | return new HttpResponse
81 | {
82 | StatusCode = System.Net.HttpStatusCode.OK,
83 | Headers = new Dictionary
84 | {
85 | { "Content-Type", "text/html" },
86 | },
87 | BodyAsString = File.ReadAllText("/assets/NewPetCreated.html"),
88 | };
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/native/wasi-outbound-http.h:
--------------------------------------------------------------------------------
1 | #ifndef __BINDINGS_WASI_OUTBOUND_HTTP_H
2 | #define __BINDINGS_WASI_OUTBOUND_HTTP_H
3 | #ifdef __cplusplus
4 | extern "C"
5 | {
6 | #endif
7 |
8 | #include
9 | #include
10 |
11 | typedef struct {
12 | char *ptr;
13 | size_t len;
14 | } wasi_outbound_http_string_t;
15 |
16 | void wasi_outbound_http_string_set(wasi_outbound_http_string_t *ret, const char *s);
17 | void wasi_outbound_http_string_dup(wasi_outbound_http_string_t *ret, const char *s);
18 | void wasi_outbound_http_string_free(wasi_outbound_http_string_t *ret);
19 | typedef struct {
20 | uint8_t *ptr;
21 | size_t len;
22 | } wasi_outbound_http_body_t;
23 | void wasi_outbound_http_body_free(wasi_outbound_http_body_t *ptr);
24 | typedef struct {
25 | wasi_outbound_http_string_t f0;
26 | wasi_outbound_http_string_t f1;
27 | } wasi_outbound_http_tuple2_string_string_t;
28 | void wasi_outbound_http_tuple2_string_string_free(wasi_outbound_http_tuple2_string_string_t *ptr);
29 | typedef struct {
30 | wasi_outbound_http_tuple2_string_string_t *ptr;
31 | size_t len;
32 | } wasi_outbound_http_headers_t;
33 | void wasi_outbound_http_headers_free(wasi_outbound_http_headers_t *ptr);
34 | typedef uint8_t wasi_outbound_http_http_error_t;
35 | #define WASI_OUTBOUND_HTTP_HTTP_ERROR_SUCCESS 0
36 | #define WASI_OUTBOUND_HTTP_HTTP_ERROR_DESTINATION_NOT_ALLOWED 1
37 | #define WASI_OUTBOUND_HTTP_HTTP_ERROR_INVALID_URL 2
38 | #define WASI_OUTBOUND_HTTP_HTTP_ERROR_REQUEST_ERROR 3
39 | #define WASI_OUTBOUND_HTTP_HTTP_ERROR_RUNTIME_ERROR 4
40 | typedef uint16_t wasi_outbound_http_http_status_t;
41 | typedef uint8_t wasi_outbound_http_method_t;
42 | #define WASI_OUTBOUND_HTTP_METHOD_GET 0
43 | #define WASI_OUTBOUND_HTTP_METHOD_POST 1
44 | #define WASI_OUTBOUND_HTTP_METHOD_PUT 2
45 | #define WASI_OUTBOUND_HTTP_METHOD_DELETE 3
46 | #define WASI_OUTBOUND_HTTP_METHOD_PATCH 4
47 | #define WASI_OUTBOUND_HTTP_METHOD_HEAD 5
48 | #define WASI_OUTBOUND_HTTP_METHOD_OPTIONS 6
49 | typedef struct {
50 | wasi_outbound_http_tuple2_string_string_t *ptr;
51 | size_t len;
52 | } wasi_outbound_http_params_t;
53 | void wasi_outbound_http_params_free(wasi_outbound_http_params_t *ptr);
54 | typedef wasi_outbound_http_string_t wasi_outbound_http_uri_t;
55 | void wasi_outbound_http_uri_free(wasi_outbound_http_uri_t *ptr);
56 | typedef struct {
57 | bool is_some;
58 | wasi_outbound_http_body_t val;
59 | } wasi_outbound_http_option_body_t;
60 | void wasi_outbound_http_option_body_free(wasi_outbound_http_option_body_t *ptr);
61 | typedef struct {
62 | wasi_outbound_http_method_t method;
63 | wasi_outbound_http_uri_t uri;
64 | wasi_outbound_http_headers_t headers;
65 | wasi_outbound_http_params_t params;
66 | wasi_outbound_http_option_body_t body;
67 | } wasi_outbound_http_request_t;
68 | void wasi_outbound_http_request_free(wasi_outbound_http_request_t *ptr);
69 | typedef struct {
70 | bool is_some;
71 | wasi_outbound_http_headers_t val;
72 | } wasi_outbound_http_option_headers_t;
73 | void wasi_outbound_http_option_headers_free(wasi_outbound_http_option_headers_t *ptr);
74 | typedef struct {
75 | wasi_outbound_http_http_status_t status;
76 | wasi_outbound_http_option_headers_t headers;
77 | wasi_outbound_http_option_body_t body;
78 | } wasi_outbound_http_response_t;
79 | void wasi_outbound_http_response_free(wasi_outbound_http_response_t *ptr);
80 | wasi_outbound_http_http_error_t wasi_outbound_http_request(wasi_outbound_http_request_t *req, wasi_outbound_http_response_t *ret0);
81 | #ifdef __cplusplus
82 | }
83 | #endif
84 | #endif
85 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Pet/Handler.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | using Fermyon.PetStore.Common;
4 |
5 | namespace Fermyon.PetStore.Pet;
6 |
7 | public static class Handler
8 | {
9 | [HttpHandler]
10 | public static HttpResponse HandleHttpRequest(HttpRequest request)
11 | {
12 | if (!request.IsRuntime())
13 | {
14 | return new HttpResponse
15 | {
16 | StatusCode = System.Net.HttpStatusCode.OK,
17 | Headers = new Dictionary
18 | {
19 | { "Content-Type", "text/html" },
20 | },
21 | BodyAsString = String.Empty,
22 | };
23 | }
24 |
25 | var pathElements = request.Headers["spin-path-info"].Split('/', StringSplitOptions.RemoveEmptyEntries);
26 | return pathElements switch {
27 | [var id] => PetPage(id),
28 | [var id, "picture"] => PetPicture(id),
29 | _ => GenericResponse.NotFound(),
30 | };
31 | }
32 |
33 | public static HttpResponse PetPage(string idHeader)
34 | {
35 | if (int.TryParse(idHeader, out var id))
36 | {
37 | var connectionString = Configuration.DbConnectionString();
38 |
39 | var rows = PostgresOutbound.Query(connectionString, "SELECT id, name FROM pets WHERE id = $1", id).Rows;
40 |
41 | if (rows.Count == 0)
42 | {
43 | return PetNotFound();
44 | }
45 |
46 | var name = rows[0][1].AsString();
47 |
48 | var template = File.ReadAllText("/assets/PetTemplate.html");
49 |
50 | var responseText = template
51 | .Replace("{{ name }}", name)
52 | .Replace("{{ id }}", id.ToString());
53 |
54 | return new HttpResponse
55 | {
56 | StatusCode = System.Net.HttpStatusCode.OK,
57 | Headers = new Dictionary
58 | {
59 | { "Content-Type", "text/html" },
60 | },
61 | BodyAsString = responseText,
62 | };
63 | }
64 | return PetNotFound();
65 | }
66 |
67 | public static HttpResponse PetPicture(string idHeader)
68 | {
69 | if (int.TryParse(idHeader, out var id))
70 | {
71 | var connectionString = Configuration.DbConnectionString();
72 |
73 | var rows = PostgresOutbound.Query(connectionString, "SELECT id, picture FROM pets WHERE id = $1", id).Rows;
74 |
75 | if (rows.Count == 0)
76 | {
77 | return PetNotFound();
78 | }
79 |
80 | var picture = rows[0][1].AsBuffer();
81 |
82 | return new HttpResponse
83 | {
84 | StatusCode = System.Net.HttpStatusCode.OK,
85 | Headers = new Dictionary
86 | {
87 | { "Content-Type", "image/*" },
88 | },
89 | BodyAsBytes = picture,
90 | };
91 | }
92 | return PetNotFound();
93 | }
94 |
95 | public static HttpResponse PetNotFound()
96 | {
97 | return new HttpResponse
98 | {
99 | StatusCode = System.Net.HttpStatusCode.NotFound,
100 | Headers = new Dictionary
101 | {
102 | { "Content-Type", "text/html" },
103 | },
104 | BodyAsString = File.ReadAllText("/assets/PetNotFound.html"),
105 | };
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/native/spin-config.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include "spin-config.h"
3 |
4 | __attribute__((weak, export_name("canonical_abi_realloc")))
5 | void *canonical_abi_realloc(
6 | void *ptr,
7 | size_t orig_size,
8 | size_t org_align,
9 | size_t new_size
10 | ) {
11 | void *ret = realloc(ptr, new_size);
12 | if (!ret)
13 | abort();
14 | return ret;
15 | }
16 |
17 | __attribute__((weak, export_name("canonical_abi_free")))
18 | void canonical_abi_free(
19 | void *ptr,
20 | size_t size,
21 | size_t align
22 | ) {
23 | free(ptr);
24 | }
25 | #include
26 |
27 | void spin_config_string_set(spin_config_string_t *ret, const char *s) {
28 | ret->ptr = (char*) s;
29 | ret->len = strlen(s);
30 | }
31 |
32 | void spin_config_string_dup(spin_config_string_t *ret, const char *s) {
33 | ret->len = strlen(s);
34 | ret->ptr = canonical_abi_realloc(NULL, 0, 1, ret->len);
35 | memcpy(ret->ptr, s, ret->len);
36 | }
37 |
38 | void spin_config_string_free(spin_config_string_t *ret) {
39 | canonical_abi_free(ret->ptr, ret->len, 1);
40 | ret->ptr = NULL;
41 | ret->len = 0;
42 | }
43 | void spin_config_error_free(spin_config_error_t *ptr) {
44 | switch ((int32_t) ptr->tag) {
45 | case 0: {
46 | spin_config_string_free(&ptr->val.provider);
47 | break;
48 | }
49 | case 1: {
50 | spin_config_string_free(&ptr->val.invalid_key);
51 | break;
52 | }
53 | case 2: {
54 | spin_config_string_free(&ptr->val.invalid_schema);
55 | break;
56 | }
57 | case 3: {
58 | spin_config_string_free(&ptr->val.other);
59 | break;
60 | }
61 | }
62 | }
63 | void spin_config_expected_string_error_free(spin_config_expected_string_error_t *ptr) {
64 | if (!ptr->is_err) {
65 | spin_config_string_free(&ptr->val.ok);
66 | } else {
67 | spin_config_error_free(&ptr->val.err);
68 | }
69 | }
70 |
71 | __attribute__((aligned(4)))
72 | static uint8_t RET_AREA[16];
73 | __attribute__((import_module("spin-config"), import_name("get-config")))
74 | void __wasm_import_spin_config_get_config(int32_t, int32_t, int32_t);
75 | void spin_config_get_config(spin_config_string_t *key, spin_config_expected_string_error_t *ret0) {
76 | int32_t ptr = (int32_t) &RET_AREA;
77 | __wasm_import_spin_config_get_config((int32_t) (*key).ptr, (int32_t) (*key).len, ptr);
78 | spin_config_expected_string_error_t expected;
79 | switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
80 | case 0: {
81 | expected.is_err = false;
82 |
83 | expected.val.ok = (spin_config_string_t) { (char*)(*((int32_t*) (ptr + 4))), (size_t)(*((int32_t*) (ptr + 8))) };
84 | break;
85 | }
86 | case 1: {
87 | expected.is_err = true;
88 | spin_config_error_t variant;
89 | variant.tag = (int32_t) (*((uint8_t*) (ptr + 4)));
90 | switch ((int32_t) variant.tag) {
91 | case 0: {
92 | variant.val.provider = (spin_config_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
93 | break;
94 | }
95 | case 1: {
96 | variant.val.invalid_key = (spin_config_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
97 | break;
98 | }
99 | case 2: {
100 | variant.val.invalid_schema = (spin_config_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
101 | break;
102 | }
103 | case 3: {
104 | variant.val.other = (spin_config_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
105 | break;
106 | }
107 | }
108 |
109 | expected.val.err = variant;
110 | break;
111 | }
112 | }*ret0 = expected;
113 | }
114 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Common/FormParser.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.Http.Headers;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.AspNetCore.Http.Features;
7 |
8 | using HttpRequest = Fermyon.Spin.Sdk.HttpRequest;
9 | using HttpResponse = Fermyon.Spin.Sdk.HttpResponse;
10 |
11 | namespace Fermyon.PetStore.Common;
12 |
13 | public static class FormParser
14 | {
15 | public static HttpContext ParsePostedForm(HttpRequest request)
16 | {
17 | var hd = request.Headers.ToDictionary(
18 | kvp => kvp.Key,
19 | kvp => new Microsoft.Extensions.Primitives.StringValues(kvp.Value),
20 | StringComparer.InvariantCultureIgnoreCase
21 | );
22 |
23 | var requestContext = new RequestFeature(
24 | "POST",
25 | request.Url,
26 | new HeaderDictionary(hd),
27 | new MemoryStream(request.Body.AsBytes().ToArray())
28 | );
29 | var requestFeatures = new FeatureCollection();
30 | requestFeatures[typeof(IHttpRequestFeature)] = requestContext;
31 |
32 | var sp = new NullServiceProvider();
33 | var contextFactory = new DefaultHttpContextFactory(sp);
34 | var ctx = contextFactory.Create(requestFeatures);
35 |
36 | return ctx;
37 | }
38 | }
39 |
40 | public class RequestFeature : IHttpRequestFeature
41 | {
42 | public RequestFeature(string httpMethod, string url, IHeaderDictionary headers, Stream requestBody)
43 | {
44 | var queryStartPos = url.IndexOf('?');
45 | var path = queryStartPos < 0 ? url : url.Substring(0, queryStartPos);
46 | var query = queryStartPos < 0 ? string.Empty : url.Substring(queryStartPos);
47 |
48 | Method = httpMethod;
49 | Path = path;
50 | QueryString = query;
51 | Headers = headers;
52 | Body = requestBody;
53 | }
54 |
55 | public string Protocol { get; set; } = "HTTP/1.1";
56 | public string Scheme { get; set; } = "http";
57 | public string Method { get; set; } = "GET";
58 | public string PathBase { get; set; } = string.Empty;
59 | public string Path { get; set; } = "/";
60 | public string QueryString { get; set; } = string.Empty;
61 | public string RawTarget { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
62 | public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
63 | public Stream Body { get; set; } = default!;
64 | }
65 |
66 | internal class NullServiceScope : Microsoft.Extensions.DependencyInjection.IServiceScope
67 | {
68 | public NullServiceScope(IServiceProvider sp) { ServiceProvider = sp; }
69 | public IServiceProvider ServiceProvider { get; init; }
70 | public void Dispose() {}
71 | }
72 |
73 | internal class NullServiceScopeFactory : Microsoft.Extensions.DependencyInjection.IServiceScopeFactory
74 | {
75 | private readonly IServiceProvider _sp;
76 | public NullServiceScopeFactory(IServiceProvider sp) { _sp = sp; }
77 |
78 | public Microsoft.Extensions.DependencyInjection.IServiceScope CreateScope() =>
79 | new NullServiceScope(_sp);
80 | }
81 |
82 | internal class NullServiceProvider : IServiceProvider
83 | {
84 | public object? GetService(Type t)
85 | {
86 | if (t == typeof(Microsoft.Extensions.Options.IOptions))
87 | {
88 | return new Microsoft.Extensions.Options.OptionsWrapper(new Microsoft.AspNetCore.Http.Features.FormOptions());
89 | }
90 | if (t == typeof(Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
91 | {
92 | return new NullServiceScopeFactory(this);
93 | }
94 | return null;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/spin.toml:
--------------------------------------------------------------------------------
1 | spin_manifest_version = 2
2 |
3 | [application]
4 | authors = ["itowlson "]
5 | description = "Where pets store their toys!"
6 | name = "Fermyon.PetStore"
7 | version = "1.0.0"
8 |
9 | [variables.pg_conn_str]
10 | required = true
11 |
12 | [[trigger.http]]
13 | route = "/"
14 | component = "home"
15 |
16 | [component.home]
17 | source = "Fermyon.PetStore.Home/bin/Release/net8.0/Fermyon.PetStore.Home.wasm"
18 | files = [{ source = "Fermyon.PetStore.Home/assets", destination = "/assets" }]
19 | allowed_outbound_hosts = ["http://localhost:3000", "postgres://localhost:5432"]
20 | [component.home.build]
21 | command = "dotnet build -c Release"
22 | workdir = "Fermyon.PetStore.Home"
23 |
24 | [[trigger.http]]
25 | route = "/pets"
26 | component = "pets"
27 |
28 | [component.pets]
29 | source = "Fermyon.PetStore.Pets/bin/Release/net8.0/Fermyon.PetStore.Pets.wasm"
30 | files = [{ source = "Fermyon.PetStore.Pets/assets", destination = "/assets" }]
31 | allowed_outbound_hosts = ["http://localhost:3000", "postgres://localhost:5432"]
32 | [component.pets.variables]
33 | pg_conn_str = "{{ pg_conn_str }}"
34 | [component.pets.build]
35 | command = "dotnet build -c Release"
36 | workdir = "Fermyon.PetStore.Pets"
37 |
38 | [[trigger.http]]
39 | route = "/pets/new"
40 | component = "new-pet"
41 |
42 | [component.new-pet]
43 | source = "Fermyon.PetStore.NewPet/bin/Release/net8.0/Fermyon.PetStore.NewPet.wasm"
44 | files = [{ source = "Fermyon.PetStore.NewPet/assets", destination = "/assets" }]
45 | allowed_outbound_hosts = ["http://localhost:3000", "postgres://localhost:5432"]
46 | [component.new-pet.variables]
47 | pg_conn_str = "{{ pg_conn_str }}"
48 | [component.new-pet.build]
49 | command = "dotnet build -c Release"
50 | workdir = "Fermyon.PetStore.NewPet"
51 |
52 | [[trigger.http]]
53 | route = "/pet/..."
54 | component = "pet"
55 |
56 | [component.pet]
57 | source = "Fermyon.PetStore.Pet/bin/Release/net8.0/Fermyon.PetStore.Pet.wasm"
58 | files = [{ source = "Fermyon.PetStore.Pet/assets", destination = "/assets" }]
59 | allowed_outbound_hosts = ["http://localhost:3000", "postgres://localhost:5432"]
60 | [component.pet.variables]
61 | pg_conn_str = "{{ pg_conn_str }}"
62 | [component.pet.build]
63 | command = "dotnet build -c Release"
64 | workdir = "Fermyon.PetStore.Pet"
65 |
66 | [[trigger.http]]
67 | route = "/toys"
68 | component = "toys"
69 |
70 | [component.toys]
71 | source = "Fermyon.PetStore.Toys/bin/Release/net8.0/Fermyon.PetStore.Toys.wasm"
72 | files = [{ source = "Fermyon.PetStore.Toys/assets", destination = "/assets" }]
73 | allowed_outbound_hosts = ["http://localhost:3000", "postgres://localhost:5432"]
74 | [component.toys.variables]
75 | pg_conn_str = "{{ pg_conn_str }}"
76 | [component.toys.build]
77 | command = "dotnet build -c Release"
78 | workdir = "Fermyon.PetStore.Toys"
79 |
80 | [[trigger.http]]
81 | route = "/toys/new"
82 | component = "new-toy"
83 |
84 | [component.new-toy]
85 | source = "Fermyon.PetStore.NewToy/bin/Release/net8.0/Fermyon.PetStore.NewToy.wasm"
86 | files = [{ source = "Fermyon.PetStore.NewToy/assets", destination = "/assets" }]
87 | allowed_outbound_hosts = ["http://localhost:3000", "postgres://localhost:5432"]
88 | [component.new-toy.variables]
89 | pg_conn_str = "{{ pg_conn_str }}"
90 | [component.new-toy.build]
91 | command = "dotnet build -c Release"
92 | workdir = "Fermyon.PetStore.NewToy"
93 |
94 | [[trigger.http]]
95 | route = "/toy/..."
96 | component = "toy"
97 |
98 | [component.toy]
99 | source = "Fermyon.PetStore.Toy/bin/Release/net8.0/Fermyon.PetStore.Toy.wasm"
100 | files = [{ source = "Fermyon.PetStore.Toy/assets", destination = "/assets" }]
101 | allowed_outbound_hosts = ["http://localhost:3000", "postgres://localhost:5432"]
102 | [component.toy.variables]
103 | pg_conn_str = "{{ pg_conn_str }}"
104 | [component.toy.build]
105 | command = "dotnet build -c Release"
106 | workdir = "Fermyon.PetStore.Toy"
107 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.NewToy/Handler.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 | using HttpRequest = Fermyon.Spin.Sdk.HttpRequest;
3 | using HttpResponse = Fermyon.Spin.Sdk.HttpResponse;
4 |
5 | using Fermyon.PetStore.Common;
6 |
7 | namespace Fermyon.PetStore.NewToy;
8 |
9 | public static class Handler
10 | {
11 | [HttpHandler]
12 | public static HttpResponse HandleHttpRequest(HttpRequest request)
13 | {
14 | return request.Method switch {
15 | Fermyon.Spin.Sdk.HttpMethod.Get => ShowNewToyForm(request),
16 | Fermyon.Spin.Sdk.HttpMethod.Post => HandleNewToyForm(request),
17 | _ => new HttpResponse { StatusCode = System.Net.HttpStatusCode.MethodNotAllowed },
18 | };
19 | }
20 |
21 | private static HttpResponse ShowNewToyForm(HttpRequest request)
22 | {
23 | var html = request.IsRuntime() ?
24 | NewToyFormHtml() :
25 | String.Empty;
26 |
27 | return new HttpResponse
28 | {
29 | StatusCode = System.Net.HttpStatusCode.OK,
30 | Headers = new Dictionary
31 | {
32 | { "Content-Type", "text/html" },
33 | },
34 | BodyAsString = html,
35 | };
36 | }
37 |
38 | private static string NewToyFormHtml()
39 | {
40 | var connectionString = Configuration.DbConnectionString();
41 |
42 | var rows = PostgresOutbound.Query(connectionString, "SELECT id, name FROM pets ORDER BY name").Rows;
43 |
44 | if (rows.Count == 0)
45 | {
46 | return File.ReadAllText("/assets/NewToyNoPets.html");
47 | }
48 |
49 | var options = rows.Select(r => $"");
50 |
51 | var template = File.ReadAllText("/assets/NewToy.html");
52 | return template.Replace("{{ pet_options }}", string.Join('\n', options));
53 | }
54 |
55 | private static HttpResponse HandleNewToyForm(HttpRequest request)
56 | {
57 | var ctx = FormParser.ParsePostedForm(request);
58 |
59 | var description = ctx.Request.Form["submitted-description"].ToString();
60 | var countText = ctx.Request.Form["submitted-count"].ToString();
61 | var ownerIdText = ctx.Request.Form["submitted-owner"].ToString();
62 | var pictureFile = ctx.Request.Form.Files["submitted-picture"];
63 |
64 | if (string.IsNullOrWhiteSpace(description) || string.IsNullOrWhiteSpace(ownerIdText) || pictureFile == null || pictureFile.Length == 0)
65 | {
66 | return new HttpResponse
67 | {
68 | StatusCode = System.Net.HttpStatusCode.OK,
69 | Headers = new Dictionary
70 | {
71 | { "Content-Type", "text/html" },
72 | },
73 | BodyAsString = File.ReadAllText("/assets/NewToyMissingField.html"),
74 | };
75 | }
76 |
77 | int? count = string.IsNullOrWhiteSpace(countText) ? null : int.Parse(countText);
78 | var ownerId = int.Parse(ownerIdText);
79 |
80 | using var memStream = new MemoryStream((int)(pictureFile.Length));
81 | pictureFile.CopyTo(memStream);
82 | var pictureData = memStream.ToArray();
83 |
84 | var connectionString = Configuration.DbConnectionString();
85 |
86 | var maxIdResults = PostgresOutbound.Query(connectionString, "SELECT MAX(id) FROM toys");
87 | var maxId = maxIdResults.Rows.First()[0].AsNullableInt32();
88 |
89 | var id = (maxId ?? 0) + 1;
90 | PostgresOutbound.Execute(
91 | connectionString,
92 | "INSERT INTO toys (id, description, count, owner_id, picture) VALUES ($1, $2, $3, $4, $5)",
93 | id,
94 | description,
95 | count,
96 | ownerId,
97 | pictureData
98 | );
99 |
100 | return new HttpResponse
101 | {
102 | StatusCode = System.Net.HttpStatusCode.OK,
103 | Headers = new Dictionary
104 | {
105 | { "Content-Type", "text/html" },
106 | },
107 | BodyAsString = File.ReadAllText("/assets/NewToyCreated.html"),
108 | };
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/native/util.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | #include "./util.h"
16 |
17 | // These are generated by the WASI SDK during build
18 | const char* dotnet_wasi_getentrypointassemblyname();
19 | const char* dotnet_wasi_getbundledfile(const char* name, int* out_length);
20 | void dotnet_wasi_registerbundledassemblies();
21 |
22 | get_member_err_t get_property(MonoObject* instance, const char* name, MonoObject** result) {
23 | MonoClass* klass = mono_object_get_class(instance);
24 | MonoProperty* prop = mono_class_get_property_from_name(klass, name);
25 | if (!prop) {
26 | return GET_MEMBER_ERR_NOT_FOUND;
27 | }
28 | MonoMethod* getter = mono_property_get_get_method(prop);
29 | if (!getter) {
30 | return GET_MEMBER_ERR_WRITE_ONLY;
31 | }
32 |
33 | MonoObject* exn = NULL;
34 | *result = mono_wasm_invoke_method(getter, instance, NULL, &exn);
35 |
36 | mono_free(prop);
37 | mono_free(getter);
38 |
39 | if (exn) {
40 | return GET_MEMBER_ERR_EXCEPTION;
41 | }
42 |
43 | return GET_MEMBER_ERR_OK;
44 | }
45 |
46 | resolve_err_t find_decorated_method(MonoAssembly* assembly, const char* attr_name, MonoObject** attr_obj, MonoMethod** decorated_method) {
47 | MonoImage* image = mono_assembly_get_image(assembly);
48 | if (!image) {
49 | return RESOLVE_ERR_IMAGE_NOT_RESOLVED;
50 | }
51 |
52 | const MonoTableInfo* table_info = mono_image_get_table_info(image, MONO_TABLE_TYPEDEF);
53 | if (!table_info) {
54 | return RESOLVE_ERR_TYPEDEF_TABLE_NOT_RESOLVED;
55 | }
56 |
57 | int rows = mono_table_info_get_rows(table_info);
58 |
59 | for (int i = 0; i < rows; i++)
60 | {
61 | uint32_t cols[MONO_TYPEDEF_SIZE];
62 | mono_metadata_decode_row(table_info, i, cols, MONO_TYPEDEF_SIZE);
63 |
64 | const char* name = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAME]);
65 | const char* name_space = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAMESPACE]);
66 | if (!name || !name_space) {
67 | continue;
68 | }
69 |
70 | MonoClass* klass = mono_class_from_name(image, name_space, name);
71 | if (!klass) {
72 | continue;
73 | }
74 |
75 | void* iter = NULL;
76 | MonoMethod* method;
77 | while ((method = mono_class_get_methods(klass, &iter)) != NULL)
78 | {
79 | MonoCustomAttrInfo* attr_info = mono_custom_attrs_from_method(method);
80 | if (attr_info) {
81 | for (int i = 0; i < attr_info->num_attrs; ++i) {
82 | char* attr_ctor_name = mono_method_full_name(attr_info->attrs[i].ctor, 1);
83 | if (strstr(attr_ctor_name, attr_name) != NULL) {
84 | *decorated_method = method;
85 | MonoClass* attr_class = mono_method_get_class(attr_info->attrs[i].ctor);
86 | if (attr_class) {
87 | *attr_obj = mono_custom_attrs_get_attr(attr_info, attr_class);
88 | } else {
89 | *attr_obj = NULL;
90 | }
91 | return RESOLVE_ERR_OK;
92 | }
93 | mono_free(attr_ctor_name);
94 | }
95 | mono_free(attr_info);
96 | }
97 | mono_free(method);
98 | }
99 |
100 | mono_free(klass);
101 | }
102 |
103 | return RESOLVE_ERR_NO_MATCH;
104 | }
105 |
106 | entry_points_err_t find_entry_points(const char* attr_name, MonoObject** attr_obj, MonoMethod** handler) {
107 | MonoAssembly* assembly = mono_assembly_open(dotnet_wasi_getentrypointassemblyname(), NULL);
108 | if (!assembly) {
109 | return EP_ERR_NO_ENTRY_ASSEMBLY;
110 | }
111 |
112 | resolve_err_t find_err = find_decorated_method(assembly, attr_name, attr_obj, handler);
113 | if (find_err) {
114 | return EP_ERR_NO_HANDLER_METHOD;
115 | }
116 |
117 | return EP_ERR_OK;
118 | }
119 |
--------------------------------------------------------------------------------
/src/native/spin-http.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include "spin-http.h"
3 |
4 | __attribute__((weak, export_name("canonical_abi_realloc")))
5 | void *canonical_abi_realloc(
6 | void *ptr,
7 | size_t orig_size,
8 | size_t org_align,
9 | size_t new_size
10 | ) {
11 | void *ret = realloc(ptr, new_size);
12 | if (!ret)
13 | abort();
14 | return ret;
15 | }
16 |
17 | __attribute__((weak, export_name("canonical_abi_free")))
18 | void canonical_abi_free(
19 | void *ptr,
20 | size_t size,
21 | size_t align
22 | ) {
23 | free(ptr);
24 | }
25 | #include
26 |
27 | void spin_http_string_set(spin_http_string_t *ret, const char *s) {
28 | ret->ptr = (char*) s;
29 | ret->len = strlen(s);
30 | }
31 |
32 | void spin_http_string_dup(spin_http_string_t *ret, const char *s) {
33 | ret->len = strlen(s);
34 | ret->ptr = canonical_abi_realloc(NULL, 0, 1, ret->len);
35 | memcpy(ret->ptr, s, ret->len);
36 | }
37 |
38 | void spin_http_string_free(spin_http_string_t *ret) {
39 | canonical_abi_free(ret->ptr, ret->len, 1);
40 | ret->ptr = NULL;
41 | ret->len = 0;
42 | }
43 | void spin_http_body_free(spin_http_body_t *ptr) {
44 | canonical_abi_free(ptr->ptr, ptr->len * 1, 1);
45 | }
46 | void spin_http_tuple2_string_string_free(spin_http_tuple2_string_string_t *ptr) {
47 | spin_http_string_free(&ptr->f0);
48 | spin_http_string_free(&ptr->f1);
49 | }
50 | void spin_http_headers_free(spin_http_headers_t *ptr) {
51 | for (size_t i = 0; i < ptr->len; i++) {
52 | spin_http_tuple2_string_string_free(&ptr->ptr[i]);
53 | }
54 | canonical_abi_free(ptr->ptr, ptr->len * 16, 4);
55 | }
56 | void spin_http_params_free(spin_http_params_t *ptr) {
57 | for (size_t i = 0; i < ptr->len; i++) {
58 | spin_http_tuple2_string_string_free(&ptr->ptr[i]);
59 | }
60 | canonical_abi_free(ptr->ptr, ptr->len * 16, 4);
61 | }
62 | void spin_http_uri_free(spin_http_uri_t *ptr) {
63 | spin_http_string_free(ptr);
64 | }
65 | void spin_http_option_body_free(spin_http_option_body_t *ptr) {
66 | if (ptr->is_some) {
67 | spin_http_body_free(&ptr->val);
68 | }
69 | }
70 | void spin_http_request_free(spin_http_request_t *ptr) {
71 | spin_http_uri_free(&ptr->uri);
72 | spin_http_headers_free(&ptr->headers);
73 | spin_http_params_free(&ptr->params);
74 | spin_http_option_body_free(&ptr->body);
75 | }
76 | void spin_http_option_headers_free(spin_http_option_headers_t *ptr) {
77 | if (ptr->is_some) {
78 | spin_http_headers_free(&ptr->val);
79 | }
80 | }
81 | void spin_http_response_free(spin_http_response_t *ptr) {
82 | spin_http_option_headers_free(&ptr->headers);
83 | spin_http_option_body_free(&ptr->body);
84 | }
85 |
86 | __attribute__((aligned(4)))
87 | static uint8_t RET_AREA[28];
88 | __attribute__((export_name("handle-http-request")))
89 | int32_t __wasm_export_spin_http_handle_http_request(int32_t arg, int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3, int32_t arg4, int32_t arg5, int32_t arg6, int32_t arg7, int32_t arg8) {
90 | spin_http_option_body_t option;
91 | switch (arg6) {
92 | case 0: {
93 | option.is_some = false;
94 |
95 | break;
96 | }
97 | case 1: {
98 | option.is_some = true;
99 |
100 | option.val = (spin_http_body_t) { (uint8_t*)(arg7), (size_t)(arg8) };
101 | break;
102 | }
103 | }spin_http_request_t arg9 = (spin_http_request_t) {
104 | arg,
105 | (spin_http_string_t) { (char*)(arg0), (size_t)(arg1) },
106 | (spin_http_headers_t) { (spin_http_tuple2_string_string_t*)(arg2), (size_t)(arg3) },
107 | (spin_http_params_t) { (spin_http_tuple2_string_string_t*)(arg4), (size_t)(arg5) },
108 | option,
109 | };
110 | spin_http_response_t ret;
111 | spin_http_handle_http_request(&arg9, &ret);
112 | int32_t ptr = (int32_t) &RET_AREA;
113 | *((int16_t*)(ptr + 0)) = (int32_t) ((ret).status);
114 |
115 | if (((ret).headers).is_some) {
116 | const spin_http_headers_t *payload10 = &((ret).headers).val;
117 | *((int8_t*)(ptr + 4)) = 1;
118 | *((int32_t*)(ptr + 12)) = (int32_t) (*payload10).len;
119 | *((int32_t*)(ptr + 8)) = (int32_t) (*payload10).ptr;
120 |
121 | } else {
122 | *((int8_t*)(ptr + 4)) = 0;
123 |
124 | }
125 |
126 | if (((ret).body).is_some) {
127 | const spin_http_body_t *payload12 = &((ret).body).val;
128 | *((int8_t*)(ptr + 16)) = 1;
129 | *((int32_t*)(ptr + 24)) = (int32_t) (*payload12).len;
130 | *((int32_t*)(ptr + 20)) = (int32_t) (*payload12).ptr;
131 |
132 | } else {
133 | *((int8_t*)(ptr + 16)) = 0;
134 |
135 | }
136 | return ptr;
137 | }
138 |
--------------------------------------------------------------------------------
/src/native/outbound-redis.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include "outbound-redis.h"
3 |
4 | __attribute__((weak, export_name("canonical_abi_realloc"))) void *canonical_abi_realloc(
5 | void *ptr,
6 | size_t orig_size,
7 | size_t org_align,
8 | size_t new_size)
9 | {
10 | void *ret = realloc(ptr, new_size);
11 | if (!ret)
12 | abort();
13 | return ret;
14 | }
15 |
16 | __attribute__((weak, export_name("canonical_abi_free"))) void canonical_abi_free(
17 | void *ptr,
18 | size_t size,
19 | size_t align)
20 | {
21 | free(ptr);
22 | }
23 | #include
24 |
25 | void outbound_redis_string_set(outbound_redis_string_t *ret, const char *s)
26 | {
27 | ret->ptr = (char *)s;
28 | ret->len = strlen(s);
29 | }
30 |
31 | void outbound_redis_string_dup(outbound_redis_string_t *ret, const char *s)
32 | {
33 | ret->len = strlen(s);
34 | ret->ptr = canonical_abi_realloc(NULL, 0, 1, ret->len);
35 | memcpy(ret->ptr, s, ret->len);
36 | }
37 |
38 | void outbound_redis_string_free(outbound_redis_string_t *ret)
39 | {
40 | canonical_abi_free(ret->ptr, ret->len, 1);
41 | ret->ptr = NULL;
42 | ret->len = 0;
43 | }
44 | void outbound_redis_payload_free(outbound_redis_payload_t *ptr)
45 | {
46 | canonical_abi_free(ptr->ptr, ptr->len * 1, 1);
47 | }
48 | typedef struct
49 | {
50 | bool is_err;
51 | union
52 | {
53 | outbound_redis_error_t err;
54 | } val;
55 | } outbound_redis_expected_unit_error_t;
56 | typedef struct
57 | {
58 | bool is_err;
59 | union
60 | {
61 | outbound_redis_payload_t ok;
62 | outbound_redis_error_t err;
63 | } val;
64 | } outbound_redis_expected_payload_error_t;
65 |
66 | __attribute__((aligned(4))) static uint8_t RET_AREA[12];
67 | __attribute__((import_module("outbound-redis"), import_name("publish"))) void __wasm_import_outbound_redis_publish(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
68 | outbound_redis_error_t outbound_redis_publish(outbound_redis_string_t *address, outbound_redis_string_t *channel, outbound_redis_payload_t *payload)
69 | {
70 | int32_t ptr = (int32_t)&RET_AREA;
71 | __wasm_import_outbound_redis_publish((int32_t)(*address).ptr, (int32_t)(*address).len, (int32_t)(*channel).ptr, (int32_t)(*channel).len, (int32_t)(*payload).ptr, (int32_t)(*payload).len, ptr);
72 | outbound_redis_expected_unit_error_t expected;
73 | switch ((int32_t)(*((uint8_t *)(ptr + 0))))
74 | {
75 | case 0:
76 | {
77 | expected.is_err = false;
78 |
79 | break;
80 | }
81 | case 1:
82 | {
83 | expected.is_err = true;
84 |
85 | expected.val.err = (int32_t)(*((uint8_t *)(ptr + 1)));
86 | break;
87 | }
88 | }
89 | return expected.is_err ? expected.val.err : -1;
90 | }
91 | __attribute__((import_module("outbound-redis"), import_name("get"))) void __wasm_import_outbound_redis_get(int32_t, int32_t, int32_t, int32_t, int32_t);
92 | outbound_redis_error_t outbound_redis_get(outbound_redis_string_t *address, outbound_redis_string_t *key, outbound_redis_payload_t *ret0)
93 | {
94 | int32_t ptr = (int32_t)&RET_AREA;
95 | __wasm_import_outbound_redis_get((int32_t)(*address).ptr, (int32_t)(*address).len, (int32_t)(*key).ptr, (int32_t)(*key).len, ptr);
96 | outbound_redis_expected_payload_error_t expected;
97 | switch ((int32_t)(*((uint8_t *)(ptr + 0))))
98 | {
99 | case 0:
100 | {
101 | expected.is_err = false;
102 |
103 | expected.val.ok = (outbound_redis_payload_t){(uint8_t *)(*((int32_t *)(ptr + 4))), (size_t)(*((int32_t *)(ptr + 8)))};
104 | break;
105 | }
106 | case 1:
107 | {
108 | expected.is_err = true;
109 |
110 | expected.val.err = (int32_t)(*((uint8_t *)(ptr + 4)));
111 | break;
112 | }
113 | }
114 | *ret0 = expected.val.ok;
115 | return expected.is_err ? expected.val.err : -1;
116 | }
117 | __attribute__((import_module("outbound-redis"), import_name("set"))) void __wasm_import_outbound_redis_set(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
118 | outbound_redis_error_t outbound_redis_set(outbound_redis_string_t *address, outbound_redis_string_t *key, outbound_redis_payload_t *value)
119 | {
120 | int32_t ptr = (int32_t)&RET_AREA;
121 | __wasm_import_outbound_redis_set((int32_t)(*address).ptr, (int32_t)(*address).len, (int32_t)(*key).ptr, (int32_t)(*key).len, (int32_t)(*value).ptr, (int32_t)(*value).len, ptr);
122 | outbound_redis_expected_unit_error_t expected;
123 | switch ((int32_t)(*((uint8_t *)(ptr + 0))))
124 | {
125 | case 0:
126 | {
127 | expected.is_err = false;
128 |
129 | break;
130 | }
131 | case 1:
132 | {
133 | expected.is_err = true;
134 |
135 | expected.val.err = (int32_t)(*((uint8_t *)(ptr + 1)));
136 | break;
137 | }
138 | }
139 | return expected.is_err ? expected.val.err : -1;
140 | }
141 |
--------------------------------------------------------------------------------
/samples/Fermyon.PetStore/Fermyon.PetStore.Toy/Handler.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 |
3 | using Fermyon.PetStore.Common;
4 |
5 | namespace Fermyon.PetStore.Toy;
6 |
7 | public static class Handler
8 | {
9 | [HttpHandler]
10 | public static HttpResponse HandleHttpRequest(HttpRequest request)
11 | {
12 | if (!request.IsRuntime())
13 | {
14 | return new HttpResponse
15 | {
16 | StatusCode = System.Net.HttpStatusCode.OK,
17 | Headers = new Dictionary
18 | {
19 | { "Content-Type", "text/html" },
20 | },
21 | BodyAsString = String.Empty,
22 | };
23 | }
24 |
25 | var pathElements = request.Headers["spin-path-info"].Split('/', StringSplitOptions.RemoveEmptyEntries);
26 | return pathElements switch {
27 | [var id] => ToyPage(id),
28 | [var id, "picture"] => ToyPicture(id),
29 | _ => GenericResponse.NotFound(),
30 | };
31 | }
32 |
33 | public static HttpResponse ToyPage(string idHeader)
34 | {
35 | if (int.TryParse(idHeader, out var id))
36 | {
37 | var connectionString = Configuration.DbConnectionString();
38 |
39 | var rows = PostgresOutbound.Query(connectionString, "SELECT toys.id, toys.description, toys.count, toys.owner_id, pets.name FROM toys INNER JOIN pets ON pets.id = toys.owner_id WHERE toys.id = $1", id).Rows;
40 |
41 | if (rows.Count == 0)
42 | {
43 | return ToyNotFound();
44 | }
45 |
46 | var toyId = rows[0][0].AsInt32();
47 | var description = rows[0][1].AsString();
48 | var count = rows[0][2].AsNullableInt32();
49 | var ownerId = rows[0][3].AsInt32();
50 | var ownerName = rows[0][4].AsString();
51 |
52 | var countText = count switch {
53 | null => "an unknown number",
54 | int n => n.ToString(),
55 | };
56 |
57 | var template = File.ReadAllText("/assets/ToyTemplate.html");
58 |
59 | var responseText = template
60 | .Replace("{{ description }}", description)
61 | .Replace("{{ count }}", countText)
62 | .Replace("{{ toy_id }}", toyId.ToString())
63 | .Replace("{{ owner_id }}", ownerId.ToString())
64 | .Replace("{{ owner_name }}", ownerName);
65 |
66 | return new HttpResponse
67 | {
68 | StatusCode = System.Net.HttpStatusCode.OK,
69 | Headers = new Dictionary
70 | {
71 | { "Content-Type", "text/html" },
72 | },
73 | BodyAsString = responseText,
74 | };
75 | }
76 | return ToyNotFound();
77 | }
78 |
79 | public static HttpResponse ToyPicture(string idHeader)
80 | {
81 | if (int.TryParse(idHeader, out var id))
82 | {
83 | var connectionString = Configuration.DbConnectionString();
84 |
85 | var rows = PostgresOutbound.Query(connectionString, "SELECT id, picture FROM toys WHERE id = $1", id).Rows;
86 |
87 | if (rows.Count == 0)
88 | {
89 | return ToyNotFound();
90 | }
91 |
92 | var picture = rows[0][1].AsNullableBuffer().GetValueOrDefault();
93 |
94 | if (picture.Length == 0)
95 | {
96 | return MysteryToyPicture();
97 | }
98 |
99 | return new HttpResponse
100 | {
101 | StatusCode = System.Net.HttpStatusCode.OK,
102 | Headers = new Dictionary
103 | {
104 | { "Content-Type", "image/*" },
105 | },
106 | BodyAsBytes = picture,
107 | };
108 | }
109 | return ToyNotFound();
110 | }
111 |
112 | public static HttpResponse ToyNotFound()
113 | {
114 | return new HttpResponse
115 | {
116 | StatusCode = System.Net.HttpStatusCode.NotFound,
117 | Headers = new Dictionary
118 | {
119 | { "Content-Type", "text/html" },
120 | },
121 | BodyAsString = File.ReadAllText("/assets/ToyNotFound.html"),
122 | };
123 | }
124 |
125 | public static HttpResponse MysteryToyPicture()
126 | {
127 | return new HttpResponse
128 | {
129 | StatusCode = System.Net.HttpStatusCode.NotFound,
130 | Headers = new Dictionary
131 | {
132 | { "Content-Type", "image/png" },
133 | },
134 | BodyAsBytes = File.ReadAllBytes("/assets/mystery-toy.png"),
135 | };
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/native/wasi-outbound-http.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include "wasi-outbound-http.h"
3 |
4 | __attribute__((weak, export_name("canonical_abi_realloc")))
5 | void *canonical_abi_realloc(
6 | void *ptr,
7 | size_t orig_size,
8 | size_t org_align,
9 | size_t new_size
10 | ) {
11 | void *ret = realloc(ptr, new_size);
12 | if (!ret)
13 | abort();
14 | return ret;
15 | }
16 |
17 | __attribute__((weak, export_name("canonical_abi_free")))
18 | void canonical_abi_free(
19 | void *ptr,
20 | size_t size,
21 | size_t align
22 | ) {
23 | free(ptr);
24 | }
25 | #include
26 |
27 | void wasi_outbound_http_string_set(wasi_outbound_http_string_t *ret, const char *s) {
28 | ret->ptr = (char*) s;
29 | ret->len = strlen(s);
30 | }
31 |
32 | void wasi_outbound_http_string_dup(wasi_outbound_http_string_t *ret, const char *s) {
33 | ret->len = strlen(s);
34 | ret->ptr = canonical_abi_realloc(NULL, 0, 1, ret->len);
35 | memcpy(ret->ptr, s, ret->len);
36 | }
37 |
38 | void wasi_outbound_http_string_free(wasi_outbound_http_string_t *ret) {
39 | canonical_abi_free(ret->ptr, ret->len, 1);
40 | ret->ptr = NULL;
41 | ret->len = 0;
42 | }
43 | void wasi_outbound_http_body_free(wasi_outbound_http_body_t *ptr) {
44 | canonical_abi_free(ptr->ptr, ptr->len * 1, 1);
45 | }
46 | void wasi_outbound_http_tuple2_string_string_free(wasi_outbound_http_tuple2_string_string_t *ptr) {
47 | wasi_outbound_http_string_free(&ptr->f0);
48 | wasi_outbound_http_string_free(&ptr->f1);
49 | }
50 | void wasi_outbound_http_headers_free(wasi_outbound_http_headers_t *ptr) {
51 | for (size_t i = 0; i < ptr->len; i++) {
52 | wasi_outbound_http_tuple2_string_string_free(&ptr->ptr[i]);
53 | }
54 | canonical_abi_free(ptr->ptr, ptr->len * 16, 4);
55 | }
56 | void wasi_outbound_http_params_free(wasi_outbound_http_params_t *ptr) {
57 | for (size_t i = 0; i < ptr->len; i++) {
58 | wasi_outbound_http_tuple2_string_string_free(&ptr->ptr[i]);
59 | }
60 | canonical_abi_free(ptr->ptr, ptr->len * 16, 4);
61 | }
62 | void wasi_outbound_http_uri_free(wasi_outbound_http_uri_t *ptr) {
63 | wasi_outbound_http_string_free(ptr);
64 | }
65 | void wasi_outbound_http_option_body_free(wasi_outbound_http_option_body_t *ptr) {
66 | if (ptr->is_some) {
67 | wasi_outbound_http_body_free(&ptr->val);
68 | }
69 | }
70 | void wasi_outbound_http_request_free(wasi_outbound_http_request_t *ptr) {
71 | wasi_outbound_http_uri_free(&ptr->uri);
72 | wasi_outbound_http_headers_free(&ptr->headers);
73 | wasi_outbound_http_params_free(&ptr->params);
74 | wasi_outbound_http_option_body_free(&ptr->body);
75 | }
76 | void wasi_outbound_http_option_headers_free(wasi_outbound_http_option_headers_t *ptr) {
77 | if (ptr->is_some) {
78 | wasi_outbound_http_headers_free(&ptr->val);
79 | }
80 | }
81 | void wasi_outbound_http_response_free(wasi_outbound_http_response_t *ptr) {
82 | wasi_outbound_http_option_headers_free(&ptr->headers);
83 | wasi_outbound_http_option_body_free(&ptr->body);
84 | }
85 | typedef struct {
86 | bool is_err;
87 | union {
88 | wasi_outbound_http_response_t ok;
89 | wasi_outbound_http_http_error_t err;
90 | } val;
91 | } wasi_outbound_http_expected_response_http_error_t;
92 |
93 | __attribute__((aligned(4)))
94 | static uint8_t RET_AREA[32];
95 | __attribute__((import_module("wasi-outbound-http"), import_name("request")))
96 | void __wasm_import_wasi_outbound_http_request(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
97 | wasi_outbound_http_http_error_t wasi_outbound_http_request(wasi_outbound_http_request_t *req, wasi_outbound_http_response_t *ret0) {
98 | int32_t option;
99 | int32_t option1;
100 | int32_t option2;
101 |
102 | if (((*req).body).is_some) {
103 | const wasi_outbound_http_body_t *payload0 = &((*req).body).val;
104 | option = 1;
105 | option1 = (int32_t) (*payload0).ptr;
106 | option2 = (int32_t) (*payload0).len;
107 |
108 | } else {
109 | option = 0;
110 | option1 = 0;
111 | option2 = 0;
112 |
113 | }
114 | int32_t ptr = (int32_t) &RET_AREA;
115 | __wasm_import_wasi_outbound_http_request((int32_t) (*req).method, (int32_t) ((*req).uri).ptr, (int32_t) ((*req).uri).len, (int32_t) ((*req).headers).ptr, (int32_t) ((*req).headers).len, (int32_t) ((*req).params).ptr, (int32_t) ((*req).params).len, option, option1, option2, ptr);
116 | wasi_outbound_http_expected_response_http_error_t expected;
117 | switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
118 | case 0: {
119 | expected.is_err = false;
120 | wasi_outbound_http_option_headers_t option3;
121 | switch ((int32_t) (*((uint8_t*) (ptr + 8)))) {
122 | case 0: {
123 | option3.is_some = false;
124 |
125 | break;
126 | }
127 | case 1: {
128 | option3.is_some = true;
129 |
130 | option3.val = (wasi_outbound_http_headers_t) { (wasi_outbound_http_tuple2_string_string_t*)(*((int32_t*) (ptr + 12))), (size_t)(*((int32_t*) (ptr + 16))) };
131 | break;
132 | }
133 | }wasi_outbound_http_option_body_t option4;
134 | switch ((int32_t) (*((uint8_t*) (ptr + 20)))) {
135 | case 0: {
136 | option4.is_some = false;
137 |
138 | break;
139 | }
140 | case 1: {
141 | option4.is_some = true;
142 |
143 | option4.val = (wasi_outbound_http_body_t) { (uint8_t*)(*((int32_t*) (ptr + 24))), (size_t)(*((int32_t*) (ptr + 28))) };
144 | break;
145 | }
146 | }
147 | expected.val.ok = (wasi_outbound_http_response_t) {
148 | (uint16_t) ((int32_t) (*((uint16_t*) (ptr + 4)))),
149 | option3,
150 | option4,
151 | };
152 | break;
153 | }
154 | case 1: {
155 | expected.is_err = true;
156 |
157 | expected.val.err = (int32_t) (*((uint8_t*) (ptr + 4)));
158 | break;
159 | }
160 | }*ret0 = expected.val.ok;
161 | return expected.is_err ? expected.val.err : -1;
162 | }
163 |
--------------------------------------------------------------------------------
/src/native/outbound-pg.h:
--------------------------------------------------------------------------------
1 | #ifndef __BINDINGS_OUTBOUND_PG_H
2 | #define __BINDINGS_OUTBOUND_PG_H
3 | #ifdef __cplusplus
4 | extern "C"
5 | {
6 | #endif
7 |
8 | #include
9 | #include
10 |
11 | typedef struct {
12 | char *ptr;
13 | size_t len;
14 | } outbound_pg_string_t;
15 |
16 | void outbound_pg_string_set(outbound_pg_string_t *ret, const char *s);
17 | void outbound_pg_string_dup(outbound_pg_string_t *ret, const char *s);
18 | void outbound_pg_string_free(outbound_pg_string_t *ret);
19 | typedef struct {
20 | uint8_t tag;
21 | union {
22 | outbound_pg_string_t connection_failed;
23 | outbound_pg_string_t bad_parameter;
24 | outbound_pg_string_t query_failed;
25 | outbound_pg_string_t value_conversion_failed;
26 | outbound_pg_string_t other_error;
27 | } val;
28 | } outbound_pg_pg_error_t;
29 | #define OUTBOUND_PG_PG_ERROR_SUCCESS 0
30 | #define OUTBOUND_PG_PG_ERROR_CONNECTION_FAILED 1
31 | #define OUTBOUND_PG_PG_ERROR_BAD_PARAMETER 2
32 | #define OUTBOUND_PG_PG_ERROR_QUERY_FAILED 3
33 | #define OUTBOUND_PG_PG_ERROR_VALUE_CONVERSION_FAILED 4
34 | #define OUTBOUND_PG_PG_ERROR_OTHER_ERROR 5
35 | void outbound_pg_pg_error_free(outbound_pg_pg_error_t *ptr);
36 | typedef uint8_t outbound_pg_db_data_type_t;
37 | #define OUTBOUND_PG_DB_DATA_TYPE_BOOLEAN 0
38 | #define OUTBOUND_PG_DB_DATA_TYPE_INT8 1
39 | #define OUTBOUND_PG_DB_DATA_TYPE_INT16 2
40 | #define OUTBOUND_PG_DB_DATA_TYPE_INT32 3
41 | #define OUTBOUND_PG_DB_DATA_TYPE_INT64 4
42 | #define OUTBOUND_PG_DB_DATA_TYPE_UINT8 5
43 | #define OUTBOUND_PG_DB_DATA_TYPE_UINT16 6
44 | #define OUTBOUND_PG_DB_DATA_TYPE_UINT32 7
45 | #define OUTBOUND_PG_DB_DATA_TYPE_UINT64 8
46 | #define OUTBOUND_PG_DB_DATA_TYPE_FLOATING32 9
47 | #define OUTBOUND_PG_DB_DATA_TYPE_FLOATING64 10
48 | #define OUTBOUND_PG_DB_DATA_TYPE_STR 11
49 | #define OUTBOUND_PG_DB_DATA_TYPE_BINARY 12
50 | #define OUTBOUND_PG_DB_DATA_TYPE_OTHER 13
51 | typedef struct {
52 | outbound_pg_string_t name;
53 | outbound_pg_db_data_type_t data_type;
54 | } outbound_pg_column_t;
55 | void outbound_pg_column_free(outbound_pg_column_t *ptr);
56 | typedef struct {
57 | uint8_t *ptr;
58 | size_t len;
59 | } outbound_pg_list_u8_t;
60 | void outbound_pg_list_u8_free(outbound_pg_list_u8_t *ptr);
61 | typedef struct {
62 | uint8_t tag;
63 | union {
64 | bool boolean;
65 | int8_t int8;
66 | int16_t int16;
67 | int32_t int32;
68 | int64_t int64;
69 | uint8_t uint8;
70 | uint16_t uint16;
71 | uint32_t uint32;
72 | uint64_t uint64;
73 | float floating32;
74 | double floating64;
75 | outbound_pg_string_t str;
76 | outbound_pg_list_u8_t binary;
77 | } val;
78 | } outbound_pg_db_value_t;
79 | #define OUTBOUND_PG_DB_VALUE_BOOLEAN 0
80 | #define OUTBOUND_PG_DB_VALUE_INT8 1
81 | #define OUTBOUND_PG_DB_VALUE_INT16 2
82 | #define OUTBOUND_PG_DB_VALUE_INT32 3
83 | #define OUTBOUND_PG_DB_VALUE_INT64 4
84 | #define OUTBOUND_PG_DB_VALUE_UINT8 5
85 | #define OUTBOUND_PG_DB_VALUE_UINT16 6
86 | #define OUTBOUND_PG_DB_VALUE_UINT32 7
87 | #define OUTBOUND_PG_DB_VALUE_UINT64 8
88 | #define OUTBOUND_PG_DB_VALUE_FLOATING32 9
89 | #define OUTBOUND_PG_DB_VALUE_FLOATING64 10
90 | #define OUTBOUND_PG_DB_VALUE_STR 11
91 | #define OUTBOUND_PG_DB_VALUE_BINARY 12
92 | #define OUTBOUND_PG_DB_VALUE_DB_NULL 13
93 | #define OUTBOUND_PG_DB_VALUE_UNSUPPORTED 14
94 | void outbound_pg_db_value_free(outbound_pg_db_value_t *ptr);
95 | typedef struct {
96 | uint8_t tag;
97 | union {
98 | bool boolean;
99 | int8_t int8;
100 | int16_t int16;
101 | int32_t int32;
102 | int64_t int64;
103 | uint8_t uint8;
104 | uint16_t uint16;
105 | uint32_t uint32;
106 | uint64_t uint64;
107 | float floating32;
108 | double floating64;
109 | outbound_pg_string_t str;
110 | outbound_pg_list_u8_t binary;
111 | } val;
112 | } outbound_pg_parameter_value_t;
113 | #define OUTBOUND_PG_PARAMETER_VALUE_BOOLEAN 0
114 | #define OUTBOUND_PG_PARAMETER_VALUE_INT8 1
115 | #define OUTBOUND_PG_PARAMETER_VALUE_INT16 2
116 | #define OUTBOUND_PG_PARAMETER_VALUE_INT32 3
117 | #define OUTBOUND_PG_PARAMETER_VALUE_INT64 4
118 | #define OUTBOUND_PG_PARAMETER_VALUE_UINT8 5
119 | #define OUTBOUND_PG_PARAMETER_VALUE_UINT16 6
120 | #define OUTBOUND_PG_PARAMETER_VALUE_UINT32 7
121 | #define OUTBOUND_PG_PARAMETER_VALUE_UINT64 8
122 | #define OUTBOUND_PG_PARAMETER_VALUE_FLOATING32 9
123 | #define OUTBOUND_PG_PARAMETER_VALUE_FLOATING64 10
124 | #define OUTBOUND_PG_PARAMETER_VALUE_STR 11
125 | #define OUTBOUND_PG_PARAMETER_VALUE_BINARY 12
126 | #define OUTBOUND_PG_PARAMETER_VALUE_DB_NULL 13
127 | void outbound_pg_parameter_value_free(outbound_pg_parameter_value_t *ptr);
128 | typedef struct {
129 | outbound_pg_db_value_t *ptr;
130 | size_t len;
131 | } outbound_pg_row_t;
132 | void outbound_pg_row_free(outbound_pg_row_t *ptr);
133 | typedef struct {
134 | outbound_pg_column_t *ptr;
135 | size_t len;
136 | } outbound_pg_list_column_t;
137 | void outbound_pg_list_column_free(outbound_pg_list_column_t *ptr);
138 | typedef struct {
139 | outbound_pg_row_t *ptr;
140 | size_t len;
141 | } outbound_pg_list_row_t;
142 | void outbound_pg_list_row_free(outbound_pg_list_row_t *ptr);
143 | typedef struct {
144 | outbound_pg_list_column_t columns;
145 | outbound_pg_list_row_t rows;
146 | } outbound_pg_row_set_t;
147 | void outbound_pg_row_set_free(outbound_pg_row_set_t *ptr);
148 | typedef struct {
149 | outbound_pg_parameter_value_t *ptr;
150 | size_t len;
151 | } outbound_pg_list_parameter_value_t;
152 | void outbound_pg_list_parameter_value_free(outbound_pg_list_parameter_value_t *ptr);
153 | typedef struct {
154 | bool is_err;
155 | union {
156 | outbound_pg_row_set_t ok;
157 | outbound_pg_pg_error_t err;
158 | } val;
159 | } outbound_pg_expected_row_set_pg_error_t;
160 | void outbound_pg_expected_row_set_pg_error_free(outbound_pg_expected_row_set_pg_error_t *ptr);
161 | typedef struct {
162 | bool is_err;
163 | union {
164 | uint64_t ok;
165 | outbound_pg_pg_error_t err;
166 | } val;
167 | } outbound_pg_expected_u64_pg_error_t;
168 | void outbound_pg_expected_u64_pg_error_free(outbound_pg_expected_u64_pg_error_t *ptr);
169 | void outbound_pg_query(outbound_pg_string_t *address, outbound_pg_string_t *statement, outbound_pg_list_parameter_value_t *params, outbound_pg_expected_row_set_pg_error_t *ret0);
170 | void outbound_pg_execute(outbound_pg_string_t *address, outbound_pg_string_t *statement, outbound_pg_list_parameter_value_t *params, outbound_pg_expected_u64_pg_error_t *ret0);
171 | #ifdef __cplusplus
172 | }
173 | #endif
174 | #endif
175 |
--------------------------------------------------------------------------------
/src/native/http-trigger.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include "./spin-http.h"
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 |
18 | #include "./host-components.h"
19 | #include "./util.h"
20 |
21 | // These are generated by the WASI SDK during build
22 | const char* dotnet_wasi_getentrypointassemblyname();
23 | const char* dotnet_wasi_getbundledfile(const char* name, int* out_length);
24 | void dotnet_wasi_registerbundledassemblies();
25 |
26 | MonoObject* call_clr_request_handler(MonoMethod* handler, spin_http_request_t* req, MonoObject** exn) {
27 | *exn = NULL;
28 |
29 | void *params[1];
30 | params[0] = req;
31 | return mono_wasm_invoke_method(handler, NULL, params, exn);
32 | }
33 |
34 | spin_http_response_t internal_error(const char* message) {
35 | spin_http_response_t response;
36 | response.status = 500;
37 | response.headers.is_some = false;
38 | response.body.is_some = true;
39 | response.body.val.ptr = (uint8_t*)message;
40 | response.body.val.len = strlen(message);
41 | return response;
42 | }
43 |
44 | unsigned long time_microseconds() {
45 | struct timeval tv;
46 | gettimeofday(&tv, NULL);
47 | return 1000000 * tv.tv_sec + tv.tv_usec;
48 | }
49 |
50 | // This is a C runtime internal: calling it is a bit of a desperate kludge.
51 | //
52 | // The call to __wasm_call_ctors is needed to set up the Wasm end of the preopened directories
53 | // (and environment variables? TBC). It's normally emitted by the compiler as part of C runtime startup.
54 | // But at the moment it is emitted only at the main() entry point. It's not clear how to get it
55 | // emitted for component model entry points. `-Wl,--no-entry -mexec-model=reactor` might do it
56 | // according to https://github.com/WebAssembly/wasi-sdk/issues/110; we should test this and, if it
57 | // works, discuss ways to get it incorporated as a build option for the .NET WASI SDK.
58 | void __wasm_call_ctors();
59 |
60 | // If wizer is run on this module, these fields will be populated at build time and hence we'll be able
61 | // to skip loading and initializing the runtime on a per-request basis. But if wizer isn't run, we'll
62 | // set up the runtime separately for each request.
63 |
64 | const char* initialization_error;
65 | MonoMethod* http_handler;
66 | MonoObject* http_handler_attr;
67 | int preinitialized;
68 |
69 | void process_http_request(spin_http_request_t *req, spin_http_response_t *ret0) {
70 | MonoObject* exn;
71 | unsigned long start_time = time_microseconds();
72 | MonoObject* call_result = call_clr_request_handler(http_handler, req, &exn);
73 | unsigned long end_time = time_microseconds();
74 |
75 | if (exn) {
76 | MonoString* exn_str = mono_object_to_string(exn, NULL);
77 | char* exn_cstr = mono_wasm_string_get_utf8(exn_str);
78 | *ret0 = internal_error(exn_cstr);
79 | return;
80 | }
81 |
82 | spin_http_response_t* resp = mono_object_unbox(call_result);
83 |
84 | // Add an HTTP response header giving the timing information
85 | // This is for debugging only - should be removed for production use
86 | spin_http_string_t end_time_header_str;
87 | spin_http_string_dup(&end_time_header_str, "time-in-dotnet");
88 | char* end_time_string;
89 | int end_time_string_len = asprintf(&end_time_string, "%f ms", (end_time - start_time) / 1000.0);
90 | int num_headers = ++resp->headers.val.len;
91 | resp->headers.val.ptr = resp->headers.is_some
92 | ? realloc(resp->headers.val.ptr, num_headers * sizeof(spin_http_tuple2_string_string_t))
93 | : malloc(num_headers * sizeof(spin_http_tuple2_string_string_t));
94 | resp->headers.is_some = 1;
95 | resp->headers.val.ptr[num_headers - 1] = (spin_http_tuple2_string_string_t){
96 | end_time_header_str,
97 | {end_time_string, end_time_string_len}
98 | };
99 |
100 | *ret0 = *resp;
101 | }
102 |
103 | void initialize() {
104 | dotnet_wasi_registerbundledassemblies();
105 | mono_wasm_load_runtime("", 0);
106 | spin_attach_internal_calls();
107 |
108 | entry_points_err_t entry_points_err = find_entry_points("Fermyon.Spin.Sdk.HttpHandlerAttribute", &http_handler_attr, &http_handler);
109 | if (entry_points_err) {
110 | if (entry_points_err == EP_ERR_NO_HANDLER_METHOD) {
111 | initialization_error = "Assembly does not contain a method with HttpHandlerAttribute";
112 | } else {
113 | initialization_error = "Internal error loading HTTP handler";
114 | }
115 | return;
116 | }
117 | }
118 |
119 | __attribute__((export_name("wizer.initialize")))
120 | void preinitialize() {
121 | preinitialized = 1;
122 | initialize();
123 |
124 | // To warm the interpreter, we need to run the main code path that is going to execute per-request. That way the preinitialized
125 | // binary is already ready to go at full speed.
126 |
127 | char* warmup_url = "/warmupz";
128 | if (http_handler_attr) {
129 | MonoString* warmup_str;
130 | if (get_property(http_handler_attr, "WarmupUrl", (MonoObject**)&warmup_str) == GET_MEMBER_ERR_OK) {
131 | warmup_url = mono_wasm_string_get_utf8(warmup_str);
132 | }
133 | }
134 | int warmup_url_len = strlen(warmup_url);
135 |
136 | // supply fake headers that would usually originate from the http trigger
137 | // we can't introspect on our own component config so we just make up some values
138 | char* fake_host = "127.0.0.1:3000";
139 | int fake_host_len = strlen(fake_host);
140 | char* warmup_url_full;
141 | int warmup_url_full_len = asprintf(&warmup_url_full, "http://%s%s", fake_host, warmup_url);
142 | spin_http_headers_t fake_headers = {.len = 10, .ptr = (spin_http_tuple2_string_string_t[]){
143 | {{"host", 4}, {fake_host, fake_host_len}},
144 | {{"user-agent", 10}, {"wizer", 5}},
145 | {{"accept", 6}, {"*/*", 3}},
146 | {{"spin-full-url", 13}, {warmup_url_full, warmup_url_full_len}},
147 | {{"spin-path-info", 14}, {warmup_url, warmup_url_len}},
148 | {{"spin-matched-route", 18}, {"/...", 3}},
149 | {{"spin-raw-component-route", 24}, {"/...", 3}},
150 | {{"spin-component-route", 20}, {"", 0}},
151 | {{"spin-base-path", 14}, {"/", 1}},
152 | {{"spin-client-addr", 14}, {fake_host, fake_host_len}}}};
153 |
154 | spin_http_request_t fake_req = {
155 | .method = SPIN_HTTP_METHOD_GET,
156 | .uri = { warmup_url, warmup_url_len },
157 | .headers = fake_headers,
158 | .body = { .is_some = 1, .val = { (void*)"Hello", 5 } }
159 | };
160 | spin_http_response_t fake_res;
161 | process_http_request(&fake_req, &fake_res);
162 | }
163 |
164 | void spin_http_handle_http_request(spin_http_request_t *req, spin_http_response_t *ret0) {
165 | __wasm_call_ctors();
166 |
167 | if (!preinitialized) {
168 | initialize();
169 | }
170 | if (initialization_error) {
171 | *ret0 = internal_error(initialization_error);
172 | return;
173 | }
174 |
175 | process_http_request(req, ret0);
176 | }
177 |
--------------------------------------------------------------------------------
/src/HttpInterop.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using System.Net;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace Fermyon.Spin.Sdk;
7 |
8 | ///
9 | /// An HTTP method.
10 | ///
11 | public enum HttpMethod : byte
12 | {
13 | ///
14 | /// The GET method.
15 | ///
16 | Get = 0,
17 | ///
18 | /// The POST method.
19 | ///
20 | Post = 1,
21 | ///
22 | /// The PUT method.
23 | ///
24 | Put = 2,
25 | ///
26 | /// The DELETE method.
27 | ///
28 | Delete = 3,
29 | ///
30 | /// The PATCH method.
31 | ///
32 | Patch = 4,
33 | ///
34 | /// The HEAD method.
35 | ///
36 | Head = 5,
37 | ///
38 | /// The OPTIONS method.
39 | ///
40 | Options = 6,
41 | }
42 |
43 | ///
44 | /// A HTTP response.
45 | ///
46 | [StructLayout(LayoutKind.Sequential)]
47 | public struct HttpResponse
48 | {
49 | private static IReadOnlyDictionary Empty = ImmutableDictionary.Create();
50 | private int _status;
51 | private Optional _headers;
52 | ///
53 | /// Gets or sets the response body. This provides access to the raw Wasm Canonical ABI
54 | /// representation - applications will usually find it move convenient to use BodyAsString
55 | /// or BodyAsBytes.
56 | ///
57 | public Optional Body;
58 |
59 | ///
60 | /// Gets or sets the response HTTP status code.
61 | ///
62 | public HttpStatusCode StatusCode
63 | {
64 | get => (HttpStatusCode)_status;
65 | set => _status = (int)value;
66 | }
67 |
68 | ///
69 | /// Gets or sets the response headers.
70 | ///
71 | public IReadOnlyDictionary Headers
72 | {
73 | get => _headers.TryGetValue(out var headers) ? headers : Empty;
74 | set => _headers = value.Count == 0 ? Optional.None : Optional.From(HttpKeyValues.FromDictionary(value));
75 | }
76 |
77 | ///
78 | /// Gets or sets the response body as a string.
79 | ///
80 | public string? BodyAsString
81 | {
82 | get => Body.TryGetValue(out var buffer) ? buffer.ToInteropString().ToString() : null;
83 | set => Body = value is null ? Optional.None : Optional.From(Buffer.FromString(value));
84 | }
85 |
86 | ///
87 | /// Gets or sets the response body as a sequence of bytes.
88 | ///
89 | public IEnumerable BodyAsBytes
90 | {
91 | get => Body.TryGetValue(out var buffer) ? buffer : Enumerable.Empty();
92 | set => Body = value is null ? Optional.None : Optional.From(Buffer.FromBytes(value));
93 | }
94 | }
95 |
96 | ///
97 | /// A HTTP request.
98 | ///
99 | [StructLayout(LayoutKind.Sequential)]
100 | public struct HttpRequest
101 | {
102 | private HttpMethod _method;
103 | private InteropString _url;
104 | private HttpKeyValues _headers;
105 | private HttpKeyValues _parameters_unused;
106 | private Optional _body;
107 |
108 | ///
109 | /// Gets or sets the request method.
110 | ///
111 | public HttpMethod Method
112 | {
113 | get => _method;
114 | set => _method = value;
115 | }
116 |
117 | ///
118 | /// Gets or sets the request URL.
119 | ///
120 | public string Url
121 | {
122 | get => _url.ToString();
123 | set => _url = InteropString.FromString(value);
124 | }
125 |
126 | ///
127 | /// Gets or sets the request headers.
128 | ///
129 | public IReadOnlyDictionary Headers
130 | {
131 | get => _headers;
132 | set => _headers = HttpKeyValues.FromDictionary(value);
133 | }
134 |
135 | ///
136 | /// Gets or sets the request body.
137 | ///
138 | public Optional Body
139 | {
140 | get => _body;
141 | set => _body = value;
142 | }
143 | }
144 |
145 | ///
146 | /// A set of key-value pairs in the Canonical ABI, such as headers or query string parameters.
147 | ///
148 | ///
149 | [StructLayout(LayoutKind.Sequential)]
150 | public unsafe readonly struct HttpKeyValues : IReadOnlyDictionary
151 | {
152 | private readonly HttpKeyValue* _valuesPtr;
153 | private readonly int _valuesLen;
154 |
155 | internal HttpKeyValues(HttpKeyValue* ptr, int length)
156 | {
157 | _valuesPtr = ptr;
158 | _valuesLen = length;
159 | }
160 |
161 | ///
162 | /// Createa a Canonical ABI representation from a .NET dictionary.
163 | ///
164 | public static HttpKeyValues FromDictionary(IReadOnlyDictionary dictionary)
165 | {
166 | var unmanagedValues = (HttpKeyValue*)Marshal.AllocHGlobal(dictionary.Count * sizeof(HttpKeyValue));
167 | var span = new Span(unmanagedValues, dictionary.Count);
168 | var index = 0;
169 | foreach (var (key, value) in dictionary)
170 | {
171 | span[index] = new HttpKeyValue(InteropString.FromString(key), InteropString.FromString(value));
172 | index++;
173 | }
174 | return new HttpKeyValues(unmanagedValues, dictionary.Count);
175 | }
176 |
177 | private Span AsSpan()
178 | => new Span(_valuesPtr, _valuesLen);
179 |
180 | // IReadOnlyDictionary
181 |
182 | ///
183 | public bool ContainsKey(string key) => false;
184 | ///
185 | public bool TryGetValue(string key, out string value)
186 | {
187 | foreach (var entry in AsSpan())
188 | {
189 | if (entry.Key.ToString() == key)
190 | {
191 | value = entry.Value.ToString();
192 | return true;
193 | }
194 | }
195 | value = String.Empty;
196 | return false;
197 | }
198 | ///
199 | public string this[string key] => TryGetValue(key, out var value) ? value : throw new KeyNotFoundException(key);
200 | ///
201 | public IEnumerable Keys => this.Select(kvp => kvp.Key);
202 | ///
203 | public IEnumerable Values => this.Select(kvp => kvp.Value);
204 | ///
205 | public int Count => _valuesLen;
206 | ///
207 | public IEnumerator> GetEnumerator() => new Enumerator(_valuesPtr, _valuesLen);
208 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
209 |
210 | // We can't lazy enumerate by foreach-yielding over a Span because
211 | // ref struct so POINTER ARITHMETIC AVENGERS ASSEMBLE
212 | private struct Enumerator : IEnumerator>
213 | {
214 | private HttpKeyValue* _valuesPtr;
215 | private int _valuesLen;
216 | private int _index = -1;
217 |
218 | public Enumerator(HttpKeyValue* valuesPtr, int valuesLen)
219 | {
220 | _valuesPtr = valuesPtr;
221 | _valuesLen = valuesLen;
222 | }
223 |
224 | public KeyValuePair Current
225 | {
226 | get
227 | {
228 | if (_index < 0 || _index >= _valuesLen)
229 | {
230 | throw new InvalidOperationException();
231 | }
232 | var ptr = _valuesPtr + _index;
233 | return KeyValuePair.Create(ptr->Key.ToString(), ptr->Value.ToString());
234 | }
235 | }
236 |
237 | public bool MoveNext()
238 | {
239 | ++_index;
240 | return _index < _valuesLen;
241 | }
242 |
243 | public void Reset()
244 | {
245 | throw new NotSupportedException();
246 | }
247 |
248 | object System.Collections.IEnumerator.Current => Current;
249 | void IDisposable.Dispose() {}
250 | }
251 | }
252 |
253 | [StructLayout(LayoutKind.Sequential)]
254 | internal readonly struct HttpKeyValue
255 | {
256 | public readonly InteropString Key;
257 | public readonly InteropString Value;
258 |
259 | internal HttpKeyValue(InteropString key, InteropString value)
260 | {
261 | Key = key;
262 | Value = value;
263 | }
264 | }
265 |
266 | internal static class OutboundHttpInterop
267 | {
268 | [MethodImpl(MethodImplOptions.InternalCall)]
269 | internal static extern unsafe byte wasi_outbound_http_request(ref HttpRequest req, ref HttpResponse ret0);
270 | }
271 |
--------------------------------------------------------------------------------
/src/native/outbound-pg.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include "outbound-pg.h"
3 |
4 | __attribute__((weak, export_name("canonical_abi_realloc")))
5 | void *canonical_abi_realloc(
6 | void *ptr,
7 | size_t orig_size,
8 | size_t org_align,
9 | size_t new_size
10 | ) {
11 | void *ret = realloc(ptr, new_size);
12 | if (!ret)
13 | abort();
14 | return ret;
15 | }
16 |
17 | __attribute__((weak, export_name("canonical_abi_free")))
18 | void canonical_abi_free(
19 | void *ptr,
20 | size_t size,
21 | size_t align
22 | ) {
23 | free(ptr);
24 | }
25 | #include
26 |
27 | void outbound_pg_string_set(outbound_pg_string_t *ret, const char *s) {
28 | ret->ptr = (char*) s;
29 | ret->len = strlen(s);
30 | }
31 |
32 | void outbound_pg_string_dup(outbound_pg_string_t *ret, const char *s) {
33 | ret->len = strlen(s);
34 | ret->ptr = canonical_abi_realloc(NULL, 0, 1, ret->len);
35 | memcpy(ret->ptr, s, ret->len);
36 | }
37 |
38 | void outbound_pg_string_free(outbound_pg_string_t *ret) {
39 | canonical_abi_free(ret->ptr, ret->len, 1);
40 | ret->ptr = NULL;
41 | ret->len = 0;
42 | }
43 | void outbound_pg_pg_error_free(outbound_pg_pg_error_t *ptr) {
44 | switch ((int32_t) ptr->tag) {
45 | case 1: {
46 | outbound_pg_string_free(&ptr->val.connection_failed);
47 | break;
48 | }
49 | case 2: {
50 | outbound_pg_string_free(&ptr->val.bad_parameter);
51 | break;
52 | }
53 | case 3: {
54 | outbound_pg_string_free(&ptr->val.query_failed);
55 | break;
56 | }
57 | case 4: {
58 | outbound_pg_string_free(&ptr->val.value_conversion_failed);
59 | break;
60 | }
61 | case 5: {
62 | outbound_pg_string_free(&ptr->val.other_error);
63 | break;
64 | }
65 | }
66 | }
67 | void outbound_pg_column_free(outbound_pg_column_t *ptr) {
68 | outbound_pg_string_free(&ptr->name);
69 | }
70 | void outbound_pg_list_u8_free(outbound_pg_list_u8_t *ptr) {
71 | canonical_abi_free(ptr->ptr, ptr->len * 1, 1);
72 | }
73 | void outbound_pg_db_value_free(outbound_pg_db_value_t *ptr) {
74 | switch ((int32_t) ptr->tag) {
75 | case 11: {
76 | outbound_pg_string_free(&ptr->val.str);
77 | break;
78 | }
79 | case 12: {
80 | outbound_pg_list_u8_free(&ptr->val.binary);
81 | break;
82 | }
83 | }
84 | }
85 | void outbound_pg_parameter_value_free(outbound_pg_parameter_value_t *ptr) {
86 | switch ((int32_t) ptr->tag) {
87 | case 11: {
88 | outbound_pg_string_free(&ptr->val.str);
89 | break;
90 | }
91 | case 12: {
92 | outbound_pg_list_u8_free(&ptr->val.binary);
93 | break;
94 | }
95 | }
96 | }
97 | void outbound_pg_row_free(outbound_pg_row_t *ptr) {
98 | for (size_t i = 0; i < ptr->len; i++) {
99 | outbound_pg_db_value_free(&ptr->ptr[i]);
100 | }
101 | canonical_abi_free(ptr->ptr, ptr->len * 16, 8);
102 | }
103 | void outbound_pg_list_column_free(outbound_pg_list_column_t *ptr) {
104 | for (size_t i = 0; i < ptr->len; i++) {
105 | outbound_pg_column_free(&ptr->ptr[i]);
106 | }
107 | canonical_abi_free(ptr->ptr, ptr->len * 12, 4);
108 | }
109 | void outbound_pg_list_row_free(outbound_pg_list_row_t *ptr) {
110 | for (size_t i = 0; i < ptr->len; i++) {
111 | outbound_pg_row_free(&ptr->ptr[i]);
112 | }
113 | canonical_abi_free(ptr->ptr, ptr->len * 8, 4);
114 | }
115 | void outbound_pg_row_set_free(outbound_pg_row_set_t *ptr) {
116 | outbound_pg_list_column_free(&ptr->columns);
117 | outbound_pg_list_row_free(&ptr->rows);
118 | }
119 | void outbound_pg_list_parameter_value_free(outbound_pg_list_parameter_value_t *ptr) {
120 | for (size_t i = 0; i < ptr->len; i++) {
121 | outbound_pg_parameter_value_free(&ptr->ptr[i]);
122 | }
123 | canonical_abi_free(ptr->ptr, ptr->len * 16, 8);
124 | }
125 | void outbound_pg_expected_row_set_pg_error_free(outbound_pg_expected_row_set_pg_error_t *ptr) {
126 | if (!ptr->is_err) {
127 | outbound_pg_row_set_free(&ptr->val.ok);
128 | } else {
129 | outbound_pg_pg_error_free(&ptr->val.err);
130 | }
131 | }
132 | void outbound_pg_expected_u64_pg_error_free(outbound_pg_expected_u64_pg_error_t *ptr) {
133 | if (!ptr->is_err) {
134 | } else {
135 | outbound_pg_pg_error_free(&ptr->val.err);
136 | }
137 | }
138 |
139 | __attribute__((aligned(8)))
140 | static uint8_t RET_AREA[20];
141 | __attribute__((import_module("outbound-pg"), import_name("query")))
142 | void __wasm_import_outbound_pg_query(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
143 | void outbound_pg_query(outbound_pg_string_t *address, outbound_pg_string_t *statement, outbound_pg_list_parameter_value_t *params, outbound_pg_expected_row_set_pg_error_t *ret0) {
144 | int32_t ptr = (int32_t) &RET_AREA;
145 | __wasm_import_outbound_pg_query((int32_t) (*address).ptr, (int32_t) (*address).len, (int32_t) (*statement).ptr, (int32_t) (*statement).len, (int32_t) (*params).ptr, (int32_t) (*params).len, ptr);
146 | outbound_pg_expected_row_set_pg_error_t expected;
147 | switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
148 | case 0: {
149 | expected.is_err = false;
150 |
151 | expected.val.ok = (outbound_pg_row_set_t) {
152 | (outbound_pg_list_column_t) { (outbound_pg_column_t*)(*((int32_t*) (ptr + 4))), (size_t)(*((int32_t*) (ptr + 8))) },
153 | (outbound_pg_list_row_t) { (outbound_pg_row_t*)(*((int32_t*) (ptr + 12))), (size_t)(*((int32_t*) (ptr + 16))) },
154 | };
155 | break;
156 | }
157 | case 1: {
158 | expected.is_err = true;
159 | outbound_pg_pg_error_t variant13;
160 | variant13.tag = (int32_t) (*((uint8_t*) (ptr + 4)));
161 | switch ((int32_t) variant13.tag) {
162 | case 0: {
163 | break;
164 | }
165 | case 1: {
166 | variant13.val.connection_failed = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
167 | break;
168 | }
169 | case 2: {
170 | variant13.val.bad_parameter = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
171 | break;
172 | }
173 | case 3: {
174 | variant13.val.query_failed = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
175 | break;
176 | }
177 | case 4: {
178 | variant13.val.value_conversion_failed = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
179 | break;
180 | }
181 | case 5: {
182 | variant13.val.other_error = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
183 | break;
184 | }
185 | }
186 |
187 | expected.val.err = variant13;
188 | break;
189 | }
190 | }*ret0 = expected;
191 | }
192 | __attribute__((import_module("outbound-pg"), import_name("execute")))
193 | void __wasm_import_outbound_pg_execute(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
194 | void outbound_pg_execute(outbound_pg_string_t *address, outbound_pg_string_t *statement, outbound_pg_list_parameter_value_t *params, outbound_pg_expected_u64_pg_error_t *ret0) {
195 | int32_t ptr = (int32_t) &RET_AREA;
196 | __wasm_import_outbound_pg_execute((int32_t) (*address).ptr, (int32_t) (*address).len, (int32_t) (*statement).ptr, (int32_t) (*statement).len, (int32_t) (*params).ptr, (int32_t) (*params).len, ptr);
197 | outbound_pg_expected_u64_pg_error_t expected;
198 | switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
199 | case 0: {
200 | expected.is_err = false;
201 |
202 | expected.val.ok = (uint64_t) (*((int64_t*) (ptr + 8)));
203 | break;
204 | }
205 | case 1: {
206 | expected.is_err = true;
207 | outbound_pg_pg_error_t variant;
208 | variant.tag = (int32_t) (*((uint8_t*) (ptr + 8)));
209 | switch ((int32_t) variant.tag) {
210 | case 0: {
211 | break;
212 | }
213 | case 1: {
214 | variant.val.connection_failed = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 12))), (size_t)(*((int32_t*) (ptr + 16))) };
215 | break;
216 | }
217 | case 2: {
218 | variant.val.bad_parameter = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 12))), (size_t)(*((int32_t*) (ptr + 16))) };
219 | break;
220 | }
221 | case 3: {
222 | variant.val.query_failed = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 12))), (size_t)(*((int32_t*) (ptr + 16))) };
223 | break;
224 | }
225 | case 4: {
226 | variant.val.value_conversion_failed = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 12))), (size_t)(*((int32_t*) (ptr + 16))) };
227 | break;
228 | }
229 | case 5: {
230 | variant.val.other_error = (outbound_pg_string_t) { (char*)(*((int32_t*) (ptr + 12))), (size_t)(*((int32_t*) (ptr + 16))) };
231 | break;
232 | }
233 | }
234 |
235 | expected.val.err = variant;
236 | break;
237 | }
238 | }*ret0 = expected;
239 | }
240 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Spin SDK for .NET Preview
2 |
3 | An experimental SDK for building Spin application components using .NET.
4 |
5 | ### Features
6 |
7 | * Handle HTTP requests using the Spin executor
8 | * Make outbound HTTP requests
9 | * Access Postgres databases
10 | * Make outbound Redis calls
11 | * Fast startup by preparing the .NET runtime during Wasm compilation (via Wizer)
12 |
13 | ### Prerequisites
14 |
15 | You'll need the following to build Spin applications using this SDK:
16 |
17 | - [Spin](https://spin.fermyon.dev) v0.5.0 or above
18 | - [.NET 8+](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
19 | - [Wizer](https://github.com/bytecodealliance/wizer/releases) - download and place it on your PATH
20 | - If you have Rust installed, you can install Wizer by running `make bootstrap` in the root of the SDK repo
21 |
22 | ### Getting the Spin SDK
23 |
24 | You can get the SDK itself from NuGet via `dotnet add package Fermyon.Spin.Sdk --prerelease`, or use
25 | the provided template - see below for details.
26 |
27 | ### Building the "hello world" sample
28 |
29 | To build and run the `hello-world` sample, clone this repo and run:
30 |
31 | ```
32 | $ cd samples/hello-world
33 | $ spin build --up
34 | ```
35 |
36 | If everything worked, you should see a Spin "serving routes" message:
37 |
38 | ```
39 | Serving http://127.0.0.1:3000
40 | Available Routes:
41 | hello: http://127.0.0.1:3000 (wildcard)
42 | ```
43 |
44 | You should be able to curl the address and get a response along the lines of:
45 |
46 | ```
47 | $ curl -v 127.0.0.1:3000
48 | // outbound trace omitted
49 | < HTTP/1.1 200 OK
50 | < content-type: text/plain
51 | < x-testheader: this is a test
52 | < content-length: 451
53 | < date: Thu, 21 Jul 2022 03:11:15 GMT
54 | <
55 | Called with method Get on URL /
56 | Header 'host' had value '127.0.0.1:3000'
57 | // ... more headers info ...
58 | Header 'spin-component-route' had value ''
59 | The body was empty
60 | ```
61 |
62 | ### Installing the Spin application template
63 |
64 | The SDK includes a Spin template for C# projects. To install it, run:
65 |
66 | ```
67 | spin templates install --git https://github.com/fermyon/spin-dotnet-sdk --branch main --update
68 | ```
69 |
70 | You can then run `spin new -t http-csharp ` to create a new Spin C# application.
71 |
72 | > If you're creating a project without using the Spin template, add a reference the Spin SDK with the command
73 | > `dotnet add package Fermyon.Spin.Sdk --prerelease`
74 |
75 | ### Handling HTTP requests
76 |
77 | Your .NET project should contain a method with the `Fermyon.Spin.Sdk.HttpHandler` attribute.
78 | This method must be `static`, and must take one argument of type `Fermyon.Spin.Sdk.HttpRequest`
79 | and return a `Fermyon.Spin.Sdk.HttpResponse`.
80 |
81 | ```csharp
82 | using Fermyon.Spin.Sdk;
83 |
84 | public static class MyHandler
85 | {
86 | [HttpHandler]
87 | public static HttpResponse HandleHttpRequest(HttpRequest request)
88 | {
89 | // ...
90 | }
91 | }
92 | ```
93 |
94 | Your `spin.toml` file should reference the compiled Wasm file built from the project.
95 |
96 | ```toml
97 | [component.test]
98 | source = "bin/Release/net8.0/MyApplication.wasm"
99 | ```
100 |
101 | ### Making outbound HTTP requests
102 |
103 | To make outbound HTTP requests, use the `HttpOutbound.Send()` method.
104 |
105 | For an example of constructing an outbound request, see the `UseOutboundHttp` method
106 | in the `hello-world` sample.
107 |
108 | ### Making Redis requests
109 |
110 | To make outbound Redis requests, use the methods of the `RedisOutbound` class -
111 | `Get`, `Set` and `Publish`.
112 |
113 | For examples of making Redis requests, see the `UseRedis` method
114 | in the `hello-world` sample.
115 |
116 | ### Working with Postgres
117 |
118 | To access Postgres databases, use the methods of the `PostgresOutbound` class -
119 | `Query` for statements that return database values (`SELECT`), and `Execute`
120 | for statements that modify the database (`INSERT`, `UPDATE`, `DELETE`).
121 |
122 | For examples of making Postgres requests, see the `UsePostgresQuery` and
123 | `UsePostgresExec` methods in the `hello-world` sample, or see the
124 | `Fermyon.PetStore` sample.
125 |
126 | ### Accessing Spin configuration
127 |
128 | To access Spin configuration, use the `SpinConfig.Get()` method.
129 |
130 | > It is not expected that a Spin component will try to access config entries
131 | > that don't exist. At the moment, the only way to detect if a config setting
132 | > is missing is to catch the exception from `Get`.
133 |
134 | For examples of accessing configuration, see the samples.
135 |
136 | ### Working with Buffers
137 |
138 | Both HTTP and Redis represent payload blobs using the `Buffer` type, and Postgres also
139 | uses `Buffer` for values of `binary` (aka 'blob') type. Buffer represents
140 | an unmanaged span of Wasm linear memory. The SDK provides several convenience methods
141 | to make it easier to work with. For example:
142 |
143 | * Use `HttpRequest.Body.AsString()` and `HttpRequest.Body.AsBytes()` to read a request
144 | body as text or a binary blob.
145 | * Use `Buffer.ToUTF8String()` to read an arbitrary Buffer as string.
146 | * Use `HttpResponse.BodyAsString` and `HttpResponse.BodyAsBytes` to set the body of
147 | a HTTP response.
148 | * Use `Buffer.FromString()` to write a string into a buffer using the UTF-8 encoding.
149 | * Use `Buffer.FromBytes()` to write any sequence of bytes into a buffer.
150 |
151 | ### Fast startup using Wizer
152 |
153 | If your project file (`.csproj`) contains `true`, then `dotnet build`
154 | will run [Wizer](https://github.com/bytecodealliance/wizer) to pre-initialise your
155 | Wasm file. Wizer will run a request through your application _at compile time_ and snapshot
156 | the state of your Wasm module at the end of the request. This means that the resulting
157 | Wasm module contains the .NET runtime in a state where it is already loaded (and the
158 | interpreter has already seen your code), saving startup time when a request comes in at runtime.
159 |
160 | > You must run install [Wizer](https://github.com/bytecodealliance/wizer/releases) and place
161 | > it on your path (or run `make bootstrap`).
162 |
163 | Using Wizer has certain observable impacts:
164 |
165 | * You should not (and in some cases cannot) call external services from the warmup request
166 | handler. If your handler talks to HTTP or Redis, you _must skip those calls at warmup time_.
167 | If the warmup code fails, then the build will fail.
168 | * Static constructors and static member initialisation happens at warmup time (at least for
169 | any type used on the warmup path). For example, if your handler type has a static
170 | `Random` member, and this gets initialised during warmup, then _the same state of the random
171 | number generator_ will be used in all requests!
172 |
173 | You can identify if a request is the warmup request because the URL will be `/warmupz`.
174 | You can override this in the `HttpHandler` attribute. However, it is not currently possible
175 | to have Wizer initialise the runtime but omit calling your handler.
176 |
177 | If your handler logic doesn't require any external services then you don't need any special
178 | warmup handling. Otherwise, you'll need to guard those calls, or skip your real handler
179 | altogether, e.g.:
180 |
181 | ```csharp
182 | [HttpHandler]
183 | public static HttpResponse HandleHttpRequest(HttpRequest request)
184 | {
185 | if (request.Url == Warmup.DefaultWarmupUrl)
186 | {
187 | return new HttpResponse
188 | {
189 | StatusCode = HttpStatusCode.OK,
190 | Headers = new Dictionary
191 | {
192 | { "Content-Type", "text/plain" },
193 | },
194 | BodyAsString = "warmup",
195 | };
196 | }
197 |
198 | // ... real handler goes here ...
199 | }
200 | ```
201 |
202 | ## Known issues
203 |
204 | The Spin .NET SDK is a preview, built on an implementation of .NET that is currently experimental.
205 | There are several known issues, of which the most severe are:
206 |
207 | * Some static methods and properties cause a "indirect call type mismatch" error when Wizer is turned
208 | on - we have seen this on numeric parse methods and `StringComparer` properties.
209 | You can work around this by turning Wizer off for affected modules. To do this, change
210 | `true` in the `.csproj` to `false`.
211 | * In some cases, unhandled exceptions also cause "indirect call type mismatch" instead of being
212 | returned as 500 Internal Server Error responses. You can work around this by catching problematic
213 | exceptions and returning error responses manually.
214 |
215 | You can track issues or report problems at https://github.com/fermyon/spin-dotnet-sdk/issues.
216 |
217 | ## What's next
218 |
219 | The initial version of the SDK closely mirrors the underlying low-level Spin interop interfaces.
220 | This maximises performance but doesn't provide an idiomatic experience for .NET developers.
221 | We'll be aiming to improve that over future releases, and welcome contributions or suggestions!
222 |
--------------------------------------------------------------------------------
/samples/hello-world/Handler.cs:
--------------------------------------------------------------------------------
1 | using Fermyon.Spin.Sdk;
2 | using System.Net;
3 | using System.Text;
4 |
5 | namespace Fermyon.Spin.HelloWorld;
6 |
7 | public static class Handler
8 | {
9 | [HttpHandler]
10 | public static HttpResponse HandleHttpRequest(HttpRequest request)
11 | {
12 | if (request.Url.StartsWith("/outbound"))
13 | {
14 | return UseOutboundHttp(request);
15 | }
16 | return request.Url switch
17 | {
18 | "/redis" => UseRedis(request),
19 | "/pg" => UsePostgresQuery(request),
20 | "/pgins" => UsePostgresExec(request),
21 | _ => EchoRequestInfo(request),
22 | };
23 | }
24 |
25 | private static HttpResponse UseOutboundHttp(HttpRequest request)
26 | {
27 | var onboundRequest = new HttpRequest
28 | {
29 | Method = Fermyon.Spin.Sdk.HttpMethod.Delete,
30 | Url = "http://127.0.0.1:3000/testingtesting?thing=otherthing",
31 | Headers = HttpKeyValues.FromDictionary(new Dictionary
32 | {
33 | { "X-Outbound-Test", "From .NET" },
34 | { "Accept", "text/plain" },
35 | }),
36 | Body = Optional.From(Buffer.FromString("see the little goblin, see his little feet")),
37 | };
38 |
39 | string onboundInfo;
40 |
41 | try
42 | {
43 | var response = HttpOutbound.Send(onboundRequest);
44 | var status = response.StatusCode;
45 | var onboundSucceeded = (int)status >= 200 && (int)status <= 299;
46 | var onboundResponseText = status == HttpStatusCode.OK ?
47 | response.BodyAsString :
48 | "";
49 | onboundInfo = onboundSucceeded ?
50 | $"The onbound request returned status {status} with {response.Headers.Count} headers ({FormatHeadersShort(response.Headers)}) and the body was:\n{onboundResponseText}\n" :
51 | $"Tragically the onbound request failed with code {status}\n";
52 | }
53 | catch (Exception ex)
54 | {
55 | onboundInfo = $"Onbound call exception {ex}";
56 | }
57 |
58 | var responseText = new StringBuilder();
59 | responseText.AppendLine($"Called with method {request.Method}, Url {request.Url}");
60 |
61 | responseText.AppendLine($"The spin-full-url header was {request.Headers["spin-full-url"]}");
62 | foreach (var h in request.Headers)
63 | {
64 | responseText.AppendLine($"Header '{h.Key}' had value '{h.Value}'");
65 | }
66 |
67 | var uri = new System.Uri(request.Headers["spin-full-url"]);
68 | var queryParameters = System.Web.HttpUtility.ParseQueryString(uri.Query);
69 | foreach (var key in queryParameters.AllKeys)
70 | {
71 | responseText.AppendLine($"Parameter '{key}' had value '{queryParameters[key]}'");
72 | }
73 |
74 | var bodyInfo = request.Body.HasContent() ?
75 | $"The body (as a string) was: {request.Body.AsString()}\n" :
76 | "The body was empty\n";
77 | responseText.AppendLine(bodyInfo);
78 |
79 | responseText.AppendLine(onboundInfo);
80 |
81 | return new HttpResponse
82 | {
83 | StatusCode = HttpStatusCode.OK,
84 | Headers = new Dictionary
85 | {
86 | { "Content-Type", "text/plain" },
87 | { "X-TestHeader", "this is a test" },
88 | },
89 | BodyAsString = responseText.ToString(),
90 | // BodyAsBytes = RandomTextBytes(),
91 | };
92 | }
93 |
94 | private static HttpResponse EchoRequestInfo(HttpRequest request)
95 | {
96 | // Warmup
97 |
98 | var responseText = new StringBuilder();
99 | responseText.AppendLine($"Called with method {request.Method}, Url {request.Url}");
100 |
101 | foreach (var h in request.Headers)
102 | {
103 | responseText.AppendLine($"Header '{h.Key}' had value '{h.Value}'");
104 | }
105 |
106 | var uri = new System.Uri(request.Headers["spin-full-url"]);
107 | var queryParameters = System.Web.HttpUtility.ParseQueryString(uri.Query);
108 | foreach (var key in queryParameters.AllKeys)
109 | {
110 | responseText.AppendLine($"Parameter '{key}' had value '{queryParameters[key]}'");
111 | }
112 |
113 | var bodyInfo = request.Body.HasContent() ?
114 | $"The body (as a string) was: {request.Body.AsString()}\n" :
115 | "The body was empty\n";
116 | responseText.AppendLine(bodyInfo);
117 |
118 | if (request.Url != Warmup.DefaultWarmupUrl)
119 | {
120 | responseText.AppendLine("We now present the contents of a static asset:");
121 | responseText.AppendLine(File.ReadAllText("/assets/asset-text.txt"));
122 | responseText.AppendLine("And here are some config strings:");
123 | responseText.AppendLine($"- 'defaulted' has value {SpinConfig.Get("defaulted")}");
124 | try
125 | {
126 | var requiredCfg = SpinConfig.Get("required");
127 | responseText.AppendLine($"- 'required' has value {requiredCfg}");
128 | }
129 | catch
130 | {
131 | responseText.AppendLine("- Oh no! 'required' was not set!");
132 | }
133 | responseText.AppendLine("We hope you enjoyed this external data!");
134 | }
135 |
136 | return new HttpResponse
137 | {
138 | StatusCode = HttpStatusCode.OK,
139 | Headers = new Dictionary
140 | {
141 | { "Content-Type", "text/plain" },
142 | { "X-TestHeader", "this is a test" },
143 | },
144 | BodyAsString = responseText.ToString(),
145 | };
146 | }
147 |
148 | private static IEnumerable RandomTextBytes()
149 | {
150 | Random r = new Random();
151 | var a = (int)'a';
152 | while (true)
153 | {
154 | int rv = r.Next(25);
155 | yield return (byte)(a + rv);
156 | if (rv == 0)
157 | {
158 | yield break;
159 | }
160 | }
161 | }
162 |
163 |
164 | private static HttpResponse UseRedis(HttpRequest request)
165 | {
166 | var address = "redis://127.0.0.1:6379";
167 | var key = "mykey";
168 | var channel = "messages";
169 |
170 | var payload = request.Body.TryGetValue(out var bodyBuffer) ? bodyBuffer : throw new Exception("cannot read body");
171 | RedisOutbound.Set(address, key, payload);
172 |
173 | var res = RedisOutbound.Get(address, key).ToUTF8String();
174 |
175 | RedisOutbound.Publish(address, channel, payload);
176 |
177 | return new HttpResponse
178 | {
179 | StatusCode = HttpStatusCode.OK,
180 | BodyAsString = res
181 | };
182 | }
183 |
184 | private static HttpResponse UsePostgresQuery(HttpRequest request)
185 | {
186 | var connectionString = "user=ivan password=pg314159$ dbname=ivantest host=127.0.0.1";
187 |
188 | var result = PostgresOutbound.Query(connectionString, "SELECT * FROM test");
189 |
190 | var responseText = new StringBuilder();
191 |
192 | responseText.AppendLine($"Got {result.Rows.Count} row(s)");
193 | responseText.AppendLine($"COL: [{String.Join(" | ", result.Columns.Select(FmtCol))}]");
194 |
195 | string FmtEntry(DbValue v)
196 | {
197 | return v.Value() switch
198 | {
199 | null => "",
200 | var val => val.ToString() ?? "",
201 | };
202 | }
203 |
204 | foreach (var row in result.Rows)
205 | {
206 | responseText.AppendLine($"ROW: [{String.Join(" | ", row.Select(FmtEntry))}]");
207 | }
208 |
209 | return new HttpResponse
210 | {
211 | StatusCode = HttpStatusCode.OK,
212 | BodyAsString = responseText.ToString(),
213 | };
214 | }
215 |
216 | private static HttpResponse UsePostgresExec(HttpRequest request)
217 | {
218 | var connectionString = "user=ivan password=pg314159$ dbname=ivantest host=127.0.0.1";
219 |
220 | var id = new Random().Next(100000);
221 | var result = PostgresOutbound.Execute(connectionString, "INSERT INTO test VALUES ($1, 'something', 'something else')", id);
222 |
223 | var responseText = $"Updates {result} rows\n";
224 |
225 | return new HttpResponse
226 | {
227 | StatusCode = HttpStatusCode.OK,
228 | BodyAsString = responseText,
229 | };
230 | }
231 |
232 | private static string FormatHeadersShort(IReadOnlyDictionary headers)
233 | {
234 | if (headers.Count == 0)
235 | {
236 | return "";
237 | }
238 | return String.Join(" / ", headers.Select(kvp => $"{kvp.Key}={kvp.Value}"));
239 | }
240 |
241 | private static string FmtCol(PgColumn c)
242 | {
243 | return $"{c.Name} ({c.DataType})";
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/src/Interop.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using System.Text;
3 |
4 | ///
5 | /// An umanaged contiguous span of bytes.
6 | ///
7 | [StructLayout(LayoutKind.Sequential)]
8 | public readonly struct Buffer : IEnumerable
9 | {
10 | private readonly nint _ptr;
11 | private readonly int _length;
12 |
13 | private Buffer(nint ptr, int length)
14 | {
15 | _ptr = ptr;
16 | _length = length;
17 | }
18 |
19 | ///
20 | /// Gets the length of the Buffer.
21 | ///
22 | public int Length => _length;
23 |
24 | ///
25 | /// Gets the contents of the Buffer as a Span.
26 | ///
27 | public unsafe ReadOnlySpan AsSpan() => new ReadOnlySpan((void*)_ptr, _length);
28 |
29 | ///
30 | /// Creates a Buffer containing a string, encoding it using UTF-8.
31 | ///
32 | public static unsafe Buffer FromString(string value)
33 | {
34 | var interopString = InteropString.FromString(value);
35 | return new Buffer(interopString._utf8Ptr, interopString._utf8Length);
36 | }
37 |
38 | ///
39 | /// Creates a Buffer containing a sequence of bytes.
40 | ///
41 | public static unsafe Buffer FromBytes(IEnumerable value)
42 | {
43 | // We materialise this so as to get its length and avoid traversing twice,
44 | // but it does seem wasteful. TODO: better way?
45 | var source = new Span(value.ToArray());
46 | var exactByteCount = source.Length;
47 | var mem = Marshal.AllocHGlobal(exactByteCount);
48 | var buffer = new Span((void*)mem, exactByteCount);
49 | source.CopyTo(buffer);
50 | return new Buffer(mem, exactByteCount);
51 | }
52 |
53 | ///
54 | /// Gets the contents of the Buffer as a string, interpreting it using
55 | /// UTF-8 encoding.
56 | ///
57 | public string ToUTF8String()
58 | {
59 | return Encoding.UTF8.GetString(this.AsSpan());
60 | }
61 |
62 | internal InteropString ToInteropString()
63 | => new InteropString(_ptr, _length);
64 |
65 | ///
66 | /// Gets an iterator over the bytes in the Buffer.
67 | ///
68 | public IEnumerator GetEnumerator() => new Enumerator(_ptr, _length);
69 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
70 |
71 | private unsafe struct Enumerator : IEnumerator
72 | {
73 | private readonly byte* _ptr;
74 | private readonly int _length;
75 | private int _index = -1;
76 |
77 | public Enumerator(nint ptr, int length)
78 | {
79 | _ptr = (byte*)ptr;
80 | _length = length;
81 | }
82 |
83 | public byte Current
84 | {
85 | get
86 | {
87 | if (_index < 0 || _index >= _length)
88 | {
89 | throw new InvalidOperationException();
90 | }
91 | var ptr = _ptr + _index;
92 | return *ptr;
93 | }
94 | }
95 |
96 | public bool MoveNext()
97 | {
98 | ++_index;
99 | return _index < _length;
100 | }
101 |
102 | public void Reset()
103 | {
104 | throw new NotSupportedException();
105 | }
106 |
107 | object System.Collections.IEnumerator.Current => Current;
108 | void IDisposable.Dispose() {}
109 | }
110 |
111 | }
112 |
113 | ///
114 | /// Convenience methods for decoding optional Buffers.
115 | ///
116 | public static class OptionalBufferExtensions
117 | {
118 | ///
119 | /// Gets whether the option contains any content.
120 | ///
121 | public static bool HasContent(this Optional buffer)
122 | {
123 | return buffer.TryGetValue(out var value) && (value.Length > 0);
124 | }
125 |
126 | ///
127 | /// Gets the contents of the contained Buffer as a Span. If the Optional
128 | /// is None, the Span is empty.
129 | ///
130 | public static ReadOnlySpan AsBytes(this Optional buffer)
131 | {
132 | if (buffer.TryGetValue(out var value))
133 | {
134 | return value.AsSpan();
135 | }
136 | return new ReadOnlySpan(Array.Empty());
137 | }
138 |
139 | ///
140 | /// Gets the contents of the contained Buffer as a string. If the Optional
141 | /// is None, the string is empty.
142 | ///
143 | public static string AsString(this Optional buffer)
144 | {
145 | if (buffer.TryGetValue(out var value))
146 | {
147 | return value.ToUTF8String();
148 | }
149 | return String.Empty;
150 | }
151 | }
152 |
153 | ///
154 | /// An unmanaged struct that may or may not contain a T.
155 | ///
156 | [StructLayout(LayoutKind.Sequential)]
157 | public readonly struct Optional
158 | where T: struct
159 | {
160 | private readonly byte _isSome;
161 | private readonly T _value;
162 |
163 | internal Optional(T value)
164 | {
165 | _isSome = 1;
166 | _value = value;
167 | }
168 |
169 | ///
170 | /// Gets the contents of the Optional, if any. If the Optional is None, it returns
171 | /// false and the out variable is undefined. If the Optional contains a value,
172 | /// returns true and the value is copied into the out variable.
173 | ///
174 | public bool TryGetValue(out T value)
175 | {
176 | value = _value;
177 | return _isSome != 0;
178 | }
179 |
180 | ///
181 | /// An Optional representing the absence of a value.
182 | ///
183 | public static readonly Optional None = default;
184 |
185 | ///
186 | /// Surfaces the Optional as a C# nullable type.
187 | ///
188 | public static implicit operator T?(Optional opt) =>
189 | opt._isSome == 0 ? null : opt._value;
190 | }
191 |
192 | ///
193 | /// Convenience methods for constructing Optionals.
194 | ///
195 | public static class Optional
196 | {
197 | ///
198 | /// Constructs an Optional containing the specified value.
199 | ///
200 | public static Optional From(T value) where T: struct => new Optional(value);
201 | }
202 |
203 | ///
204 | /// The Wasm Canonical ABI representation of a string.
205 | ///
206 | [StructLayout(LayoutKind.Sequential)]
207 | public readonly struct InteropString
208 | {
209 | internal readonly nint _utf8Ptr;
210 | internal readonly int _utf8Length;
211 |
212 | internal InteropString(nint ptr, int length)
213 | {
214 | _utf8Ptr = ptr;
215 | _utf8Length = length;
216 | }
217 |
218 | ///
219 | /// Gets the string represented by the InteropString.
220 | ///
221 | public override string ToString()
222 | => Marshal.PtrToStringUTF8(_utf8Ptr, _utf8Length);
223 |
224 | ///
225 | /// Creates the Canonical ABI representation from a .NET string.
226 | ///
227 | public static unsafe InteropString FromString(string value)
228 | {
229 | var exactByteCount = checked(Encoding.UTF8.GetByteCount(value));
230 | var mem = Marshal.AllocHGlobal(exactByteCount);
231 | var buffer = new Span((void*)mem, exactByteCount);
232 | int byteCount = Encoding.UTF8.GetBytes(value, buffer);
233 | return new InteropString(mem, byteCount);
234 | }
235 | }
236 |
237 | [StructLayout(LayoutKind.Sequential)]
238 | internal unsafe readonly struct InteropStringList
239 | {
240 | private readonly InteropString* _ptr;
241 | private readonly int _len;
242 |
243 | internal InteropStringList(InteropString* ptr, int length)
244 | {
245 | _ptr = ptr;
246 | _len = length;
247 | }
248 |
249 | internal static InteropStringList FromStrings(string[] values)
250 | {
251 | var unmanagedValues = (InteropString*)Marshal.AllocHGlobal(values.Length * sizeof(InteropString));
252 | var span = new Span(unmanagedValues, values.Length);
253 | var index = 0;
254 | foreach (var value in values)
255 | {
256 | span[index] = InteropString.FromString(value);
257 | index++;
258 | }
259 | return new InteropStringList(unmanagedValues, values.Length);
260 | }
261 | }
262 |
263 | [StructLayout(LayoutKind.Sequential)]
264 | internal unsafe readonly struct InteropList : IEnumerable
265 | where T: unmanaged
266 | {
267 | internal readonly T* _ptr;
268 | internal readonly int _len;
269 |
270 | private InteropList(T* ptr, int len)
271 | {
272 | _ptr = ptr;
273 | _len = len;
274 | }
275 |
276 | public int Count => _len;
277 |
278 | public static InteropList From(T[] values)
279 | {
280 | var sourceSpan = new Span(values);
281 |
282 | var unmanagedValues = (T*)Marshal.AllocHGlobal(values.Length * sizeof(T));
283 | var span = new Span(unmanagedValues, values.Length);
284 | sourceSpan.CopyTo(span);
285 | return new InteropList(unmanagedValues, values.Length);
286 | }
287 |
288 | public IEnumerator GetEnumerator() => new Enumerator(_ptr, _len);
289 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
290 |
291 | private struct Enumerator : IEnumerator
292 | {
293 | private T* _ptr;
294 | private int _len;
295 | private int _index = -1;
296 |
297 | public Enumerator(T* ptr, int len)
298 | {
299 | _ptr = ptr;
300 | _len = len;
301 | }
302 |
303 | public T Current
304 | {
305 | get
306 | {
307 | if (_index < 0 || _index >= _len)
308 | {
309 | throw new InvalidOperationException();
310 | }
311 | var ptr = _ptr + _index;
312 | return *ptr;
313 | }
314 | }
315 |
316 | public bool MoveNext()
317 | {
318 | ++_index;
319 | return _index < _len;
320 | }
321 |
322 | public void Reset()
323 | {
324 | throw new NotSupportedException();
325 | }
326 |
327 | object System.Collections.IEnumerator.Current => Current;
328 | void IDisposable.Dispose() {}
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright (c) The Spin Framework Contributors. All Rights Reserved.
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 | --- LLVM Exceptions to the Apache 2.0 License ----
204 |
205 | As an exception, if, as a result of your compiling your source code, portions
206 | of this Software are embedded into an Object form of such source code, you
207 | may redistribute such embedded portions in such Object form without complying
208 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
209 |
210 | In addition, if you combine or link compiled forms of this Software with
211 | software that is licensed under the GPLv2 ("Combined Software") and if a
212 | court of competent jurisdiction determines that the patent provision (Section
213 | 3), the indemnity provision (Section 9) or other Section of the License
214 | conflicts with the conditions of the GPLv2, you may retroactively and
215 | prospectively choose to deem waived or otherwise exclude such Section(s) of
216 | the License, but only in their entirety and only with respect to the Combined
217 | Software.
218 |
--------------------------------------------------------------------------------
/src/PostgresInterop.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using System.Runtime.CompilerServices;
3 |
4 | namespace Fermyon.Spin.Sdk;
5 |
6 | ///
7 | /// A value retrieved from a Postgres database.
8 | ///
9 | [StructLayout(LayoutKind.Explicit)]
10 | public unsafe readonly struct DbValue {
11 | internal const byte OUTBOUND_PG_DB_VALUE_BOOLEAN = 0;
12 | internal const byte OUTBOUND_PG_DB_VALUE_INT8 = 1;
13 | internal const byte OUTBOUND_PG_DB_VALUE_INT16 = 2;
14 | internal const byte OUTBOUND_PG_DB_VALUE_INT32 = 3;
15 | internal const byte OUTBOUND_PG_DB_VALUE_INT64 = 4;
16 | internal const byte OUTBOUND_PG_DB_VALUE_UINT8 = 5;
17 | internal const byte OUTBOUND_PG_DB_VALUE_UINT16 = 6;
18 | internal const byte OUTBOUND_PG_DB_VALUE_UINT32 = 7;
19 | internal const byte OUTBOUND_PG_DB_VALUE_UINT64 = 8;
20 | internal const byte OUTBOUND_PG_DB_VALUE_FLOATING32 = 9;
21 | internal const byte OUTBOUND_PG_DB_VALUE_FLOATING64 = 10;
22 | internal const byte OUTBOUND_PG_DB_VALUE_STR = 11;
23 | internal const byte OUTBOUND_PG_DB_VALUE_BINARY = 12;
24 | internal const byte OUTBOUND_PG_DB_VALUE_DB_NULL = 13;
25 | internal const byte OUTBOUND_PG_DB_VALUE_UNSUPPORTED = 14;
26 |
27 | [FieldOffset(0)]
28 | internal readonly byte tag;
29 | [FieldOffset(8)]
30 | internal readonly bool boolean;
31 | [FieldOffset(8)]
32 | internal readonly sbyte int8;
33 | [FieldOffset(8)]
34 | internal readonly Int16 int16;
35 | [FieldOffset(8)]
36 | internal readonly Int32 int32;
37 | [FieldOffset(8)]
38 | internal readonly Int64 int64;
39 | [FieldOffset(8)]
40 | internal readonly byte uint8;
41 | [FieldOffset(8)]
42 | internal readonly UInt16 uint16;
43 | [FieldOffset(8)]
44 | internal readonly UInt32 uint32;
45 | [FieldOffset(8)]
46 | internal readonly UInt64 uint64;
47 | [FieldOffset(8)]
48 | internal readonly float floating32;
49 | [FieldOffset(8)]
50 | internal readonly double floating64;
51 | [FieldOffset(8)]
52 | internal readonly InteropString str;
53 | [FieldOffset(8)]
54 | internal readonly Buffer binary;
55 |
56 | ///
57 | /// Gets the value as a .NET object.
58 | ///
59 | public object? Value()
60 | {
61 | switch (tag)
62 | {
63 | case OUTBOUND_PG_DB_VALUE_BOOLEAN: return boolean;
64 | case OUTBOUND_PG_DB_VALUE_INT8: return int8;
65 | case OUTBOUND_PG_DB_VALUE_INT16: return int16;
66 | case OUTBOUND_PG_DB_VALUE_INT32: return int32;
67 | case OUTBOUND_PG_DB_VALUE_INT64: return int64;
68 | case OUTBOUND_PG_DB_VALUE_UINT8: return uint8;
69 | case OUTBOUND_PG_DB_VALUE_UINT16: return uint16;
70 | case OUTBOUND_PG_DB_VALUE_UINT32: return uint32;
71 | case OUTBOUND_PG_DB_VALUE_UINT64: return uint64;
72 | case OUTBOUND_PG_DB_VALUE_FLOATING32: return floating32;
73 | case OUTBOUND_PG_DB_VALUE_FLOATING64: return floating64;
74 | case OUTBOUND_PG_DB_VALUE_STR: return str.ToString();
75 | case OUTBOUND_PG_DB_VALUE_BINARY: return binary;
76 | case OUTBOUND_PG_DB_VALUE_DB_NULL: return null;
77 | default: throw new InvalidOperationException($"Spin doesn't support type {tag}");
78 | }
79 | }
80 | }
81 |
82 | [StructLayout(LayoutKind.Explicit)]
83 | internal unsafe readonly struct ParameterValue {
84 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_BOOLEAN = 0;
85 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_INT8 = 1;
86 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_INT16 = 2;
87 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_INT32 = 3;
88 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_INT64 = 4;
89 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_UINT8 = 5;
90 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_UINT16 = 6;
91 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_UINT32 = 7;
92 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_UINT64 = 8;
93 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_FLOATING32 = 9;
94 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_FLOATING64 = 10;
95 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_STR = 11;
96 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_BINARY = 12;
97 | internal const byte OUTBOUND_PG_PARAMETER_VALUE_DB_NULL = 13;
98 |
99 | public ParameterValue(bool value) : this()
100 | {
101 | tag = OUTBOUND_PG_PARAMETER_VALUE_BOOLEAN;
102 | boolean = value;
103 | }
104 |
105 | public ParameterValue(sbyte value) : this()
106 | {
107 | tag = OUTBOUND_PG_PARAMETER_VALUE_INT8;
108 | int8 = value;
109 | }
110 |
111 | public ParameterValue(short value) : this()
112 | {
113 | tag = OUTBOUND_PG_PARAMETER_VALUE_INT16;
114 | int16 = value;
115 | }
116 |
117 | public ParameterValue(int value) : this()
118 | {
119 | tag = OUTBOUND_PG_PARAMETER_VALUE_INT32;
120 | int32 = value;
121 | }
122 |
123 | public ParameterValue(long value) : this()
124 | {
125 | tag = OUTBOUND_PG_PARAMETER_VALUE_INT64;
126 | int64 = value;
127 | }
128 |
129 | public ParameterValue(float value) : this()
130 | {
131 | tag = OUTBOUND_PG_PARAMETER_VALUE_FLOATING32;
132 | floating32 = value;
133 | }
134 |
135 | public ParameterValue(double value) : this()
136 | {
137 | tag = OUTBOUND_PG_PARAMETER_VALUE_FLOATING64;
138 | floating64 = value;
139 | }
140 |
141 | public ParameterValue(string value) : this()
142 | {
143 | tag = OUTBOUND_PG_PARAMETER_VALUE_STR;
144 | str = InteropString.FromString(value);
145 | }
146 |
147 | public ParameterValue(IEnumerable value) : this()
148 | {
149 | tag = OUTBOUND_PG_PARAMETER_VALUE_BINARY;
150 | binary = Buffer.FromBytes(value);
151 | }
152 |
153 | public ParameterValue(object? value) : this()
154 | {
155 | if (value is null)
156 | {
157 | tag = OUTBOUND_PG_PARAMETER_VALUE_DB_NULL;
158 | }
159 | else
160 | {
161 | throw new ArgumentException(nameof(value));
162 | }
163 | }
164 |
165 | public static ParameterValue From(object? value)
166 | {
167 | return value switch
168 | {
169 | bool v => new ParameterValue(v),
170 | byte v => new ParameterValue(v),
171 | short v => new ParameterValue(v),
172 | int v => new ParameterValue(v),
173 | long v => new ParameterValue(v),
174 | float v => new ParameterValue(v),
175 | double v => new ParameterValue(v),
176 | string v => new ParameterValue(v),
177 | IEnumerable v => new ParameterValue(v),
178 | null => new ParameterValue((object?)null),
179 | _ => throw new ArgumentException($"No conversion for type '{value.GetType().FullName}'", nameof(value))
180 | };
181 | }
182 |
183 | [FieldOffset(0)]
184 | internal readonly byte tag;
185 | [FieldOffset(8)]
186 | internal readonly bool boolean;
187 | [FieldOffset(8)]
188 | internal readonly sbyte int8;
189 | [FieldOffset(8)]
190 | internal readonly Int16 int16;
191 | [FieldOffset(8)]
192 | internal readonly Int32 int32;
193 | [FieldOffset(8)]
194 | internal readonly Int64 int64;
195 | [FieldOffset(8)]
196 | internal readonly byte uint8;
197 | [FieldOffset(8)]
198 | internal readonly UInt16 uint16;
199 | [FieldOffset(8)]
200 | internal readonly UInt32 uint32;
201 | [FieldOffset(8)]
202 | internal readonly UInt64 uint64;
203 | [FieldOffset(8)]
204 | internal readonly float floating32;
205 | [FieldOffset(8)]
206 | internal readonly double floating64;
207 | [FieldOffset(8)]
208 | internal readonly InteropString str;
209 | [FieldOffset(8)]
210 | internal readonly Buffer binary;
211 | }
212 |
213 | ///
214 | /// A row retrieved from a Postgres database.
215 | ///
216 | [StructLayout(LayoutKind.Sequential)]
217 | public unsafe readonly struct PgRow : IReadOnlyList {
218 | internal readonly DbValue* _ptr;
219 | internal readonly int _len;
220 |
221 | ///
222 | /// The number of columns in the row.
223 | ///
224 | public int Count => _len;
225 |
226 | ///
227 | /// Gets the value of the column at the specified index.
228 | ///
229 | public DbValue this[int index]
230 | {
231 | get
232 | {
233 | if (index < 0 || index >= _len)
234 | {
235 | throw new ArgumentOutOfRangeException(nameof(index));
236 | }
237 | var ptr = _ptr + index;
238 | return *ptr;
239 | }
240 | }
241 |
242 | ///
243 | /// Gets an iterator over the values in the row.
244 | ///
245 | public IEnumerator GetEnumerator() => new Enumerator(_ptr, _len);
246 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
247 |
248 | // TODO: surely we can make this generic by now
249 | private struct Enumerator : IEnumerator
250 | {
251 | private DbValue* _ptr;
252 | private int _len;
253 | private int _index = -1;
254 |
255 | public Enumerator(DbValue* ptr, int len)
256 | {
257 | _ptr = ptr;
258 | _len = len;
259 | }
260 |
261 | public DbValue Current
262 | {
263 | get
264 | {
265 | if (_index < 0 || _index >= _len)
266 | {
267 | throw new InvalidOperationException();
268 | }
269 | var ptr = _ptr + _index;
270 | return *ptr;
271 | }
272 | }
273 |
274 | public bool MoveNext()
275 | {
276 | ++_index;
277 | return _index < _len;
278 | }
279 |
280 | public void Reset()
281 | {
282 | throw new NotSupportedException();
283 | }
284 |
285 | object System.Collections.IEnumerator.Current => Current;
286 | void IDisposable.Dispose() {}
287 | }
288 | }
289 |
290 | [StructLayout(LayoutKind.Sequential)]
291 | internal unsafe readonly struct PgError {
292 | internal const byte OUTBOUND_PG_PG_ERROR_SUCCESS = 0;
293 | internal const byte OUTBOUND_PG_PG_ERROR_CONNECTION_FAILED = 1;
294 | internal const byte OUTBOUND_PG_PG_ERROR_BAD_PARAMETER = 2;
295 | internal const byte OUTBOUND_PG_PG_ERROR_QUERY_FAILED = 3;
296 | internal const byte OUTBOUND_PG_PG_ERROR_VALUE_CONVERSION_FAILED = 4;
297 | internal const byte OUTBOUND_PG_PG_ERROR_OTHER_ERROR = 5;
298 |
299 | // NOTE: this relies on all variants with data having the same layout!
300 | internal readonly byte tag;
301 | internal readonly InteropString message;
302 | }
303 |
304 | ///
305 | /// A list of rows retrieved from a Postgres database.
306 | ///
307 | [StructLayout(LayoutKind.Sequential)]
308 | public unsafe readonly struct PgRows : IReadOnlyList {
309 | internal readonly PgRow* _ptr;
310 | internal readonly int _len;
311 |
312 | ///
313 | /// Gets the number of rows.
314 | ///
315 | public int Count => _len;
316 |
317 | ///
318 | /// Gets the row at the specified index.
319 | ///
320 | public PgRow this[int index]
321 | {
322 | get
323 | {
324 | if (index < 0 || index >= _len)
325 | {
326 | throw new ArgumentOutOfRangeException(nameof(index));
327 | }
328 | var ptr = _ptr + index;
329 | return *ptr;
330 | }
331 | }
332 |
333 | ///
334 | /// Gets an iterator over the rows.
335 | ///
336 | public IEnumerator GetEnumerator() => new Enumerator(_ptr, _len);
337 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
338 |
339 | private struct Enumerator : IEnumerator
340 | {
341 | private PgRow* _ptr;
342 | private int _len;
343 | private int _index = -1;
344 |
345 | public Enumerator(PgRow* ptr, int len)
346 | {
347 | _ptr = ptr;
348 | _len = len;
349 | }
350 |
351 | public PgRow Current
352 | {
353 | get
354 | {
355 | if (_index < 0 || _index >= _len)
356 | {
357 | throw new InvalidOperationException();
358 | }
359 | var ptr = _ptr + _index;
360 | return *ptr;
361 | }
362 | }
363 |
364 | public bool MoveNext()
365 | {
366 | ++_index;
367 | return _index < _len;
368 | }
369 |
370 | public void Reset()
371 | {
372 | throw new NotSupportedException();
373 | }
374 |
375 | object System.Collections.IEnumerator.Current => Current;
376 | void IDisposable.Dispose() {}
377 | }
378 | }
379 |
380 | ///
381 | /// The data type of a Postgres column.
382 | ///
383 | public enum PgDataType : byte
384 | {
385 | ///
386 | /// Boolean data type.
387 | ///
388 | OUTBOUND_PG_DB_DATA_TYPE_BOOLEAN = 0,
389 | ///
390 | /// 8-bit signed integer (sbyte) data type.
391 | ///
392 | OUTBOUND_PG_DB_DATA_TYPE_INT8 = 1,
393 | ///
394 | /// 16-bit signed integer (short) data type.
395 | ///
396 | OUTBOUND_PG_DB_DATA_TYPE_INT16 = 2,
397 | ///
398 | /// 32-bit signed integer (int) data type.
399 | ///
400 | OUTBOUND_PG_DB_DATA_TYPE_INT32 = 3,
401 | ///
402 | /// 64-bit signed integer (long) data type.
403 | ///
404 | OUTBOUND_PG_DB_DATA_TYPE_INT64 = 4,
405 | ///
406 | /// 8-bit unsigned integer (byte) data type.
407 | ///
408 | OUTBOUND_PG_DB_DATA_TYPE_UINT8 = 5,
409 | ///
410 | /// 16-bit unsigned integer (ushort) data type.
411 | ///
412 | OUTBOUND_PG_DB_DATA_TYPE_UINT16 = 6,
413 | ///
414 | /// 32-bit unsigned integer (uint) data type.
415 | ///
416 | OUTBOUND_PG_DB_DATA_TYPE_UINT32 = 7,
417 | ///
418 | /// 64-bit unsigned integer (ulong) data type.
419 | ///
420 | OUTBOUND_PG_DB_DATA_TYPE_UINT64 = 8,
421 | ///
422 | /// 32-bit floating point (float) data type.
423 | ///
424 | OUTBOUND_PG_DB_DATA_TYPE_FLOATING32 = 9,
425 | ///
426 | /// 64-bit floating point (double) data type.
427 | ///
428 | OUTBOUND_PG_DB_DATA_TYPE_FLOATING64 = 10,
429 | ///
430 | /// String data type.
431 | ///
432 | OUTBOUND_PG_DB_DATA_TYPE_STR = 11,
433 | ///
434 | /// Binary blob (Buffer) data type.
435 | ///
436 | OUTBOUND_PG_DB_DATA_TYPE_BINARY = 12,
437 | ///
438 | /// Any data type not supported by Spin.
439 | ///
440 | OUTBOUND_PG_DB_DATA_TYPE_OTHER = 13,
441 | }
442 |
443 | ///
444 | /// Column metadata from a Postgres database.
445 | ///
446 | [StructLayout(LayoutKind.Sequential)]
447 | public unsafe readonly struct PgColumn
448 | {
449 | internal readonly InteropString name;
450 | internal readonly PgDataType data_type;
451 |
452 | ///
453 | /// Gets the name of the column.
454 | ///
455 | public string Name => name.ToString();
456 | ///
457 | /// Gets the data type of the column.
458 | ///
459 | public PgDataType DataType => data_type;
460 | }
461 |
462 | ///
463 | /// The result of a query to a Postgres database.
464 | ///
465 | [StructLayout(LayoutKind.Sequential)]
466 | public unsafe readonly struct PgRowSet {
467 | internal readonly InteropList _columns;
468 | internal readonly PgRows _rows;
469 |
470 | ///
471 | /// Gets the columns retrieved by the query.
472 | ///
473 | public IEnumerable Columns => _columns;
474 | ///
475 | /// Gets the rows retrieved by the query.
476 | ///
477 | public PgRows Rows => _rows;
478 | }
479 |
480 | [StructLayout(LayoutKind.Explicit)]
481 | internal unsafe readonly struct PgU64OrError {
482 | [FieldOffset(0)]
483 | internal readonly byte is_err;
484 | [FieldOffset(8)]
485 | internal readonly UInt64 value;
486 | [FieldOffset(8)]
487 | internal readonly PgError err;
488 | }
489 |
490 | [StructLayout(LayoutKind.Explicit)]
491 | internal unsafe readonly struct PgRowSetOrError {
492 | [FieldOffset(0)]
493 | internal readonly byte is_err;
494 | [FieldOffset(4)]
495 | internal readonly PgRowSet value;
496 | [FieldOffset(4)]
497 | internal readonly PgError err;
498 | }
499 |
500 | internal static class OutboundPgInterop
501 | {
502 | [MethodImpl(MethodImplOptions.InternalCall)]
503 | internal static extern unsafe void outbound_pg_query(ref InteropString address, ref InteropString statement, ref InteropList parameters, ref PgRowSetOrError ret0);
504 |
505 | [MethodImpl(MethodImplOptions.InternalCall)]
506 | internal static extern unsafe void outbound_pg_execute(ref InteropString address, ref InteropString statement, ref InteropList parameters, ref PgU64OrError ret0);
507 | }
508 |
--------------------------------------------------------------------------------