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 000000000..85431a2fc Binary files /dev/null and b/apps/Extism/count-vowels/CountVowels.wasm differ 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 000000000..de38db7f2 Binary files /dev/null and b/apps/Extism/greet/Greet.wasm differ 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 000000000..b9a3e9a0b Binary files /dev/null and b/apps/Extism/http-get/HttpGet.wasm differ 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;