package extism
Install
Dune Dependency
Authors
Maintainers
Sources
md5=25befaef404bc12c3092cea2b6d55c21
sha512=4809b21de4dc0d5174fecc8605cb868da37bb4d17563dfa6be1531c019c966482596f0c914908ca5a2f82706ee622112121f8da2a1809ce9b7505dc7e784db07
README.md.html
Extism OCaml Host SDK
This repo contains the OCaml package for integrating with the Extism runtime.
Documentation
Documentation is available at https://extism.github.io/ocaml-sdk
Installation
Install the Extism Runtime Dependency
For this library, you first need to install the Extism Runtime. You can download the shared object directly from a release or use the Extism CLI to install it.
Add the library to dune
Then add extism
to your dune depdendencies:
(libraries extism)
If you're generating an opam file using dune then add extism
to your dune-project
package depends
section:
(package
(depends
(extism (>= 1.1.0))))
Installing the extism
package on opam will also install the extism-call
executable, which can be used to execute Extism plugins.
Getting Started
This guide should walk you through some of the concepts in Extism and the OCaml bindings.
Creating A Plug-in
The primary concept in Extism is the plug-in. You can think of a plug-in as a code module stored in a .wasm
file.
Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web:
open Extism
let wasm = Manifest.Wasm.url "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"
let manifest = Manifest.create [wasm]
let plugin = Plugin.of_manifest_exn manifest
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 Extism.Plugin.call:
# Plugin.call_string_exn plugin ~name:"count_vowels" "Hello, world!";;
- : string = "{\"count\":3,\"total\":3,\"vowels\":\"aeiouAEIOU\"}"
All exports have a simple interface of bytes-in and bytes-out. This plug-in happens to take a string and return a JSON encoded string with a report of results.
This library also allows for calls to be typed, when the input and output types are not strings. Instead of getting the output as a JSON encoded string, we can convert it directly to Yojson.Safe.t
:
# Plugin.call_exn Type.string Type.json plugin ~name:"count_vowels" "Hello, world!";;
- : Yojson.Safe.t =
`Assoc
[("count", `Int 3); ("total", `Int 6); ("vowels", `String "aeiouAEIOU")]
See Extism.Type.S to define your own input/output types.
Typed Plugins
Plug-ins can also use pre-defined functions using Plugin.Typed
:
module Example = struct
include Plugin.Typed.Init ()
let count_vowels = exn @@ fn "count_vowels" Type.string Type.json
end
This can then be initialized using an existing Plugin.t
:
let example = Example.of_plugin_exn plugin in
let res = Example.count_vowels example "this is a test" in
print_endline (Yojson.Safe.to_string res)
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:
# Plugin.call_string_exn plugin ~name:"count_vowels" "Hello, world!" |> print_endline;;
{"count":3,"total":9,"vowels":"aeiouAEIOU"}
- : unit = ()
# Plugin.call_string_exn plugin ~name:"count_vowels" "Hello, world!" |> print_endline;;
{"count":3,"total":12,"vowels":"aeiouAEIOU"}
- : unit = ()
These variables will persist until this plug-in is freed or you initialize a new one.
Configuration
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:
# let manifest = Manifest.create [wasm];;
val manifest : Extism_manifest.t =
{Extism.Manifest.wasm =
[Extism.Manifest.Wasm.Url
{Extism.Manifest.Wasm.url =
"https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm";
headers = None; meth = None; name = None; hash = None}];
memory = None; config = None; allowed_hosts = None; allowed_paths = None;
timeout_ms = None}
# let plugin = Plugin.of_manifest_exn manifest;;
val plugin : Plugin.t = <abstr>
# Plugin.call_string_exn plugin ~name:"count_vowels" "Yellow, world!" |> print_endline;;
{"count":3,"total":3,"vowels":"aeiouAEIOU"}
- : unit = ()
# let plugin = Plugin.of_manifest_exn @@ Manifest.with_config ["vowels", Some "aeiouAEIOUY"] manifest;;
val plugin : Plugin.t = <abstr>
# Plugin.call_string_exn plugin ~name:"count_vowels" "Yellow, world!" |> print_endline;;
{"count":4,"total":4,"vowels":"aeiouAEIOUY"}
- : unit = ()
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 come in.
Host functions allow us to grant new capabilities to our plug-ins from our application. They are simply some OCaml 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:
open Extism
let url =
"https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"
let wasm = Manifest.Wasm.url url
let manifest = Manifest.create [ wasm ]
Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our its import interface for a KV store.
Using Extism.Function we can define a host function that can be called from the guest plug-in. In this example we will create a function to help us load plugins and setup the host functions.
We want to expose two functions to our plugin (in OCaml types): val kv_write: string -> string -> unit
which writes a bytes value to a key and val kv_read: string -> string
which reads the bytes at the given key
.
let make_kv_plugin () =
(* pretend this is Redis or something :) *)
let kv_store = Hashtbl.create 8 in
let kv_read =
let open Val_type in
Function.create "kv_read" ~params:[ ptr ] ~results:[ ptr ] ~user_data:()
@@ fun plugin () ->
let key = Host_function.input_string plugin in
Printf.printf "Reading from key=%s\n" key;
let value =
try Hashtbl.find kv_store key
with Not_found -> String.init 4 (fun _ -> char_of_int 0)
in
Host_function.output_string plugin value
in
let kv_write =
let open Val_type in
Function.create "kv_write" ~params:[ ptr; ptr ] ~results:[] ~user_data:()
@@ fun plugin () ->
let key = Host_function.input_string ~index:0 plugin in
let value = Host_function.input_string ~index:1 plugin in
Printf.printf "Write value=%s to key=%s\n" value key;
Hashtbl.replace kv_store key value
in
(* Create a plugin from the manifest with the kv host functions *)
Plugin.of_manifest_exn ~functions:[ kv_read; kv_write ] ~wasi:true manifest
Now we can invoke the event:
# let plugin = make_kv_plugin ();;
val plugin : Plugin.t = <abstr>
# Extism.Plugin.call_string_exn plugin ~name:"count_vowels" "Hello, world" |> print_endline;;
Reading from key=count-vowels
Write value=^C^@^@^@ to key=count-vowels
{"count":3,"total":3,"vowels":"aeiouAEIOU"}
- : unit = ()
# Extism.Plugin.call_string_exn plugin ~name:"count_vowels" "Hello, world" |> print_endline;;
Reading from key=count-vowels
Write value=^F^@^@^@ to key=count-vowels
{"count":3,"total":6,"vowels":"aeiouAEIOU"}
- : unit = ()