diff --git a/README.md b/README.md index 6d19c65..34f3193 100644 --- a/README.md +++ b/README.md @@ -12,19 +12,173 @@ JavaScript runtimes: Instead of using FFI and the libextism shared object, this library uses whatever Wasm runtime is already available with the JavaScript runtime. +## Installation -## Install - +Install via npm: ``` npm install @extism/extism@1.0.0-rc1 --save ``` > **Note**: Keep in mind we will possibly have breaking changes b/w rc versions until we hit 1.0. -## API +## Getting Started + +This guide should walk you through some of the concepts in Extism and this JS library. + +First you should import `createPlugin` and `ExtismPluginOptions` from Extism: +```js +// CommonJS +const createPlugin = require("../dist/node/index") + +// ES Modules +import createPlugin from '../src/deno/mod.ts' +``` + +## Creating A Plug-in + +The primary concept in Extism is the [plug-in](https://extism.org/docs/concepts/plug-in). You can think of a plug-in as a code module stored in a `.wasm` file. + +Plug-in code can come from a file on disk, object storage or any number of places. Since you may not have one handy let's load a demo plug-in from the web: + +```js +const wasm = { + url: 'https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm' +} + +const plugin = await createPlugin(wasm, { + // NOTE: If you get an error like "TypeError: WebAssembly.instantiate(): Import #0 module="wasi_snapshot_preview1": module is not an object or function", then your plugin requires WASI support + useWasi: true, +}); +``` + +## Calling A Plug-in's Exports + +This plug-in was written in Rust and it does one thing, it counts vowels in a string. As such, it exposes one "export" function: `count_vowels`. We can call exports using `ExtismPlugin.call`: + +```js +let out = await plugin.call("count_vowels", new TextEncoder().encode(input)); +console.log(new TextDecoder().decode(out.buffer)) + +// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} +``` + +All exports have a simple interface of optional bytes in, and optional bytes out. This plug-in happens to take a string and return a JSON encoded string with a report of results. + +### Plug-in State + +Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export: + +```js +let out = await plugin.call("count_vowels", new TextEncoder().encode("Hello, World!")); +console.log(new TextDecoder().decode(out.buffer)) + +// => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"} + +out = await plugin.call("count_vowels", new TextEncoder().encode("Hello, World!")); +console.log(new TextDecoder().decode(out.buffer)) +// => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"} +``` + +These variables will persist until this plug-in is freed or you initialize a new one. + +### Configuration -We'll be publishing more docs very soon. For the time being look at [these tests](src/tests/mod.test.ts) -for up to date examples. +Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example: + +```js +const wasm = { + url: 'https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm' +} + +let plugin = await createPlugin(wasm, { + useWasi: true, +}); + +let out = await plugin.call("count_vowels", new TextEncoder().encode("Yellow, World!")); +console.log(new TextDecoder().decode(out.buffer)) +// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} + +plugin = await createPlugin(wasm, { + useWasi: true, + config: { "vowels": "aeiouyAEIOUY" } +}); + +out = await plugin.call("count_vowels", new TextEncoder().encode("Yellow, World!")); +console.log(new TextDecoder().decode(out.buffer)) +// => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"} +``` + +### Host Functions + +Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var, let's store it in a persistent key-value store! + +Wasm can't use our KV store on it's own. This is where [Host Functions](https://extism.org/docs/concepts/host-functions) come in. + +[Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. They are simply some JS functions you write which can be passed down and invoked from any language inside the plug-in. + +Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in: + +```js +const wasm = { + url: "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm" +} +``` + +> *Note*: The source code for this is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs) and is written in rust, but it could be written in any of our PDK languages. + +Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our its import interface for a KV store. + +We want to expose two functions to our plugin, `kv_write(key: string, value: Uint8Array)` which writes a bytes value to a key and `kv_read(key: string): Uint8Array` which reads the bytes at the given `key`. +```js +// pretend this is Redis or something :) +let kvStore = new Map(); + +const options = { + useWasi: true, + functions: { + "env": { + // NOTE: the first argument is always a CurrentPlugin + "kv_read": function (cp: CurrentPlugin, offs: bigint) { + const key = cp.readString(offs); + let value = kvStore.get(key) ?? new Uint8Array([0, 0, 0, 0]); + console.log(`Read ${new DataView(value.buffer).getUint32(0, true)} from key=${key}`); + return cp.writeBytes(value); + }, + "kv_write": function (cp: CurrentPlugin, kOffs: bigint, vOffs: bigint) { // this: CurrentPlugin + const key = cp.readString(kOffs); + const value = cp.readBytes(vOffs); + console.log(`Writing value=${new DataView(value.buffer).getUint32(0, true)} from key=${key}`); + + kvStore.set(key, value); + } + } + } +}; +``` + +> *Note*: In order to write host functions you should get familiar with the methods on the `CurrentPlugin` type. `this` is bound to an instance of `CurrentPlugin`. + +We need to pass these imports to the plug-in to create them. All imports of a plug-in must be satisfied for it to be initialized: + +```js +const plugin = await createPlugin(wasm, options); +``` + +Now we can invoke the event: + +```js +let out = await plugin.call("count_vowels", new TextEncoder().encode("Hello World!")); +console.log(new TextDecoder().decode(out.buffer)) +// => Read from key=count-vowels" +// => Writing value=3 from key=count-vowels" +// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} + +out = await plugin.call("count_vowels", new TextEncoder().encode("Hello World!")); +console.log(new TextDecoder().decode(out.buffer)) +// => Read from key=count-vowels" +// => Writing value=6 from key=count-vowels" +// => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} +``` ## Run Examples: @@ -33,15 +187,15 @@ npm run build node --experimental-wasi-unstable-preview1 ./examples/node.js wasm/config.wasm -deno run -A ./examples/deno.js ./wasm/config.wasm +deno run -A ./examples/deno.ts ./wasm/config.wasm bun run ./examples/node.js wasm/config.wasm ``` ## Update `extism-kernel.wasm`: -We are shipping an embedded kernel in base64 form in plugin.ts. To update it, you can run these commands: +We are shipping an embedded kernel in base64 form in plugin.ts. To update it, you can run: ``` make update-kernel -``` +``` \ No newline at end of file diff --git a/build.js b/build.js index fa8c394..82d9817 100644 --- a/build.js +++ b/build.js @@ -1,6 +1,5 @@ const { build } = require("esbuild"); const { peerDependencies } = require('./package.json') -const fs = require('fs') const sharedConfig = { bundle: true, diff --git a/examples/deno.js b/examples/deno.js deleted file mode 100644 index 6ab7ae9..0000000 --- a/examples/deno.js +++ /dev/null @@ -1,22 +0,0 @@ -import { ExtismPlugin, ExtismPluginOptions } from '../src/deno/mod.ts' - -async function main() { - const filename = Deno.args[0] || "wasm/hello.wasm"; - const funcname = Deno.args[1] || "run_test"; - const input = Deno.args[2] || "this is a test"; - const wasm = { - path: filename - } - - const options = new ExtismPluginOptions() - .withConfig("thing", "testing") - .withWasi(); - - const plugin = await ExtismPlugin.new(wasm, options); - - const res = await plugin.call(funcname, new TextEncoder().encode(input)); - const s = new TextDecoder().decode(res.buffer); - console.log(s) -} - -main(); \ No newline at end of file diff --git a/examples/deno.ts b/examples/deno.ts new file mode 100644 index 0000000..5f061e7 --- /dev/null +++ b/examples/deno.ts @@ -0,0 +1,19 @@ +import createPlugin from '../src/deno/mod.ts' + +const filename = Deno.args[0] || "wasm/hello.wasm"; +const funcname = Deno.args[1] || "run_test"; +const input = Deno.args[2] || "this is a test"; +const wasm = { + url: filename +} + +const plugin = await createPlugin(wasm, { + useWasi: true, + config: { + "thing": "testing" + } +}); + +const res = await plugin.call(funcname, new TextEncoder().encode(input)); +const s = new TextDecoder().decode(res.buffer); +console.log(s) \ No newline at end of file diff --git a/examples/index.html b/examples/index.html index 77f686a..dc27dff 100644 --- a/examples/index.html +++ b/examples/index.html @@ -58,6 +58,8 @@