From 09ac5e280ba760b967544c5bd265ebadf224eefa Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:34:08 -0700 Subject: [PATCH] Add Extism PDK (#258) --------- Signed-off-by: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> --- apps/Extism/DEPS | 2 + apps/Extism/README.md | 28 ++++ apps/Extism/TARGETS | 1 + apps/Extism/assets/simulatedExtismSdk.js | 156 ++++++++++++++++++++++ apps/Extism/build.sh | 24 ++++ apps/Extism/count-vowels/CountVowels.v3 | 70 ++++++++++ apps/Extism/count-vowels/CountVowels.wasm | Bin 0 -> 7246 bytes apps/Extism/count-vowels/README.md | 12 ++ apps/Extism/count-vowels/index.html | 43 ++++++ apps/Extism/greet/Greet.v3 | 23 ++++ apps/Extism/greet/Greet.wasm | Bin 0 -> 1661 bytes apps/Extism/greet/README.md | 12 ++ apps/Extism/greet/index.html | 36 +++++ apps/Extism/http-get/HttpGet.v3 | 28 ++++ apps/Extism/http-get/HttpGet.wasm | Bin 0 -> 7771 bytes apps/Extism/http-get/README.md | 8 ++ apps/Extism/run.sh | 4 + apps/Extism/scripts/count-vowels.sh | 2 + apps/Extism/scripts/greet.sh | 2 + apps/Extism/scripts/http-get.sh | 6 + apps/Extism/scripts/python-server.sh | 2 + lib/pdk/Config.v3 | 36 +++++ lib/pdk/Extism.v3 | 103 ++++++++++++++ lib/pdk/Header.v3 | 45 +++++++ lib/pdk/Host.v3 | 99 ++++++++++++++ lib/pdk/Http.v3 | 51 +++++++ lib/pdk/Memory.v3 | 33 +++++ lib/pdk/Method.v3 | 22 +++ lib/pdk/Var.v3 | 74 ++++++++++ lib/util/Arrays.v3 | 21 ++- 30 files changed, 941 insertions(+), 2 deletions(-) create mode 100644 apps/Extism/DEPS create mode 100644 apps/Extism/README.md create mode 100644 apps/Extism/TARGETS create mode 100644 apps/Extism/assets/simulatedExtismSdk.js create mode 100755 apps/Extism/build.sh create mode 100644 apps/Extism/count-vowels/CountVowels.v3 create mode 100644 apps/Extism/count-vowels/CountVowels.wasm create mode 100644 apps/Extism/count-vowels/README.md create mode 100644 apps/Extism/count-vowels/index.html create mode 100644 apps/Extism/greet/Greet.v3 create mode 100644 apps/Extism/greet/Greet.wasm create mode 100644 apps/Extism/greet/README.md create mode 100644 apps/Extism/greet/index.html create mode 100644 apps/Extism/http-get/HttpGet.v3 create mode 100644 apps/Extism/http-get/HttpGet.wasm create mode 100644 apps/Extism/http-get/README.md create mode 100755 apps/Extism/run.sh create mode 100755 apps/Extism/scripts/count-vowels.sh create mode 100755 apps/Extism/scripts/greet.sh create mode 100755 apps/Extism/scripts/http-get.sh create mode 100755 apps/Extism/scripts/python-server.sh create mode 100644 lib/pdk/Config.v3 create mode 100644 lib/pdk/Extism.v3 create mode 100644 lib/pdk/Header.v3 create mode 100644 lib/pdk/Host.v3 create mode 100644 lib/pdk/Http.v3 create mode 100644 lib/pdk/Memory.v3 create mode 100644 lib/pdk/Method.v3 create mode 100644 lib/pdk/Var.v3 diff --git a/apps/Extism/DEPS b/apps/Extism/DEPS new file mode 100644 index 000000000..f6ad7b8c4 --- /dev/null +++ b/apps/Extism/DEPS @@ -0,0 +1,2 @@ +../../lib/pdk/*.v3 +../../lib/util/*.v3 diff --git a/apps/Extism/README.md b/apps/Extism/README.md new file mode 100644 index 000000000..f9cfa451f --- /dev/null +++ b/apps/Extism/README.md @@ -0,0 +1,28 @@ +# apps/Extism + +Here are a few [Extism] plugins written using the Virgil programming language. + +[Extism]: https://extism.org/ + +* [greet](greet) +* [count-vowels](count-vowels) +* [http-get](http-get) + +## Build + +To build the examples, change your directory to this one (`apps/Extism`) +and type: + +```bash +$ ./build.sh +``` + +## Run + +To run all the examples with the [Extism CLI], type: + +```bash +$ ./run.sh +``` + +[Extism CLI]: https://github.com/extism/cli diff --git a/apps/Extism/TARGETS b/apps/Extism/TARGETS new file mode 100644 index 000000000..f65e5817d --- /dev/null +++ b/apps/Extism/TARGETS @@ -0,0 +1 @@ +wasm diff --git a/apps/Extism/assets/simulatedExtismSdk.js b/apps/Extism/assets/simulatedExtismSdk.js new file mode 100644 index 000000000..9e0b15627 --- /dev/null +++ b/apps/Extism/assets/simulatedExtismSdk.js @@ -0,0 +1,156 @@ +// This is a simulated Extism SDK written in JavaScript in order to assist +// in the debugging of the MoonBit Extism PDK. + +// Adapted from: https://dmitripavlutin.com/timeout-fetch-request/ +export const fetchWithTimeout = async (resource, options = {}) => { + const { timeout = 8000 } = options // 8000 ms = 8 seconds + + const controller = new AbortController() + const id = setTimeout(() => controller.abort(), timeout) + const response = await fetch(resource, { + ...options, + signal: controller.signal, + }) + clearTimeout(id) + return response +} + +// `log` and `flust` are useful for debugging the wasm-gc or wasm targets with `println()`: +export const [log, flush] = (() => { + var buffer = [] + function flush() { + if (buffer.length > 0) { + console.log(new TextDecoder("utf-16").decode(new Uint16Array(buffer).valueOf())) + buffer = [] + } + } + function log(ch) { + if (ch == '\n'.charCodeAt(0)) { flush() } + else if (ch == '\r'.charCodeAt(0)) { /* noop */ } + else { buffer.push(ch) } + } + return [log, flush] +})() + +const memory = new WebAssembly.Memory({ initial: 1, maximum: 1, shared: false }) +const fakeAlloc = { offset: 0, buffers: {} } +const alloc = (lengthBigInt) => { + const offset = fakeAlloc.offset + const length = Number(lengthBigInt) + fakeAlloc.buffers[offset] = { + offset, + length, + buffer: new Uint8Array(memory.buffer, offset, length), + } + fakeAlloc.offset += length + return BigInt(offset) +} +const allocAndCopy = (str) => { + const offsetBigInt = alloc(BigInt(str.length)) + const offset = Number(offsetBigInt) + const b = fakeAlloc.buffers[offset] + for (let i = 0; i < str.length; i++) { b.buffer[i] = str.charCodeAt(i) } + return offsetBigInt +} +const decodeOffset = (offset) => new TextDecoder().decode(fakeAlloc.buffers[offset].buffer) +const lastHttpResponse = { statusCode: 0 } +const http_request = async (reqOffsetBigInt, bodyOffsetBigInt) => { + const req = JSON.parse(decodeOffset(reqOffsetBigInt)) + const body = bodyOffsetBigInt ? decodeOffset(bodyOffsetBigInt) : '' + console.log(`http_request: req=${JSON.stringify(req)}`) + console.log(`http_request: body=${body}`) + const fetchParams = { + method: req.method, + headers: req.header, + } + if (body) { fetchParams.body = body } + const response = await fetchWithTimeout(req.url, fetchParams) + const result = await response.text() + console.log(`result=${result}`) + lastHttpResponse.statusCode = response.status + return allocAndCopy(result) +} +const http_status_code = () => lastHttpResponse.statusCode + +export const configs = {} // no configs to start with +export const vars = {} // no vars to start with + +export const inputString = { value: '' } // allows for exporting + +export const importObject = { + "extism:host/env": { + alloc, + config_get: (offsetBigInt) => { + const offset = Number(offsetBigInt) + const key = decodeOffset(offset) + // console.log(`config_get(${offset}) = configs[${key}] = ${configs[key]}`) + if (!configs[key]) { return BigInt(0) } + return allocAndCopy(configs[key]) + }, + free: () => { }, // noop for now. + http_request, + http_status_code, + input_length: () => BigInt(inputString.value.length), + input_load_u8: (offsetBigInt) => { + const offset = Number(offsetBigInt) + if (offset < inputString.value.length) { return inputString.value.charCodeAt(offset) } + console.error(`input_load_u8: wasm requested offset(${offset}) > inputString.value.length(${inputString.value.length})`) + return 0 + }, + length: (offsetBigInt) => { + const offset = Number(offsetBigInt) + const b = fakeAlloc.buffers[offset] + if (!b) { return BigInt(0) } + // console.log(`length(${offset}) = ${b.length}`) + return BigInt(b.length) + }, + load_u8: (offsetBigInt) => { + const offset = Number(offsetBigInt) + const bs = Object.keys(fakeAlloc.buffers).filter((key) => { + const b = fakeAlloc.buffers[key] + return (offset >= b.offset && offset < b.offset + b.length) + }) + if (bs.length !== 1) { + console.error(`load_u8: offset ${offset} not found`) + return 0 + } + const key = bs[0] + const b = fakeAlloc.buffers[key] + const byte = b.buffer[offset - key] + // console.log(`load_u8(${offset}) = ${byte}`) + return byte + }, + log_info: (offset) => console.info(`log_info: ${decodeOffset(offset)}`), + log_debug: (offset) => console.log(`log_debug: ${decodeOffset(offset)}`), + log_error: (offset) => console.error(`log_error: ${decodeOffset(offset)}`), + log_warn: (offset) => console.warn(`log_warn: ${decodeOffset(offset)}`), + output_set: (offset) => console.log(decodeOffset(offset)), + store_u8: (offsetBigInt, byte) => { + const offset = Number(offsetBigInt) + Object.keys(fakeAlloc.buffers).forEach((key) => { + const b = fakeAlloc.buffers[key] + if (offset >= b.offset && offset < b.offset + b.length) { + b.buffer[offset - key] = byte + // console.log(`store_u8(${offset})=${byte}`) + // if (offset == b.offset + b.length - 1) { + // console.log(`store_u8 completed offset=${key}..${offset}, length=${b.length}: '${decodeOffset(key)}'`) + // } + } + }) + }, + var_get: (offsetBigInt) => { + const offset = Number(offsetBigInt) + const key = decodeOffset(offset) + // console.log(`var_get(${offset}) = vars[${key}] = ${vars[key]}`) + if (!vars[key]) { return BigInt(0) } + return vars[key] // BigInt + }, + var_set: (offsetBigInt, bufOffsetBigInt) => { + const offset = Number(offsetBigInt) + const key = decodeOffset(offset) + // console.log(`var_set(${offset}, ${bufOffsetBigInt}) = vars[${key}]`) + vars[key] = bufOffsetBigInt + }, + }, + spectest: { print_char: log }, +} diff --git a/apps/Extism/build.sh b/apps/Extism/build.sh new file mode 100755 index 000000000..5206cdc28 --- /dev/null +++ b/apps/Extism/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash -ex +v3c \ + -entry-export=_initialize \ + -heap-size=500m \ + -main-export=_initialize \ + -output=greet \ + -target=wasm \ + greet/Greet.v3 $(cat DEPS) + +v3c \ + -entry-export=_initialize \ + -heap-size=500m \ + -main-export=_initialize \ + -output=count-vowels \ + -target=wasm \ + count-vowels/CountVowels.v3 $(cat DEPS) + +v3c \ + -entry-export=_initialize \ + -heap-size=500m \ + -main-export=_initialize \ + -output=http-get \ + -target=wasm \ + http-get/HttpGet.v3 $(cat DEPS) diff --git a/apps/Extism/count-vowels/CountVowels.v3 b/apps/Extism/count-vowels/CountVowels.v3 new file mode 100644 index 000000000..68f8caf2c --- /dev/null +++ b/apps/Extism/count-vowels/CountVowels.v3 @@ -0,0 +1,70 @@ +// `defaultVowels` represents the default set of vowels +// if the host provides no "config.vowels" string. +def defaultVowels = "aeiouAEIOU"; + +// `VowelReport` represents the JSON struct returned to the host. +class VowelReport { + var count: int; + var total: int; + var vowels: string; + + new(count, total, vowels) {} + + def toJson() -> string { + var b = StringBuilder.new(); + b.put1("{\"count\":%d,", count); + b.put1("\"total\":%d,", total); + b.put1("\"vowels\":\"%s\"}", vowels); + return b.toString(); + } +} + +def getTotal() -> int { + match (Var.getInt("total")) { + Some(total) => return total; + None => return 0; + } +} + +def storeTotal(total: int) { + Var.setInt("total", total); +} + +def getVowels() -> string { + match (Config.get("vowels")) { + Some(s) => return s; + None => return defaultVowels; + } +} + +// Exported: `count_vowels` reads the input string from the host, reads the "vowels" +// config from the host, then counts the number of vowels in the input +// string and keeps a running total (over multiple iterations) +// in the host's "total" var. +// It sends the JSON `VowelReport` to the host via its output data channel. +// It returns 0 to the host on success. +def count_vowels() -> int { + def input = Host.inputString(); + // + def vowels = getVowels(); + def vowelsArr = Array.!(vowels); + def count = Arrays.filter(input, Arrays.contains(vowelsArr, _)).length; + // + def total = getTotal() + count; + storeTotal(total); + // + def jsonStr = VowelReport.new(count, total, vowels).toJson(); + Host.outputString(jsonStr); + return 0; +} + +// Unused. +def main() { +} + +export count_vowels; + +// Hack to compile to wasm: provide Virgil identifiers that cannot be found: +component System { + def error(s1: string, s2: string) {} +} diff --git a/apps/Extism/count-vowels/CountVowels.wasm b/apps/Extism/count-vowels/CountVowels.wasm new file mode 100644 index 0000000000000000000000000000000000000000..85431a2fc6e712de7bf88787390caed095a96694 GIT binary patch literal 7246 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 literal 0 HcmV?d00001 diff --git a/apps/Extism/count-vowels/README.md b/apps/Extism/count-vowels/README.md new file mode 100644 index 000000000..e7bc39da9 --- /dev/null +++ b/apps/Extism/count-vowels/README.md @@ -0,0 +1,12 @@ +# CountVowels + +The `CountVowels.wasm` plugin can be run from the top-level of the repo by +typing: + +```bash +$ ./build.sh +$ ./scripts/python-server.sh +``` + +Then open your browser window to: +http://localhost:8080/examples/count-vowels diff --git a/apps/Extism/count-vowels/index.html b/apps/Extism/count-vowels/index.html new file mode 100644 index 000000000..6d9d2eac2 --- /dev/null +++ b/apps/Extism/count-vowels/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/apps/Extism/greet/Greet.v3 b/apps/Extism/greet/Greet.v3 new file mode 100644 index 000000000..6014dde2c --- /dev/null +++ b/apps/Extism/greet/Greet.v3 @@ -0,0 +1,23 @@ +// Exported: `greet` reads the input string from the host and writes a +// greeting to the host's output string. +// It returns 0 to the host on success. +def greet() -> int { + def input = Host.inputString(); + var b = StringBuilder.new(); + b.puts("Hello, "); + b.puts(input); + b.puts("!"); + Host.outputString(b.toString()); + return 0; // success +} + +// Unused. +def main() { +} + +export greet; + +// Hack to compile to wasm: provide Virgil identifiers that cannot be found: +component System { + def error(s1: string, s2: string) {} +} diff --git a/apps/Extism/greet/Greet.wasm b/apps/Extism/greet/Greet.wasm new file mode 100644 index 0000000000000000000000000000000000000000..de38db7f2c291a44a67cd72bb5d076f5552e198f GIT binary patch literal 1661 zcmZWpOK%)S5U%RS%y@h4ZLb4_1W3)E5-BK`$cb{W+>t;L5+^R)YztP&PHb=DT_cFB zosE+4Iz?Q#?yK1h#G0{HvQ_dP0Q=ch$^v;;m(kSaOhWh!pDtrGzh~^0B>d*1hL1`Z2ZMU~ zl#AKk@aySla&+gxcrv{)JpQUXI(~FI-8&o}-=97JSaq-0?(xBwd#ASnSN)yw=~Ut; z!zrj$r#(149Df0Nbw8U-$0tL{XY?zf;Id89G_z~+WF70e>l;1a*|aw6_st7Afse4? zx8Ft+-|W2ENq-SryS>rzXgWGL96cFAk{k_>#wXt>u=h`fj3{0EIYvN0x7k_0@YdV! zTzvP^*5&u!|KQ5nM(6h*r1nk3GW2M~b$1Ys^ge(e$gVf+a%+a2HWE`WO;llZRtCuT`Y!jEDJ4!Nq#K>77N z3y^EQOPbU@vrCLI8(7Ln7kNrZI%GxhAwX`bNXv1Qej2l+?8_6floVv{0PtZfa@|GC zm^hIkRc!N{+i>6_x4iE++;>iXGYIQf|=lO#Sp!u=z`<_NQ?V3jsqJUWq2i%i&eO!l&a?> zQ~KCc=Xm5i`K^J}Gbk0TDv+$VYew*!@&IqqDAl0iSA-OB$(hv`xJ7Sgm zpB7coq9QG=WNhqzNhGm-(v?2A_c)6cZE$fX%{up@nFptRI7pkSX*&#ERn?TrFfcbP zR92N#)ogH3vW%ZhRT#)LvbChSF-oE%qOqb3BT6-*R2`M^w@f6ea5>+?>)h1M)->@< zg{TO@kY1eejGH^+d854nYLVM;k=2XAh2j literal 0 HcmV?d00001 diff --git a/apps/Extism/greet/README.md b/apps/Extism/greet/README.md new file mode 100644 index 000000000..5c177f3f6 --- /dev/null +++ b/apps/Extism/greet/README.md @@ -0,0 +1,12 @@ +# Greet + +The `Greet.wasm` plugin can be run from the apps/Extism dir of the repo +(the parent of this dir) by typing: + +```bash +$ ./build.sh +$ ./scripts/python-server.sh +``` + +Then open your browser window to: +http://localhost:8080/greet diff --git a/apps/Extism/greet/index.html b/apps/Extism/greet/index.html new file mode 100644 index 000000000..87215e7bb --- /dev/null +++ b/apps/Extism/greet/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/apps/Extism/http-get/HttpGet.v3 b/apps/Extism/http-get/HttpGet.v3 new file mode 100644 index 000000000..5fceccea5 --- /dev/null +++ b/apps/Extism/http-get/HttpGet.v3 @@ -0,0 +1,28 @@ +// Exported: `http_get` makes a GET HTTP request through the Extism host, gets +// the response back, then sends it (unmodified) to the Extism host output. +// It returns 0 to the host on success. +def http_get() -> int { + Host.logErrorStr("ENTER http_get"); + // create an HTTP Request (without relying on WASI), set headers as needed + var req = Http.newRequest(Method.GET, "https://jsonplaceholder.typicode.com/todos/1"); + req.header.add("some-name", "some-value"); + req.header.add("another", "again"); + // send the request, get response back + def res = req.send(null); // no body + + // zero-copy send output to host + res.output(); + Host.logErrorStr("LEAVE http_get"); + return 0; +} + +// Unused. +def main() { +} + +export http_get; + +// Hack to compile to wasm: provide Virgil identifiers that cannot be found: +component System { + def error(s1: string, s2: string) {} +} diff --git a/apps/Extism/http-get/HttpGet.wasm b/apps/Extism/http-get/HttpGet.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b9a3e9a0bd1b84cce81211650a580460601a8009 GIT binary patch literal 7771 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 literal 0 HcmV?d00001 diff --git a/apps/Extism/http-get/README.md b/apps/Extism/http-get/README.md new file mode 100644 index 000000000..6ae6002af --- /dev/null +++ b/apps/Extism/http-get/README.md @@ -0,0 +1,8 @@ +# HttpGet + +The `HttpGet.wasm` plugin can be run from the top-level of the repo by typing: + +```bash +$ ./build.sh +$ ./scripts/http-get.sh +``` diff --git a/apps/Extism/run.sh b/apps/Extism/run.sh new file mode 100755 index 000000000..cf4b4afe1 --- /dev/null +++ b/apps/Extism/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +./scripts/greet.sh Benjamin && echo && echo +./scripts/count-vowels.sh 'Once upon a dream' && echo && echo +./scripts/http-get.sh && echo && echo diff --git a/apps/Extism/scripts/count-vowels.sh b/apps/Extism/scripts/count-vowels.sh new file mode 100755 index 000000000..0cad16d01 --- /dev/null +++ b/apps/Extism/scripts/count-vowels.sh @@ -0,0 +1,2 @@ +#!/bin/bash -ex +extism call count-vowels/CountVowels.wasm count_vowels --wasi --input "$@" diff --git a/apps/Extism/scripts/greet.sh b/apps/Extism/scripts/greet.sh new file mode 100755 index 000000000..6506d48d9 --- /dev/null +++ b/apps/Extism/scripts/greet.sh @@ -0,0 +1,2 @@ +#!/bin/bash -ex +extism call greet/Greet.wasm greet --wasi --input "$@" diff --git a/apps/Extism/scripts/http-get.sh b/apps/Extism/scripts/http-get.sh new file mode 100755 index 000000000..b3cac17d6 --- /dev/null +++ b/apps/Extism/scripts/http-get.sh @@ -0,0 +1,6 @@ +#!/bin/bash -ex +extism call \ + http-get/HttpGet.wasm \ + http_get \ + --wasi \ + --allow-host='*.typicode.com' diff --git a/apps/Extism/scripts/python-server.sh b/apps/Extism/scripts/python-server.sh new file mode 100755 index 000000000..97469b984 --- /dev/null +++ b/apps/Extism/scripts/python-server.sh @@ -0,0 +1,2 @@ +#!/bin/bash -ex +python3 -m http.server 8080 diff --git a/lib/pdk/Config.v3 b/lib/pdk/Config.v3 new file mode 100644 index 000000000..5d16fa997 --- /dev/null +++ b/lib/pdk/Config.v3 @@ -0,0 +1,36 @@ +// The {Maybe} algebraic data type can express 0 or 1 value of type {T} +type Maybe { + case None; + case Some(t: T); +} + +// `Config` represents functions used to get Extism host SDK "config" vars. +component Config { + // `getMemory` returns a "config" Memory block from the host that is keyed by `key`. + // Note that no processing is performed on this block of memory. + def getMemory(key: string) -> Maybe { + def keyMem = Host.allocateString(key); + def offset = Extism.config_get(keyMem.offset); + keyMem.free(); + if (offset == 0L) { + return Maybe.None; + } + def length = Extism.length(offset); + if (length == 0L) { + return Maybe.None; + } + return Maybe.Some(Memory.new(offset, length)); + } + + // `get` returns a "config" string from the host that is keyed by `key`. + def get(key: string) -> Maybe { + match (getMemory(key)) { + Some(mem) => { + def s = mem.toString(); + mem.free(); + return Maybe.Some(s); + } + None => return Maybe.None; + } + } +} diff --git a/lib/pdk/Extism.v3 b/lib/pdk/Extism.v3 new file mode 100644 index 000000000..0a8add38d --- /dev/null +++ b/lib/pdk/Extism.v3 @@ -0,0 +1,103 @@ +// `Extism` represents the low-level calls used to communicate with the Extism host SDK. +import "extism:host/env" component Extism { + // `input_length` returns the number of bytes provided by the host via its input methods. + // The user of this PDK will typically not call this method directly. + def input_length() -> i64; + + // `input_load_u8` returns the byte at location `offset` of the "input" data from the host. + // The user of this PDK will typically not call this method directly. + def input_load_u8(offset : i64) -> byte; + + // `input_load_u64` returns the 64-bit unsigned integer of the "input" data from the host. + // Note that MoonBit has no unsigned integers, + // so the result is returned as an i64. + // Also note that `offset` must lie on an 8-byte boundary. + // The user of this PDK will typically not call this method directly. + def input_load_u64(offset : i64) -> i64; + + // `length` returns the number of bytes associated with the block of host memory + // located at `offset`. + // The user of this PDK will typically not call this method directly. + def length(offset : i64) -> i64; + + // `alloc` allocates `length` bytes of data with host memory for use by the plugin + // and returns its `offset` within the host memory block. + // The user of this PDK will typically not call this method directly. + def alloc(length : i64) -> i64; + + // `free` releases the bytes previously allocated with `alloc` at the given `offset`. + // The user of this PDK will typically not call this method directly. + def free(offset : i64); + + // `output_set` sets the "output" data from the plugin to the host to be the memory that + // has been written at `offset` with the given `length`. + // The user of this PDK will typically not call this method directly. + def output_set(offset : i64, length : i64); + + // `error_set` sets the "error" data from the plugin to the host to be the memory that + // has been written at `offset`. + // The user of this PDK will typically not call this method directly. + def error_set(offset : i64); + + // `config_get` returns the host memory block offset for the "config" data associated with + // the key which is represented by the UTF-8 string which as been previously + // written at `offset`. + // The user of this PDK will typically not call this method directly. + def config_get(offset : i64) -> i64; + + // `var_get` returns the host memory block offset for the "var" data associated with + // the key which is represented by the UTF-8 string which as been previously + // written at `offset`. + // The user of this PDK will typically not call this method directly. + def var_get(offset : i64) -> i64; + + // `var_set` sets the host "var" memory keyed by the UTF-8 string located at `offset` + // to be the value which has been previously written at `value_offset`. + // The user of this PDK will typically not call this method directly. + def var_set(offset : i64, value_offset : i64); + + // `store_u8` stores the byte `b` at location `offset` in the host memory block. + // The user of this PDK will typically not call this method directly. + def store_u8(offset : i64, b : byte); + + // `load_u8` returns the byte located at `offset` in the host memory block. + // The user of this PDK will typically not call this method directly. + def load_u8(offset : i64) -> byte; + + // `store_u64` stores the i64 value `v` at location `offset` in the host memory block. + // Note that MoonBit does not have unsigned integers, but the host interprets + // the provided `v` value as an unsigned 64-bit integer. + // Also note that `offset` must lie on an 8-byte boundary. + // The user of this PDK will typically not call this method directly. + def store_u64(offset : i64, v : i64); + + // `load_u64` returns the 64-bit unsigned integer at location `offset` in the host memory block. + // Note that MoonBit has no unsigned integers, + // so the result is returned as an i64. + // Also note that `offset` must lie on an 8-byte boundary. + // The user of this PDK will typically not call this method directly. + def load_u64(offset : i64) -> i64; + + // `http_request` sends the HTTP request to the Extism host and returns back the + // memory offset to the response body. + def http_request(req : i64, body : i64) -> i64; + + // `http_status_code` returns the status code for the last-sent `http_request` call. + def http_status_code() -> int; + + // `log_warn` logs a "warning" string to the host from the previously-written UTF-8 string written to `offset`. + // The user of this PDK will typically not call this method directly. + def log_warn(offset : i64); + + // `log_info` logs an "info" string to the host from the previously-written UTF-8 string written to `offset`. + // The user of this PDK will typically not call this method directly. + def log_info(offset : i64); + + // `log_debug` logs a "debug" string to the host from the previously-written UTF-8 string written to `offset`. + // The user of this PDK will typically not call this method directly. + def log_debug(offset : i64); + + // `log_error` logs an "error" string to the host from the previously-written UTF-8 string written to `offset`. + // The user of this PDK will typically not call this method directly. + def log_error(offset : i64); +} \ No newline at end of file diff --git a/lib/pdk/Header.v3 b/lib/pdk/Header.v3 new file mode 100644 index 000000000..bee0b3ff3 --- /dev/null +++ b/lib/pdk/Header.v3 @@ -0,0 +1,45 @@ +// `Header` represents an HTTP Request header. +// Multiple values for a single key are not deduped. +class Header { + def map = Strings.newMap(); + var mapLen = 0; + + // hack to work around no inline function closures: + var b: StringBuilder; + var index = 0; + private def keyValueToJson(key: string, value: string) { + if (index < mapLen - 1) { + b.put2("\"%s\":\"%s\",", key, value); + } else { + b.put2("\"%s\":\"%s\"", key, value); + } + index++; + } + + def toJson() -> string { + b = StringBuilder.new(); + b.puts("{"); + index = 0; + map.apply(keyValueToJson); + b.puts("}"); + return b.toString(); + } + + // `add` adds a value to a named (by `key`) header field. + // If the header key already exists, the value is appended after a comma. + def add(key: string, value: string) { + match (map.has(key)) { + true => { + var b = StringBuilder.new(); + b.puts(map[key]); + b.puts(","); + b.puts(value); + map[key] = b.toString(); + } + false => { + map[key] = value; + mapLen++; + } + } + } +} \ No newline at end of file diff --git a/lib/pdk/Host.v3 b/lib/pdk/Host.v3 new file mode 100644 index 000000000..b81672931 --- /dev/null +++ b/lib/pdk/Host.v3 @@ -0,0 +1,99 @@ +// `Host` represents functions used to interact with the Extism host SDK. +component Host { + // `input` returns a sequence of bytes from the host. + def input() -> Array { + def length = Extism.input_length(); + def value = Array.new(int.view(length)); + for (j < length) { + value[j] = Extism.input_load_u8(j); + } + return value; + } + + // `inputString` returns a string from the host. + def inputString() -> string { + return string.!(input()); + } + + // `outputBytesToMemory` writes the bytes to a Memory buffer on the host. + private def outputBytesToMemory(b: Array) -> Memory { + def offset = Extism.alloc(b.length); + for (i < b.length) { + Extism.store_u8(offset + long.view(i), b[i]); + } + return Memory.new(offset, b.length); + } + + // `output` sends an array of bytes to the host as the plugin's "output". + def output(b: Array) { + def mem = outputBytesToMemory(b); + Extism.output_set(mem.offset, mem.length); + } + + // `outputString` and sends a string to the host. + def outputString(s: string) { + output(Array.!(s)); + } + + // `outputJsonValue` sends a JSON blob to the host. + def outputJsonValue(j: void) { + // TODO + } + + // `logWarnStr` is a helper function to log a warning string to the host. + def logWarnStr(s: string) { + def mem = outputBytesToMemory(Array.!(s)); + Extism.log_warn(mem.offset); + Extism.free(mem.offset); + } + + // `logInfoStr` is a helper function to log an info string to the host. + def logInfoStr(s: string) { + def mem = outputBytesToMemory(Array.!(s)); + Extism.log_info(mem.offset); + Extism.free(mem.offset); + } + + // `logDebugStr` is a helper function to log a debug string to the host. + def logDebugStr(s: string) { + def mem = outputBytesToMemory(Array.!(s)); + Extism.log_debug(mem.offset); + Extism.free(mem.offset); + } + + // `logErrorStr` is a helper function to log an error string to the host. + def logErrorStr(s: string) { + def mem = outputBytesToMemory(Array.!(s)); + Extism.log_error(mem.offset); + Extism.free(mem.offset); + } + + // `allocate` allocates an uninitialized (determined by host) + // area of shared memory on the host. + def allocate(length: i64) -> Memory { + return Memory.new(Extism.alloc(length), length); + } + + // `allocateBytes` allocates and initializes host memory + // with the provided (unprocessed) bytes. + def allocateBytes(bytes: Array) -> Memory { + def offset = Extism.alloc(bytes.length); + for (i < bytes.length) { + Extism.store_u8(offset + i, bytes[i]); + } + return Memory.new(offset, bytes.length); + } + + // `allocateString` allocates and initializes a UTF-8 string + // in host memory that is converted from this UTF-16 MoonBit String. + def allocateString(s: string) -> Memory { + return allocateBytes(Array.!(s)); + } + + // `allocateJsonValue` allocates and initializes a UTF-8 string + // in host memory that is converted from this `@json.JsonValue`. + def allocateJsonValue(j: void) -> Memory { + // TODO + return Memory.new(0, 0); + } +} \ No newline at end of file diff --git a/lib/pdk/Http.v3 b/lib/pdk/Http.v3 new file mode 100644 index 000000000..e5dc1ea82 --- /dev/null +++ b/lib/pdk/Http.v3 @@ -0,0 +1,51 @@ +// `Request` represents an HTTP request made by the Extism host. +class Request(method: Method, header: Header, url: string) { + def toJson() -> string { + var b = StringBuilder.new(); + b.put1("{\"method\":\"%s\",", method.name); + b.put1("\"header\":%s,", header.toJson()); + b.put1("\"url\":\"%s\"}", url); + return b.toString(); + } + + // `send` sends the `Request` to the host, waits for a response, + // and returns it to the caller. + // Note that the (possibly null) `body` is freed by this call. + def send(body: Memory) -> Response { + def metaMem = Host.allocateString(this.toJson()); + var bodyMemoryOffset: i64 = 0; + if (body != null) { bodyMemoryOffset = body.offset; } + // + def responseOffset = Extism.http_request(metaMem.offset, bodyMemoryOffset); + def responseLength = Extism.length(responseOffset); + def statusCode = Extism.http_status_code(); + // + metaMem.free(); + if (body != null) { body.free(); } + // + def responseBody = Memory.new(responseOffset, responseLength); + return Response.new(statusCode, responseBody); + } +} + +// `Response` represents an HTTP response from the Extism host. +class Response { + var statusCode: int; + var body: Memory; + + new(statusCode, body) {} + + // `output` sends the (unprocessed) `Response` body to the Extism host "output". + def output() { + body.output(); + } +} + +component Http { + // `newRequest` returns a new `Request` using the provided + // `method` and `url`. + def newRequest(method: Method, url: string) -> Request { + def header = Header.new(); + return Request.new(method, header, url); + } +} diff --git a/lib/pdk/Memory.v3 b/lib/pdk/Memory.v3 new file mode 100644 index 000000000..300a7a3c3 --- /dev/null +++ b/lib/pdk/Memory.v3 @@ -0,0 +1,33 @@ +// `Memory` represents memory allocated by (and shared with) the host. +class Memory(offset: i64, length: i64) { + // `free` releases this Memory from the host. + def free() { + Extism.free(offset); + } + + // `output` sets the host's "output" to be the contents of this Memory data. + def output() { + Extism.output_set(offset, length); + } + + // `toString` reads and returns the UTF-8 string residing in the host memory. + def toString() -> string { + return string.!(toBytes()); + } + + // `toInt` reads and converts the u32 residing in the host memory to an int. + def toInt() -> int { + def bytes = toBytes(); + return bytes[0] + (bytes[1] << 8) + (bytes[2] << 16) + (bytes[3] << 24); + } + + // `toBytes` reads the (unprocessed) bytes residing in the host memory + // to an array of bytes. + def toBytes() -> Array { + def bytes = Array.new(int.view(length)); + for (i < length) { + bytes[i] = Extism.load_u8(offset + i); + } + return bytes; + } +} diff --git a/lib/pdk/Method.v3 b/lib/pdk/Method.v3 new file mode 100644 index 000000000..1089fcbfd --- /dev/null +++ b/lib/pdk/Method.v3 @@ -0,0 +1,22 @@ +// `Method` represents an HTTP method. +// Descriptions are from: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods +enum Method { + // The GET method requests a representation of the specified resource. Requests using GET should only retrieve data. + GET + // The HEAD method asks for a response identical to a GET request, but without the response body. + HEAD + // The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server. + POST + // The PUT method replaces all current representations of the target resource with the request payload. + PUT + // The DELETE method deletes the specified resource. + DELETE + // The CONNECT method establishes a tunnel to the server identified by the target resource. + CONNECT + // The OPTIONS method describes the communication options for the target resource. + OPTIONS + // The TRACE method performs a message loop-back test along the path to the target resource. + TRACE + // The PATCH method applies partial modifications to a resource. + PATCH +} diff --git a/lib/pdk/Var.v3 b/lib/pdk/Var.v3 new file mode 100644 index 000000000..03b739eba --- /dev/null +++ b/lib/pdk/Var.v3 @@ -0,0 +1,74 @@ +// `Var` represents functions used to read and write the Extism host SDK "vars". +component Var { + // `getMemory` returns the (unprocessed) host Memory block for the "var" data associated with + // the provided `key`. + def getMemory(key: string) -> Maybe { + def keyMem = Host.allocateString(key); + def offset = Extism.var_get(keyMem.offset); + keyMem.free(); + if (offset == 0L) { + return Maybe.None; + } + def length = Extism.length(offset); + if (length == 0L) { + return Maybe.None; + } + return Maybe.Some(Memory.new(offset, length)); + } + + // `getBytes` returns the (unprocessed) host Memory block for the "var" data associated with + // the provided `key`. + def getBytes(key: string) -> Maybe> { + match (getMemory(key)) { + Some(v) => return Maybe>.Some(v.toBytes()); + None => return Maybe>.None; + } + } + + // `getInt` returns the host's "var" Int associated with the provided `key`. + def getInt(key: string) -> Maybe { + match (getMemory(key)) { + Some(v) => return Maybe.Some(v.toInt()); + None => return Maybe.None; + } + } + + // `getString` returns the host's "var" string associated with the provided `key`. + def getString(key: string) -> Maybe { + match (getMemory(key)) { + Some(v) => return Maybe.Some(v.toString()); + None => return Maybe.None; + } + } + + // `setBytes` sets the (unprocessed) host Memory block for the "var" data associated with + // the provided `key`. + def setBytes(key: string, value: Array) { + def keyMem = Host.allocateString(key); + def val_mem = Host.allocateBytes(value); + Extism.var_set(keyMem.offset, val_mem.offset); + keyMem.free(); + val_mem.free(); + } + + // `setInt` sets the host's "var" Int associated with the provided `key`. + def setInt(key: string, value: int) { + def keyMem = Host.allocateString(key); + def bytes = Array.new(4); + bytes[0] = byte.view(value & 255); + bytes[1] = byte.view((value >> 8) & 255); + bytes[2] = byte.view((value >> 16) & 255); + bytes[3] = byte.view((value >> 24) & 255); + def val_mem = Host.allocateBytes(bytes); + Extism.var_set(keyMem.offset, val_mem.offset); + keyMem.free(); + val_mem.free(); + } + + // `remove` deletes the value in the host's "var" memory associated with the provided `key`. + def remove(key: string) { + def keyMem = Host.allocateString(key); + Extism.var_set(keyMem.offset, 0L); + keyMem.free(); + } +} \ No newline at end of file diff --git a/lib/util/Arrays.v3 b/lib/util/Arrays.v3 index 4f9feea08..9517fa8d4 100644 --- a/lib/util/Arrays.v3 +++ b/lib/util/Arrays.v3 @@ -19,6 +19,23 @@ component Arrays { for (i < x.length) if (x[i] != y[i]) return false; return true; } + // Check if a value is contained in an array. + def contains(array: Array, element: T) -> bool { + if (array == null) return false; + for (i < array.length) if (array[i] == element) { return true; } + return false; + } + // Filter an array, keeping elements whose {func} value is true, + // returning a new array of the results. + def filter(array: Array, func: T -> bool) -> Array { + if (array == null) return null; + var max = array.length, t = Array.new(max); + var j = 0; + for (i < max) { if (func(array[i])) { t[j] = array[i]; j++; }} + var r = Array.new(j); + for (i < j) r[i] = t[i]; + return r; + } // Map {func} over an input {array}, returning a new array of the results. def map(array: Array, func: A -> B) -> Array { if (array == null) return null; @@ -151,10 +168,10 @@ component Arrays { var l = a[i], r = b[j]; if (cmp(l, r)) { n[k] = l; - if (++i == a.length) return finish(n, k + 1, b, j); + if (++i == a.length) return finish(n, k + 1, b, j); } else { n[k] = r; - if (++j == b.length) return finish(n, k + 1, a, i); + if (++j == b.length) return finish(n, k + 1, a, i); } } return n;