Skip to content

Commit

Permalink
Add Extism PDK (#258)
Browse files Browse the repository at this point in the history
---------

Signed-off-by: Glenn Lewis <[email protected]>
  • Loading branch information
gmlewis committed Jun 20, 2024
1 parent 5f44a40 commit 09ac5e2
Show file tree
Hide file tree
Showing 30 changed files with 941 additions and 2 deletions.
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
// 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

0 comments on commit 09ac5e2

Please sign in to comment.