Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Extism PDK #258

Merged
merged 13 commits into from
Jun 20, 2024
Merged
2 changes: 2 additions & 0 deletions apps/Extism/DEPS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
../../lib/pdk/*.v3
../../lib/util/*.v3
28 changes: 28 additions & 0 deletions apps/Extism/README.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions apps/Extism/TARGETS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wasm
156 changes: 156 additions & 0 deletions apps/Extism/assets/simulatedExtismSdk.js
Original file line number Diff line number Diff line change
@@ -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 },
}
24 changes: 24 additions & 0 deletions apps/Extism/build.sh
Original file line number Diff line number Diff line change
@@ -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)
70 changes: 70 additions & 0 deletions apps/Extism/count-vowels/CountVowels.v3
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// `defaultVowels` represents the default set of vowels
gmlewis marked this conversation as resolved.
Show resolved Hide resolved
// 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<byte>.!(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) {}
}
Binary file added apps/Extism/count-vowels/CountVowels.wasm
Binary file not shown.
12 changes: 12 additions & 0 deletions apps/Extism/count-vowels/README.md
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions apps/Extism/count-vowels/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>

<head></head>

<body>
<script type="module">
import { configs, flush, importObject, inputString } from '/assets/simulatedExtismSdk.js'

const wasmUnderTest = '/count-vowels/CountVowels.wasm'

// WebAssembly.instantiateStreaming(fetch("/target/wasm-gc/release/build/examples/count-vowels/count-vowels.wasm"), importObject).then(
WebAssembly.instantiateStreaming(fetch(wasmUnderTest), importObject).then(
(obj) => {
console.log('Using simulated Extism SDK...')
obj.instance.exports._initialize()
// configs.vowels = 'aeiouyAEIOUY'
inputString.value = 'Once upon a dream'
obj.instance.exports['count_vowels']()
inputString.value = 'eight more vowels yo'
obj.instance.exports['count_vowels']()
flush()
}
)

// Next, use the official Extism JavaScript SDK:
// Read the JS SDK docs at: https://extism.github.io/js-sdk/
const extism = await import('https://esm.sh/@extism/extism')

const plugin = await extism.createPlugin(
fetch(wasmUnderTest),
{ useWasi: true }
)

console.log('Using official Extism JavaScript SDK...')
let out = await plugin.call('count_vowels', 'from official Extism JavaScript SDK')
console.log(out.text())
out = await plugin.call('count_vowels', 'eight more vowels yo ho')
console.log(out.text());
</script>
</body>

</html>
23 changes: 23 additions & 0 deletions apps/Extism/greet/Greet.v3
Original file line number Diff line number Diff line change
@@ -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) {}
}
Binary file added apps/Extism/greet/Greet.wasm
Binary file not shown.
12 changes: 12 additions & 0 deletions apps/Extism/greet/README.md
Original file line number Diff line number Diff line change
@@ -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
Loading