package extism-manifest

  1. Overview
  2. Docs
Extism manifest bindings

Install

Dune Dependency

Authors

Maintainers

Sources

v1.2.0.tar.gz
md5=25befaef404bc12c3092cea2b6d55c21
sha512=4809b21de4dc0d5174fecc8605cb868da37bb4d17563dfa6be1531c019c966482596f0c914908ca5a2f82706ee622112121f8da2a1809ce9b7505dc7e784db07

Description

Bindings to the Extism manifest format

Tags

topics wasm plugin

Published: 13 Mar 2024

README

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 = ()

Dependencies (5)

  1. base64 >= "3.5.0"
  2. ppx_inline_test >= "v0.15.0"
  3. ppx_yojson_conv >= "v0.15.0"
  4. dune >= "3.2"
  5. ocaml >= "4.14.1"

Dev Dependencies (1)

  1. odoc with-doc

Used by (1)

  1. extism < "0.2.0" | >= "1.2.0"

Conflicts

None