├── samples ├── hello-world │ ├── assets │ │ └── asset-text.txt │ ├── HelloWorld.Spin.csproj │ ├── spin.toml │ └── Handler.cs └── Fermyon.PetStore │ ├── Fermyon.PetStore.Toy │ ├── assets │ │ ├── mystery-toy.png │ │ ├── ToyNotFound.html │ │ └── ToyTemplate.html │ ├── Project.csproj │ └── Handler.cs │ ├── Fermyon.PetStore.NewPet │ ├── spin.toml │ ├── assets │ │ ├── NewPetMissingField.html │ │ ├── NewPetCreated.html │ │ └── NewPet.html │ ├── Project.csproj │ └── Handler.cs │ ├── Fermyon.PetStore.Common │ ├── Configuration.cs │ ├── HttpRequestExtensions.cs │ ├── GenericResponse.cs │ ├── Fermyon.PetStore.Common.csproj │ ├── PostgresExtensions.cs │ └── FormParser.cs │ ├── Fermyon.PetStore.Pet │ ├── assets │ │ ├── PetNotFound.html │ │ └── PetTemplate.html │ ├── Project.csproj │ └── Handler.cs │ ├── Fermyon.PetStore.NewToy │ ├── assets │ │ ├── NewToyMissingField.html │ │ ├── NewToyNoPets.html │ │ ├── NewToyCreated.html │ │ └── NewToy.html │ ├── Project.csproj │ └── Handler.cs │ ├── Fermyon.PetStore.Pets │ ├── assets │ │ └── PetsTemplate.html │ ├── Project.csproj │ └── Handler.cs │ ├── Fermyon.PetStore.Toys │ ├── assets │ │ └── ToysTemplate.html │ ├── Project.csproj │ └── Handler.cs │ ├── sql │ └── schema.sql │ ├── Fermyon.PetStore.Home │ ├── assets │ │ └── Home.html │ ├── Handler.cs │ └── Project.csproj │ ├── README.md │ └── spin.toml ├── .gitignore ├── src ├── build │ ├── Fermyon.Spin.Sdk.props │ └── Fermyon.Spin.Sdk.targets ├── native │ ├── host-components.h │ ├── outbound-redis.h │ ├── util.h │ ├── host-components.c │ ├── spin-config.h │ ├── spin-http.h │ ├── wasi-outbound-http.h │ ├── spin-config.c │ ├── util.c │ ├── spin-http.c │ ├── outbound-redis.c │ ├── wasi-outbound-http.c │ ├── outbound-pg.h │ ├── http-trigger.c │ └── outbound-pg.c ├── RedisInterop.cs ├── HttpTrigger.cs ├── HttpOutbound.cs ├── Fermyon.Spin.Sdk.csproj ├── Config.cs ├── ConfigInterop.cs ├── RedisOutbound.cs ├── PostgresOutbound.cs ├── HttpInterop.cs ├── Interop.cs └── PostgresInterop.cs ├── CODE_OF_CONDUCT.md ├── wit └── ephemeral │ ├── spin-http.wit │ ├── redis-types.wit │ ├── wasi-outbound-http.wit │ ├── pg-types.wit │ ├── spin-config.wit │ ├── outbound-pg.wit │ ├── outbound-redis.wit │ ├── rdbms-types.wit │ └── http-types.wit ├── MAINTAINERS.md ├── templates └── http-csharp │ ├── metadata │ └── spin-template.toml │ └── content │ ├── spin.toml │ ├── Handler.cs │ └── Project.csproj ├── Makefile ├── Fermyon.Spin.Sdk.sln ├── readme.md └── LICENSE /samples/hello-world/assets/asset-text.txt: -------------------------------------------------------------------------------- 1 | Hello from an asset file! 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | *.wat 4 | *.wasm 5 | .suo 6 | .vs/ 7 | !templates/**/filters/*.wasm 8 | **/.spin 9 | -------------------------------------------------------------------------------- /src/build/Fermyon.Spin.Sdk.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project subscribes to the Fermyon [Code of Conduct](https://www.fermyon.com/code-of-conduct). 4 | -------------------------------------------------------------------------------- /src/native/host-components.h: -------------------------------------------------------------------------------- 1 | #ifndef __HOST_COMPONENTS_H 2 | #define __HOST_COMPONENTS_H 3 | 4 | void spin_attach_internal_calls(); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /wit/ephemeral/spin-http.wit: -------------------------------------------------------------------------------- 1 | use * from http-types 2 | 3 | // The entrypoint for an HTTP handler. 4 | handle-http-request: func(req: request) -> response 5 | -------------------------------------------------------------------------------- /wit/ephemeral/redis-types.wit: -------------------------------------------------------------------------------- 1 | // General purpose error. 2 | enum error { 3 | success, 4 | error, 5 | } 6 | 7 | // The message payload. 8 | type payload = list 9 | -------------------------------------------------------------------------------- /samples/Fermyon.PetStore/Fermyon.PetStore.Toy/assets/mystery-toy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spinframework/spin-dotnet-sdk/main/samples/Fermyon.PetStore/Fermyon.PetStore.Toy/assets/mystery-toy.png -------------------------------------------------------------------------------- /wit/ephemeral/wasi-outbound-http.wit: -------------------------------------------------------------------------------- 1 | use * from http-types 2 | 3 | // Send an HTTP request and return a response or a potential error. 4 | request: func(req: request) -> expected 5 | -------------------------------------------------------------------------------- /samples/Fermyon.PetStore/Fermyon.PetStore.NewPet/spin.toml: -------------------------------------------------------------------------------- 1 | spin_version = "1" 2 | authors = ["itowlson "] 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 |
11 |

14 |

17 |

18 |
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 |
11 |

14 |

17 |

22 |

25 |

26 |
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 = $"\n\n{tbody}\n
Name
"; 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\n{tbody}\n
ToyCountOwner
"; 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 | --------------------------------------------------------------------------------