v0.0.1 alpha

Co-authored-by: Zach Shipko <zach@dylib.so>
This commit is contained in:
Steve Manuel
2022-08-25 00:48:35 -06:00
commit e27fae9193
72 changed files with 3878 additions and 0 deletions

105
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,105 @@
on: [ push, pull_request ]
name: CI
env:
RUNTIME_MANIFEST: runtime/Cargo.toml
jobs:
build_and_test:
name: Build & Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Cache Rust environment
uses: Swatinem/rust-cache@v1
- name: Format
run: cargo fmt --check --manifest-path ${{ env.RUNTIME_MANIFEST }}
- name: Build
run: cargo build --release --manifest-path ${{ env.RUNTIME_MANIFEST }}
- name: Lint
run: cargo clippy --release --no-deps --manifest-path ${{ env.RUNTIME_MANIFEST }}
- name: Test
run: cargo test --release --manifest-path ${{ env.RUNTIME_MANIFEST }}
- name: Install extism shared library
shell: bash
run: sudo make install
- name: Setup Go env
uses: actions/setup-go@v3
- name: Test Go Host SDK
run: |
go version
cd go && LD_LIBRARY_PATH=/usr/local/lib go run main.go
- name: Load cached Poetry installation
uses: actions/cache@v2
with:
path: ~/.local
key: poetry-0
- name: Setup Python env
uses: snok/install-poetry@v1
- name: Test Python Host SDK
run: |
cd python
poetry install
poetry run python example.py
- name: Setup Ruby env
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- name: Test Ruby Host SDK
run: |
cd ruby
bundle install
ruby example.rb
- name: Setup Node env
uses: actions/setup-node@v3
with:
node-version: 16
- name: Test Node Host SDK
run: |
cd node
npm i
LD_LIBRARY_PATH=/usr/local/lib node example.js
- name: Test Rust Host SDK
run: LD_LIBRARY_PATH=/usr/local/lib cargo test --release --manifest-path rust/Cargo.toml
# - name: Setup OCaml env
# uses: ocaml/setup-ocaml@v2
# - name: Test OCaml Host SDK
# run: |
# cd ocaml
# opam install -y .
# opam exec -- dune exec extism

217
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,217 @@
on:
release:
types: [created]
name: Release
env:
RUNTIME_MANIFEST: runtime/Cargo.toml
RUSTFLAGS: -C target-feature=-crt-static
ARTIFACT_DIR: release-artifacts
jobs:
release-linux:
name: linux
runs-on: ubuntu-latest
strategy:
matrix:
target: [
aarch64-unknown-linux-gnu,
aarch64-unknown-linux-musl,
i686-unknown-linux-gnu,
x86_64-unknown-linux-gnu ]
if: always()
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
target: ${{ matrix.target }}
- name: Build Target (${{ matrix.target }})
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target ${{ matrix.target }} --manifest-path ${{ env.RUNTIME_MANIFEST }}
- name: Prepare Artifact
run: |
EXT=so
SRC_DIR=runtime/target/${{ matrix.target }}/release
DEST_DIR=${{ env.ARTIFACT_DIR }}
RELEASE_NAME=libextism-${{ matrix.target }}-${{ github.ref_name }}
ARCHIVE=${RELEASE_NAME}.tar.gz
CHECKSUM=${RELEASE_NAME}.checksum.txt
# compress the shared library & create checksum
cp runtime/extism.h ${SRC_DIR}
cp LICENSE ${SRC_DIR}
tar -C ${SRC_DIR} -czvf ${ARCHIVE} libextism.${EXT} extism.h
ls -ll ${ARCHIVE}
shasum -a 256 ${ARCHIVE} > ${CHECKSUM}
# copy archive and checksum into release artifact directory
mkdir -p ${DEST_DIR}
cp ${ARCHIVE} ${DEST_DIR}
cp ${CHECKSUM} ${DEST_DIR}
ls ${DEST_DIR}
- name: Upload Artifact to Summary
uses: actions/upload-artifact@v3
with:
name: ${{ env.ARTIFACT_DIR }}
path: |
*.tar.gz
*.txt
- name: Upload Artifact to Release
uses: softprops/action-gh-release@v1
with:
files: |
*.tar.gz
*.txt
release-macos:
name: macos
runs-on: macos-latest
strategy:
matrix:
target: [
x86_64-apple-darwin,
aarch64-apple-darwin ]
if: always()
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
target: ${{ matrix.target }}
- name: Build Target (${{ matrix.target }})
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target ${{ matrix.target }} --manifest-path ${{ env.RUNTIME_MANIFEST }}
- name: Prepare Artifact
run: |
EXT=dylib
SRC_DIR=runtime/target/${{ matrix.target }}/release
DEST_DIR=${{ env.ARTIFACT_DIR }}
RELEASE_NAME=libextism-${{ matrix.target }}-${{ github.ref_name }}
ARCHIVE=${RELEASE_NAME}.tar.gz
CHECKSUM=${RELEASE_NAME}.checksum.txt
# compress the shared library & create checksum
cp runtime/extism.h ${SRC_DIR}
cp LICENSE ${SRC_DIR}
tar -C ${SRC_DIR} -czvf ${ARCHIVE} libextism.${EXT} extism.h
ls -ll ${ARCHIVE}
shasum -a 256 ${ARCHIVE} > ${CHECKSUM}
# copy archive and checksum into release artifact directory
mkdir -p ${DEST_DIR}
cp ${ARCHIVE} ${DEST_DIR}
cp ${CHECKSUM} ${DEST_DIR}
ls ${DEST_DIR}
- name: Upload Artifact to Summary
uses: actions/upload-artifact@v3
with:
name: ${{ env.ARTIFACT_DIR }}
path: |
*.tar.gz
*.txt
- name: Upload Artifact to Release
uses: softprops/action-gh-release@v1
with:
files: |
*.tar.gz
*.txt
release-windows:
name: windows
runs-on: windows-latest
strategy:
matrix:
target: [
i686-pc-windows-gnu,
i686-pc-windows-msvc,
x86_64-pc-windows-gnu,
x86_64-pc-windows-msvc,
aarch64-pc-windows-msvc
]
if: always()
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
target: ${{ matrix.target }}
- name: Build Target (${{ matrix.target }})
uses: actions-rs/cargo@v1
with:
use-cross: false
command: build
args: --release --target ${{ matrix.target }} --manifest-path ${{ env.RUNTIME_MANIFEST }}
- name: Prepare Artifact
shell: bash
run: |
EXT=dll
SRC_DIR=runtime/target/${{ matrix.target }}/release
DEST_DIR=${{ env.ARTIFACT_DIR }}
RELEASE_NAME=libextism-${{ matrix.target }}-${{ github.ref_name }}
ARCHIVE=${RELEASE_NAME}.tar.gz
CHECKSUM=${RELEASE_NAME}.checksum.txt
# compress the shared library & create checksum
cp runtime/extism.h ${SRC_DIR}
cp LICENSE ${SRC_DIR}
tar -C ${SRC_DIR} -czvf ${ARCHIVE} libextism.${EXT} extism.h
ls -ll ${ARCHIVE}
certutil -hashfile ${ARCHIVE} SHA256 >${CHECKSUM}
# copy archive and checksum into release artifact directory
mkdir -p ${DEST_DIR}
cp ${ARCHIVE} ${DEST_DIR}
cp ${CHECKSUM} ${DEST_DIR}
ls ${DEST_DIR}
- name: Upload Artifact to Summary
uses: actions/upload-artifact@v3
with:
name: ${{ env.ARTIFACT_DIR }}
path: |
*.tar.gz
*.txt
- name: Upload Artifact to Release
uses: softprops/action-gh-release@v1
with:
files: |
*.tar.gz
*.txt

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
runtime/target
Cargo.lock
.DS_Store
.vscode
**/libextism.dylib
**/libextism.so
*.o
manifest/target
**/node_modules
__pycache__
python/dist
python/poetry.lock
c/main
go/main
ruby/.bundle/
ruby/.yardoc
ruby/_yardoc/
ruby/coverage/
ruby/doc/
ruby/pkg/
ruby/spec/reports/
ruby/tmp/
ruby/Gemfile.lock
cpp/example
rust/target
ocaml/duniverse
ocaml/_build
wasm/rust-pdk/target

11
LICENSE Normal file
View File

@@ -0,0 +1,11 @@
Copyright 2022 Dylibso, Inc.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

40
Makefile Normal file
View File

@@ -0,0 +1,40 @@
DEST?=/usr/local
SOEXT=so
FEATURES?=default
DEFAULT_FEATURES?=yes
UNAME := $(shell uname -s)
ifeq ($(UNAME),Darwin)
SOEXT=dylib
endif
ifeq ($(DEFAULT_FEATURES),no)
ifeq ($(FEATURES),default)
FEATURE_FLAGS=--no-default-features
else
FEATURE_FLAGS=--features $(FEATURES) --no-default-features
endif
else
FEATURE_FLAGS=--features $(FEATURES)
endif
.PHONY: build
lint:
cargo clippy --release --no-deps --manifest-path runtime/Cargo.toml
build:
cargo build --release $(FEATURE_FLAGS) --manifest-path runtime/Cargo.toml
install:
install runtime/extism.h $(DEST)/include
install runtime/target/release/libextism.$(SOEXT) $(DEST)/lib
ls $(DEST)/include | grep extism
ls $(DEST)/lib | grep extism
uninstall:
rm -f $(DEST)/include/extism.h $(DEST)/lib/libextism.$(SOEXT)

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
# [Extism](https://extism.org)
The universal plug-in system. Run WebAssembly extensions inside your app. Use idiomatic Host SDKs for [Go](https://extism.org/docs/integrate-into-your-codebase/go-host-sdk),
[Ruby](https://extism.org/docs/integrate-into-your-codebase/ruby-host-sdk), [Python](https://extism.org/docs/integrate-into-your-codebase/python-host-sdk),
[Node](https://extism.org/docs/integrate-into-your-codebase/node-host-sdk), [Rust](https://extism.org/docs/integrate-into-your-codebase/rust-host-sdk),
[C](https://extism.org/docs/integrate-into-your-codebase/c-host-sdk), [C++](https://extism.org/docs/integrate-into-your-codebase/cpp-host-sdk),
[OCaml](https://extism.org/docs/integrate-into-your-codebase/ocaml-host-sdk) &amp; more (others coming soon).
Plug-in development kits (PDK) for plug-in authors supported in Rust, AssemblyScript, Go, C/C++.
<p align="center">
<img src="https://user-images.githubusercontent.com/7517515/184472910-36d42d73-bd1e-49e2-9b4d-9b020959603d.png"/>
</p>
Add a flexible, secure, and _bLaZiNg FaSt_ plug-in system to your project. Server, desktop, mobile, web, database -- you name it. Enable users to write and execute safe extensions to your software in **3 easy steps:**
### 1. Import
Import an Extism Host SDK into your code as a library dependency.
### 2. Integrate
Identify the place(s) in your code where some arbitrary logic should run (the plug-in!), returning your code some results.
### 3. Execute
Load WebAssembly modules at any time in your app's lifetime and Extism will execute them in a secure sandbox, fully isolated from your program's memory.
---
## Usage
Head to the [project website](https://extism.org) for more information and docs. Also, consider reading an [overview](/docs/overview) of Extism and its goals & approach.
## Contribution
Thank you for considering a contribution to Extism, we are happy to help you make a PR or find something to work on!
The easiest way to start would be to join the [Discord](https://discord.gg/cx3usBCWnc) or open an issue on the [`extism/proposals`](https://github.com/extism/proposals) issue tracker, which can eventually become an Extism Improvement Proposal (EIP).
---
## Who's behind this?
Extism is an open-source product from the team at:
<p align="left">
<a href="https://dylib.so" _target="blanks"><img width="200px" src="https://dylib.so/assets/dylibso-logo.svg"/></a>
</p>
_Reach out and tell us what you're building! We'd love to help._

2
c/Makefile Normal file
View File

@@ -0,0 +1,2 @@
build:
clang -o main main.c -lextism -L .

56
c/main.c Normal file
View File

@@ -0,0 +1,56 @@
#include "../runtime/extism.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
uint8_t *read_file(const char *filename, size_t *len) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
return NULL;
}
fseek(fp, 0, SEEK_END);
size_t length = ftell(fp);
fseek(fp, 0, SEEK_SET);
uint8_t *data = malloc(length);
if (data == NULL) {
return NULL;
}
assert(fread(data, 1, length, fp) == length);
fclose(fp);
*len = length;
return data;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fputs("Not enough arguments\n", stderr);
exit(1);
}
size_t len = 0;
uint8_t *data = read_file("../wasm/code.wasm", &len);
ExtismPlugin plugin = extism_plugin_register(data, len, false);
free(data);
if (plugin < 0) {
exit(1);
}
assert(extism_call(plugin, "count_vowels", (uint8_t *)argv[1],
strlen(argv[1])) == 0);
ExtismSize out_len = extism_output_length(plugin);
char output[out_len];
extism_output_get(plugin, (uint8_t *)output, out_len);
write(STDOUT_FILENO, output, out_len);
write(STDOUT_FILENO, "\n", 1);
return 0;
}

3
cpp/Makefile Normal file
View File

@@ -0,0 +1,3 @@
build:
clang++ -std=c++11 -o example example.cpp -lextism -L .

30
cpp/example.cpp Normal file
View File

@@ -0,0 +1,30 @@
#include "extism.hpp"
#include <cstring>
#include <fstream>
#include <iostream>
using namespace extism;
std::vector<uint8_t> read(const char *filename) {
std::ifstream file(filename, std::ios::binary);
return std::vector<uint8_t>((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
}
int main(int argc, char *argv[]) {
auto wasm = read("../wasm/code.wasm");
Plugin plugin(wasm);
if (argc < 2) {
std::cout << "Not enough arguments" << std::endl;
return 1;
}
auto input = std::vector<uint8_t>((uint8_t *)argv[1],
(uint8_t *)argv[1] + strlen(argv[1]));
auto output = plugin.call("count_vowels", input);
std::string str(output.begin(), output.end());
std::cout << str << std::endl;
return 0;
}

1
cpp/extism.h Symbolic link
View File

@@ -0,0 +1 @@
../core/extism.h

56
cpp/extism.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include <string>
#include <vector>
extern "C" {
#include "extism.h"
}
namespace extism {
class Error : public std::exception {
private:
std::string message;
public:
Error(std::string msg) : message(msg) {}
const char *what() { return message.c_str(); }
};
class Plugin {
ExtismPlugin plugin;
public:
Plugin(const uint8_t *wasm, size_t length, bool with_wasi = false) {
this->plugin = extism_plugin_register(wasm, length, with_wasi);
if (this->plugin < 0) {
throw Error("Unable to load plugin");
}
}
Plugin(const std::string &s, bool with_wasi = false)
: Plugin((const uint8_t *)s.c_str(), s.size(), with_wasi) {}
Plugin(const std::vector<uint8_t> &s, bool with_wasi = false)
: Plugin(s.data(), s.size(), with_wasi) {}
std::vector<uint8_t> call(const std::string &func,
std::vector<uint8_t> input) {
int32_t rc =
extism_call(this->plugin, func.c_str(), input.data(), input.size());
if (rc != 0) {
const char *error = extism_error(this->plugin);
if (error == nullptr) {
throw Error("extism_call failed");
}
throw Error(error);
}
ExtismSize length = extism_output_length(this->plugin);
std::vector<uint8_t> out = std::vector<uint8_t>(length);
extism_output_get(this->plugin, out.data(), length);
return out;
}
};
} // namespace extism

104
extism.go Normal file
View File

@@ -0,0 +1,104 @@
package extism
import (
"encoding/json"
"errors"
"fmt"
"io"
"unsafe"
)
/*
#cgo pkg-config: libextism.pc
#include <extism.h>
*/
import "C"
type Plugin struct {
id int32
}
type WasmData struct {
Data []byte `json:"data"`
Hash string `json:"hash,omitempty"`
Name string `json:"name,omitempty"`
}
type WasmFile struct {
Path string `json:"path"`
Hash string `json:"hash,omitempty"`
Name string `json:"name,omitempty"`
}
type WasmUrl struct {
Url string `json:"url"`
Hash string `json:"hash,omitempty"`
Header map[string]string `json:"header,omitempty"`
Name string `json:"name,omitempty"`
Method string `json:"method,omitempty"`
}
type Wasm interface{}
type Manifest struct {
Wasm []Wasm `json:"wasm"`
Memory struct {
Max uint32 `json:"max,omitempty"`
} `json:"memory,omitempty"`
Config map[string]string `json:"config,omitempty"`
}
func register(data []byte, wasi bool) (Plugin, error) {
plugin := C.extism_plugin_register(
(*C.uchar)(unsafe.Pointer(&data[0])),
C.uint64_t(len(data)),
C._Bool(wasi),
)
if plugin < 0 {
return Plugin{id: -1}, errors.New("Unable to load plugin")
}
return Plugin{id: int32(plugin)}, nil
}
func LoadManifest(manifest Manifest, wasi bool) (Plugin, error) {
data, err := json.Marshal(manifest)
if err != nil {
return Plugin{id: -1}, err
}
return register(data, wasi)
}
func LoadPlugin(module io.Reader, wasi bool) (Plugin, error) {
wasm, err := io.ReadAll(module)
if err != nil {
return Plugin{id: -1}, err
}
return register(wasm, wasi)
}
func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
rc := C.extism_call(
C.int32_t(plugin.id),
C.CString(functionName),
(*C.uchar)(unsafe.Pointer(&input[0])),
C.uint64_t(len(input)),
)
if rc != 0 {
error := C.extism_error(C.int32_t(plugin.id))
if error != nil {
return nil, errors.New(
fmt.Sprintf("ERROR (extism plugin code: %d): %s", rc, C.GoString(error)),
)
}
}
length := C.extism_output_length(C.int32_t(plugin.id))
buf := make([]byte, length)
C.extism_output_get(C.int32_t(plugin.id), (*C.uchar)(unsafe.Pointer(&buf[0])), length)
return buf, nil
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/extism/extism
go 1.18

7
go/go.mod Normal file
View File

@@ -0,0 +1,7 @@
module github.com/extism/extism-go-example
go 1.18
replace github.com/extism/extism => ../
require github.com/extism/extism v0.0.0-00010101000000-000000000000

41
go/main.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/extism/extism"
)
func main() {
// set some input data to provide to the plugin module
var data []byte
if len(os.Args) > 1 {
data = []byte(os.Args[1])
} else {
data = []byte("testing from go -> wasm shared memory...")
}
manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: "../wasm/code.wasm"}}}
plugin, err := extism.LoadManifest(manifest, false)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// use the extism Go library to provide the input data to the plugin, execute it, and then
// collect the plugin state and error if present
out, err := plugin.Call("count_vowels", data)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// "out" is []byte type, and the plugin sends back json, so deserialize it into a map.
// expect this object: `{"count": n}`
var dest map[string]int
json.Unmarshal(out, &dest)
fmt.Println("Count:", dest["count"])
}

10
libextism.pc Normal file
View File

@@ -0,0 +1,10 @@
prefix=/usr/local
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib
Name: extism
Description: The Extism universal plug-in system.
Version: 0.0.1
Cflags: -I${includedir}
Libs: -L${libdir} -lextism

13
manifest/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "extism-manifest"
version = "0.0.1-alpha"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"
homepage = "https://extism.org"
repository = "https://github.com/extism/extism"
description = "Extism plug-in manifest crate"
[dependencies]
serde = {version = "1", features=["derive"]}
base64 = "0.20.0-alpha"

55
manifest/src/lib.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::collections::BTreeMap;
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub struct ManifestMemory {
pub max: Option<u32>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum ManifestWasm {
File {
path: std::path::PathBuf,
name: Option<String>,
hash: Option<String>,
},
Data {
#[serde(with = "base64")]
data: Vec<u8>,
name: Option<String>,
hash: Option<String>,
},
Url {
url: String,
#[serde(default)]
header: BTreeMap<String, String>,
name: Option<String>,
method: Option<String>,
hash: Option<String>,
},
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub struct Manifest {
#[serde(default = "Vec::new")]
pub wasm: Vec<ManifestWasm>,
#[serde(default)]
pub memory: ManifestMemory,
#[serde(default)]
pub config: BTreeMap<String, String>,
}
mod base64 {
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
let base64 = base64::encode(v);
String::serialize(&base64, s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64 = String::deserialize(d)?;
base64::decode(base64.as_bytes()).map_err(|e| serde::de::Error::custom(e))
}
}

7
node/example.js Normal file
View File

@@ -0,0 +1,7 @@
import { Plugin } from "./index.js";
import { readFileSync } from "fs";
let wasm = readFileSync("../wasm/code.wasm");
let p = new Plugin(wasm);
let buf = p.call("count_vowels", process.argv[2] || "this is a test");
console.log(JSON.parse(buf.toString())['count']);

46
node/index.js Normal file
View File

@@ -0,0 +1,46 @@
import ffi from 'ffi-napi';
import path from 'path';
import url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
var lib = ffi.Library(
'libextism',
{
extism_plugin_register: ['int32', ['string', 'uint64', 'bool']],
extism_error: ['char*', ['int32']],
extism_call: ['int32', ['int32', 'string', 'string', 'uint64']],
extism_output_length: ['uint64', ['int32']],
extism_output_get: ['void', ['int32', 'char*', 'uint64']]
}
)
export class Plugin {
constructor(data, wasi = false) {
if (typeof data === "object" && data.wasm) {
data = JSON.stringify(data);
}
let plugin = lib.extism_plugin_register(data, data.length, wasi);
if (plugin < 0) {
throw "Unable to load plugin";
}
this.id = plugin;
}
call(name, input) {
var rc = lib.extism_call(this.id, name, input, input.length);
if (rc != 0) {
var err = lib.extism_error(this.id);
if (err.length == 0) {
throw "extism_call failed";
}
throw err.toString();
}
var out_len = lib.extism_output_length(this.id);
var buf = new Buffer.alloc(out_len);
lib.extism_output_get(this.id, buf, out_len);
return buf;
}
}

193
node/package-lock.json generated Normal file
View File

@@ -0,0 +1,193 @@
{
"name": "node",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "node",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"ffi-napi": "^4.0.3"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/ffi-napi": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
"hasInstallScript": true,
"dependencies": {
"debug": "^4.1.1",
"get-uv-event-loop-napi-h": "^1.0.5",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1",
"ref-napi": "^2.0.1 || ^3.0.2",
"ref-struct-di": "^1.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/get-symbol-from-current-process-h": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw=="
},
"node_modules/get-uv-event-loop-napi-h": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
"dependencies": {
"get-symbol-from-current-process-h": "^1.0.1"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
},
"node_modules/node-gyp-build": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz",
"integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/ref-napi": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
"integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
"hasInstallScript": true,
"dependencies": {
"debug": "^4.1.1",
"get-symbol-from-current-process-h": "^1.0.2",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/ref-struct-di": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
"dependencies": {
"debug": "^3.1.0"
}
},
"node_modules/ref-struct-di/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dependencies": {
"ms": "^2.1.1"
}
}
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ffi-napi": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
"requires": {
"debug": "^4.1.1",
"get-uv-event-loop-napi-h": "^1.0.5",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1",
"ref-napi": "^2.0.1 || ^3.0.2",
"ref-struct-di": "^1.1.0"
}
},
"get-symbol-from-current-process-h": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw=="
},
"get-uv-event-loop-napi-h": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
"requires": {
"get-symbol-from-current-process-h": "^1.0.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
},
"node-gyp-build": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz",
"integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg=="
},
"ref-napi": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
"integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
"requires": {
"debug": "^4.1.1",
"get-symbol-from-current-process-h": "^1.0.2",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1"
}
},
"ref-struct-di": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
"requires": {
"debug": "^3.1.0"
},
"dependencies": {
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
}
}
}
}
}

16
node/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"example": "node example.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"ffi-napi": "^4.0.3"
}
}

BIN
node/user_code.wasm Executable file

Binary file not shown.

1
ocaml/.ocamlformat Normal file
View File

@@ -0,0 +1 @@
version = 0.24.1

4
ocaml/bin/dune Normal file
View File

@@ -0,0 +1,4 @@
(executable
(public_name extism)
(name main)
(libraries extism))

10
ocaml/bin/main.ml Normal file
View File

@@ -0,0 +1,10 @@
open Extism
let () =
let input =
if Array.length Sys.argv > 1 then Sys.argv.(1) else "this is a test"
in
let manifest = Manifest.v [ Manifest.file "../wasm/code.wasm" ] in
let plugin = Extism.register_manifest manifest in
let res = Extism.call plugin ~name:"count_vowels" input |> Result.get_ok in
print_endline res

26
ocaml/dune-project Normal file
View File

@@ -0,0 +1,26 @@
(lang dune 3.2)
(name extism)
(generate_opam_files true)
(source
(github extism/extism))
(authors "Author Name")
(maintainers "Maintainer Name")
(license LICENSE)
(documentation https://url/to/documentation)
(package
(name extism)
(synopsis "A short synopsis")
(description "A longer description")
(depends ocaml dune ctypes-foreign bigstringaf ppx_yojson_conv)
(tags
(topics "to describe" your project)))
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project

34
ocaml/extism.opam Normal file
View File

@@ -0,0 +1,34 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis: "A short synopsis"
description: "A longer description"
maintainer: ["Maintainer Name"]
authors: ["Author Name"]
license: "LICENSE"
tags: ["topics" "to describe" "your" "project"]
homepage: "https://github.com/extism/extism"
doc: "https://url/to/documentation"
bug-reports: "https://github.com/extism/extism/issues"
depends: [
"ocaml"
"dune" {>= "3.2"}
"ctypes-foreign"
"bigstringaf"
"ppx_yojson_conv"
"odoc" {with-doc}
]
build: [
["dune" "subst"] {dev}
[
"dune"
"build"
"-p"
name
"-j"
jobs
"@install"
"@runtest" {with-test}
"@doc" {with-doc}
]
]
dev-repo: "git+https://github.com/extism/extism.git"

233
ocaml/extism.opam.locked Normal file
View File

@@ -0,0 +1,233 @@
opam-version: "2.0"
synopsis: "opam-monorepo generated lockfile"
maintainer: "opam-monorepo"
depends: [
"base" {= "v0.15.0" & ?vendor}
"base-bigarray" {= "base"}
"base-threads" {= "base"}
"base-unix" {= "base"}
"bigstringaf" {= "0.9.0" & ?vendor}
"conf-libffi" {= "2.0.0"}
"conf-pkg-config" {= "2"}
"cppo" {= "1.6.9" & ?vendor}
"csexp" {= "1.5.1" & ?vendor}
"ctypes-foreign" {= "0.4.0"}
"dune" {= "3.4.1"}
"dune-configurator" {= "3.4.1" & ?vendor}
"ocaml" {= "4.14.0"}
"ocaml-base-compiler" {= "4.14.0"}
"ocaml-compiler-libs" {= "v0.12.4" & ?vendor}
"ocaml-config" {= "2"}
"ocaml-options-vanilla" {= "1"}
"octavius" {= "1.2.2" & ?vendor}
"ppx_derivers" {= "1.2.1" & ?vendor}
"ppx_js_style" {= "v0.15.0" & ?vendor}
"ppx_yojson_conv" {= "v0.15.0" & ?vendor}
"ppx_yojson_conv_lib" {= "v0.15.0" & ?vendor}
"ppxlib" {= "0.26.0" & ?vendor}
"seq" {= "base"}
"sexplib0" {= "v0.15.1" & ?vendor}
"stdlib-shims" {= "0.3.0" & ?vendor}
"yojson" {= "2.0.2" & ?vendor}
]
depexts: [
["devel/pkgconf"] {os = "openbsd"}
["libffi"] {os = "freebsd"}
["libffi"] {os-distribution = "nixos"}
["libffi"] {os = "macos" & os-distribution = "homebrew"}
["libffi"] {os = "macos" & os-distribution = "macports"}
["libffi"] {os = "win32" & os-distribution = "cygwinports"}
["libffi-dev"] {os-distribution = "alpine"}
["libffi-dev"] {os-family = "debian"}
["libffi-devel"] {os-distribution = "centos"}
["libffi-devel"] {os-distribution = "fedora"}
["libffi-devel"] {os-distribution = "mageia"}
["libffi-devel"] {os-distribution = "ol"}
["libffi-devel"] {os-family = "suse"}
["pkg-config"] {os-family = "debian"}
["pkg-config"] {os = "macos" & os-distribution = "homebrew"}
["pkgconf"] {os = "freebsd"}
["pkgconf"] {os-distribution = "alpine"}
["pkgconf"] {os-distribution = "arch"}
["pkgconf-pkg-config"] {os-distribution = "fedora"}
["pkgconf-pkg-config"] {os-distribution = "mageia"}
["pkgconf-pkg-config"] {os-distribution = "centos" & os-version >= "8"}
["pkgconf-pkg-config"] {os-distribution = "ol" & os-version >= "8"}
["pkgconf-pkg-config"] {os-distribution = "rhel" & os-version >= "8"}
["pkgconfig"] {os-distribution = "nixos"}
["pkgconfig"] {os = "macos" & os-distribution = "macports"}
["pkgconfig"] {os-distribution = "centos" & os-version <= "7"}
["pkgconfig"] {os-distribution = "ol" & os-version <= "7"}
["pkgconfig"] {os-distribution = "rhel" & os-version <= "7"}
["system:pkgconf"] {os = "win32" & os-distribution = "cygwinports"}
]
pin-depends: [
[
"base.v0.15.0"
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/base-v0.15.0.tar.gz"
]
[
"bigstringaf.0.9.0"
"https://github.com/inhabitedtype/bigstringaf/archive/0.9.0.tar.gz"
]
[
"cppo.1.6.9"
"https://github.com/ocaml-community/cppo/archive/v1.6.9.tar.gz"
]
[
"csexp.1.5.1"
"https://github.com/ocaml-dune/csexp/releases/download/1.5.1/csexp-1.5.1.tbz"
]
[
"dune-configurator.3.4.1"
"https://github.com/ocaml/dune/releases/download/3.4.1/dune-3.4.1.tbz"
]
[
"ocaml-compiler-libs.v0.12.4"
"https://github.com/janestreet/ocaml-compiler-libs/releases/download/v0.12.4/ocaml-compiler-libs-v0.12.4.tbz"
]
[
"octavius.1.2.2"
"https://github.com/ocaml-doc/octavius/archive/v1.2.2.tar.gz"
]
[
"ppx_derivers.1.2.1"
"https://github.com/ocaml-ppx/ppx_derivers/archive/1.2.1.tar.gz"
]
[
"ppx_js_style.v0.15.0"
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_js_style-v0.15.0.tar.gz"
]
[
"ppx_yojson_conv.v0.15.0"
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_yojson_conv-v0.15.0.tar.gz"
]
[
"ppx_yojson_conv_lib.v0.15.0"
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_yojson_conv_lib-v0.15.0.tar.gz"
]
[
"ppxlib.0.26.0"
"https://github.com/ocaml-ppx/ppxlib/releases/download/0.26.0/ppxlib-0.26.0.tbz"
]
[
"sexplib0.v0.15.1"
"https://github.com/janestreet/sexplib0/archive/refs/tags/v0.15.1.tar.gz"
]
[
"stdlib-shims.0.3.0"
"https://github.com/ocaml/stdlib-shims/releases/download/0.3.0/stdlib-shims-0.3.0.tbz"
]
[
"yojson.2.0.2"
"https://github.com/ocaml-community/yojson/releases/download/2.0.2/yojson-2.0.2.tbz"
]
]
x-opam-monorepo-duniverse-dirs: [
[
"https://github.com/inhabitedtype/bigstringaf/archive/0.9.0.tar.gz"
"bigstringaf"
["md5=0d8ceddeb7db821fd4e5235a75ae9752"]
]
[
"https://github.com/janestreet/ocaml-compiler-libs/releases/download/v0.12.4/ocaml-compiler-libs-v0.12.4.tbz"
"ocaml-compiler-libs"
[
"sha256=4ec9c9ec35cc45c18c7a143761154ef1d7663036a29297f80381f47981a07760"
"sha512=978dba8dfa61f98fa24fda7a9c26c2e837081f37d1685fe636dc19cfc3278a940cf01a10293504b185c406706bc1008bc54313d50f023bcdea6d5ac6c0788b35"
]
]
[
"https://github.com/janestreet/sexplib0/archive/refs/tags/v0.15.1.tar.gz"
"sexplib0"
["md5=ab8fd6273f35a792cad48cbb3024a7f9"]
]
[
"https://github.com/ocaml-community/cppo/archive/v1.6.9.tar.gz"
"cppo"
[
"md5=d23ffe85ac7dc8f0afd1ddf622770d09"
"sha512=26ff5a7b7f38c460661974b23ca190f0feae3a99f1974e0fd12ccf08745bd7d91b7bc168c70a5385b837bfff9530e0e4e41cf269f23dd8cf16ca658008244b44"
]
]
[
"https://github.com/ocaml-community/yojson/releases/download/2.0.2/yojson-2.0.2.tbz"
"yojson"
[
"sha256=876bb6f38af73a84a29438a3da35e4857c60a14556a606525b148c6fdbe5461b"
"sha512=9e150689a814a64e53e361e336fe826df5a3e3851d1367fda4a001392175c29348de55db0b7d7ba18539dec2cf78198efcb7f41b77a9861763f5aa97c05509ad"
]
]
[
"https://github.com/ocaml-doc/octavius/archive/v1.2.2.tar.gz"
"octavius"
["md5=72f9e1d996e6c5089fc513cc9218607b"]
]
[
"https://github.com/ocaml-dune/csexp/releases/download/1.5.1/csexp-1.5.1.tbz"
"csexp"
[
"sha256=d605e4065fa90a58800440ef2f33a2d931398bf2c22061a8acb7df845c0aac02"
"sha512=d785bbabaff9f6bf601399149ef0a42e5e99647b54e27f97ef1625907793dda22a45bf83e0e8a1eba2c63634c5484b54739ff0904ef556f5fc592efa38af7505"
]
]
[
"https://github.com/ocaml-ppx/ppx_derivers/archive/1.2.1.tar.gz"
"ppx_derivers"
["md5=5dc2bf130c1db3c731fe0fffc5648b41"]
]
[
"https://github.com/ocaml-ppx/ppxlib/releases/download/0.26.0/ppxlib-0.26.0.tbz"
"ppxlib"
[
"sha256=63117b67ea5863935455fe921f88fe333c0530f0483f730313303a93af817efd"
"sha512=9cfc9587657d17cdee5483e2a03292f872c42886e512bcc7442652e6418ce74c0135c731d8a68288c7fecae7f1b2defd93fe5acf8edb41e25144a8cec2806227"
]
]
[
"https://github.com/ocaml/dune/releases/download/3.4.1/dune-3.4.1.tbz"
"dune_"
[
"sha256=299fa33cffc108cc26ff59d5fc9d09f6cb0ab3ac280bf23a0114cfdc0b40c6c5"
"sha512=cb425d08c989fd27e1a87a6c72f37494866b508b0fe4ec05070adad995a99710b223a9047b6649776f63943dafb61903eefe4d5efde4c439103a89e2d6ff5337"
]
]
[
"https://github.com/ocaml/stdlib-shims/releases/download/0.3.0/stdlib-shims-0.3.0.tbz"
"stdlib-shims"
[
"sha256=babf72d3917b86f707885f0c5528e36c63fccb698f4b46cf2bab5c7ccdd6d84a"
"sha512=1151d7edc8923516e9a36995a3f8938d323aaade759ad349ed15d6d8501db61ffbe63277e97c4d86149cf371306ac23df0f581ec7e02611f58335126e1870980"
]
]
[
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/base-v0.15.0.tar.gz"
"base"
[
"sha256=8657ae4324a9948457112245c49d97d2da95f157f780f5d97f0b924312a6a53d"
]
]
[
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_js_style-v0.15.0.tar.gz"
"ppx_js_style"
[
"sha256=9d05e3f97bf9351146e95a3bf99cdc77873738639bb29eded61888b7e38febeb"
]
]
[
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_yojson_conv-v0.15.0.tar.gz"
"ppx_yojson_conv"
[
"sha256=1efba0d62128e43b618ada9d6ccd615e46d5227611594f0b2e01a8d6c0f9b40f"
]
]
[
"https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_yojson_conv_lib-v0.15.0.tar.gz"
"ppx_yojson_conv_lib"
[
"sha256=f9d2c5eff4566ec1f1f379b186ed22c8ddd6be0909a160bc5a9ac7abc6a6b684"
]
]
]
x-opam-monorepo-root-packages: ["extism"]
x-opam-monorepo-version: "0.3"

4
ocaml/lib/dune Normal file
View File

@@ -0,0 +1,4 @@
(library
(name extism)
(libraries ctypes.foreign bigstringaf)
(preprocess (pps ppx_yojson_conv)))

161
ocaml/lib/extism.ml Normal file
View File

@@ -0,0 +1,161 @@
let ( // ) = Filename.concat
module Bindings = struct
let paths =
[
"/usr/lib";
"/usr/local/lib";
Sys.getenv "HOME" // ".local/lib";
Sys.getcwd ();
]
let check x =
let a = x // "libextism.so" in
let b = x // "libextism.dylib" in
if Sys.file_exists a then Some a
else if Sys.file_exists b then Some b
else None
let locate () =
let init =
match Sys.getenv_opt "EXTISM_PATH" with
| Some path -> (
match check path with
| None -> check (path // "lib")
| Some _ as x -> x)
| None -> None
in
List.fold_left
(fun acc path -> match acc with Some _ -> acc | None -> check path)
init paths
|> function
| Some x -> x
| None -> raise Not_found
let from =
let filename = locate () in
Dl.dlopen ~filename ~flags:[ Dl.RTLD_GLOBAL; Dl.RTLD_NOW ]
open Ctypes
let fn = Foreign.foreign ~from ~release_runtime_lock:true
let extism_plugin_register =
fn "extism_plugin_register"
(string @-> uint64_t @-> bool @-> returning int32_t)
let extism_call =
fn "extism_call"
(int32_t @-> string @-> ptr char @-> uint64_t @-> returning int32_t)
let extism_call_s =
fn "extism_call"
(int32_t @-> string @-> string @-> uint64_t @-> returning int32_t)
let extism_error = fn "extism_error" (int32_t @-> returning string_opt)
let extism_output_length =
fn "extism_output_length" (int32_t @-> returning uint64_t)
let extism_output_get =
fn "extism_output_get" (int32_t @-> ptr char @-> uint64_t @-> returning void)
end
type error = [ `Msg of string ]
type t = { id : int32 }
module Manifest = struct
type memory = { max : int option [@yojson.option] } [@@deriving yojson]
type wasm_file = {
path : string;
name : string option; [@yojson.option]
hash : string option; [@yojson.option]
}
[@@deriving yojson]
type wasm_data = {
data : string;
name : string option; [@yojson.option]
hash : string option; [@yojson.option]
}
[@@deriving yojson]
type wasm_url = {
url : string;
header : (string * string) list option; [@yojson.option]
name : string option; [@yojson.option]
meth : string option; [@yojson.option] [@key "method"]
hash : string option; [@yojson.option]
}
[@@deriving yojson]
type config = (string * string) list
type wasm = File of wasm_file | Data of wasm_data | Url of wasm_url
let yojson_of_wasm = function
| File f -> yojson_of_wasm_file f
| Data d -> yojson_of_wasm_data d
| Url u -> yojson_of_wasm_url u
let wasm_of_yojson x =
try File (wasm_file_of_yojson x)
with _ -> (
try Data (wasm_data_of_yojson x) with _ -> Url (wasm_url_of_yojson x))
let config_of_yojson j =
let assoc = Yojson.Safe.Util.to_assoc j in
List.map (fun (k, v) -> (k, Yojson.Safe.Util.to_string v)) assoc
let yojson_of_config c = `Assoc (List.map (fun (k, v) -> (k, `String v)) c)
type t = {
wasm : wasm list;
memory : memory option; [@yojson.option]
config : config option; [@yojson.option]
}
[@@deriving yojson]
let file ?name ?hash path = File { path; name; hash }
let data ?name ?hash data = Data { data; name; hash }
let url ?header ?name ?meth ?hash url = Url { header; name; meth; hash; url }
let v ?config ?memory wasm = { config; wasm; memory }
let json t = yojson_of_t t |> Yojson.Safe.to_string
end
exception Failed_to_load_plugin
let register ?(wasi = false) wasm =
let id =
Bindings.extism_plugin_register wasm
(Unsigned.UInt64.of_int (String.length wasm))
wasi
in
if id < 0l then raise Failed_to_load_plugin else { id }
let register_manifest ?wasi manifest =
let data = Manifest.json manifest in
register ?wasi data
let call' f { id } ~name input len =
let rc = f id name input len in
if rc <> 0l then
match Bindings.extism_error id with
| None -> Error (`Msg "extism_call failed")
| Some msg -> Error (`Msg msg)
else
let out_len = Bindings.extism_output_length id in
let buf = Bigstringaf.create (Unsigned.UInt64.to_int out_len) in
let ptr = Ctypes.bigarray_start Ctypes.array1 buf in
let () = Bindings.extism_output_get id ptr out_len in
Ok buf
let call_bigstring t ~name input =
let len = Unsigned.UInt64.of_int (Bigstringaf.length input) in
let ptr = Ctypes.bigarray_start Ctypes.array1 input in
call' Bindings.extism_call t ~name ptr len
let call t ~name input =
let len = String.length input in
call' Bindings.extism_call_s t ~name input (Unsigned.UInt64.of_int len)
|> Result.map Bigstringaf.to_string

48
ocaml/lib/extism.mli Normal file
View File

@@ -0,0 +1,48 @@
type t
type error = [`Msg of string]
exception Failed_to_load_plugin
module Manifest : sig
type memory = { max : int option } [@@deriving yojson]
type wasm_file = {
path : string;
name : string option; [@yojson.option]
hash : string option; [@yojson.option]
}
type wasm_data = {
data : string;
name : string option; [@yojson.option]
hash : string option; [@yojson.option]
}
type wasm_url = {
url : string;
header : (string * string) list option; [@yojson.option]
name : string option; [@yojson.option]
meth : string option; [@yojson.option] [@key "method"]
hash : string option; [@yojson.option]
}
type wasm = File of wasm_file | Data of wasm_data | Url of wasm_url
type config = (string * string) list
type t = {
wasm : wasm list;
memory : memory option;
config: config option;
}
val file: ?name:string -> ?hash:string -> string -> wasm
val data: ?name:string -> ?hash:string -> string -> wasm
val url: ?header:(string * string) list -> ?name:string -> ?meth:string -> ?hash:string -> string -> wasm
val v: ?config:config -> ?memory:memory -> wasm list -> t
val json: t -> string
end
val register: ?wasi:bool -> string -> t
val register_manifest: ?wasi:bool -> Manifest.t -> t
val call_bigstring: t -> name:string -> Bigstringaf.t -> (Bigstringaf.t, error) result
val call: t -> name:string -> string -> (string, error) result

34
python/example.py Normal file
View File

@@ -0,0 +1,34 @@
import sys
import os
import json
import hashlib
sys.path.append(".")
from extism import Plugin
if len(sys.argv) > 1:
data = sys.argv[1].encode()
else:
data = b"some data from python!"
wasm = open("../wasm/code.wasm", 'rb').read()
hash = hashlib.sha256(wasm).hexdigest()
config = {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max": 5}}
plugin = Plugin(config)
# Call `count_vowels`
j = json.loads(plugin.call("count_vowels", data))
print("Number of vowels:", j["count"])
# Compare against Python implementation
def count_vowels(data):
count = 0
for c in data:
if c in b'AaEeIiOoUu':
count += 1
return count
assert (j["count"] == count_vowels(data))

View File

@@ -0,0 +1 @@
from .extism import Error, Plugin

120
python/extism/extism.py Normal file
View File

@@ -0,0 +1,120 @@
import sys
import json
import os
from base64 import b64encode
from cffi import FFI
from typing import Union
class Error(Exception):
pass
search_dirs = [
"/usr/local", "/usr",
os.path.join(os.getenv("HOME"), ".local"), "."
]
def exists(a, *b):
return os.path.exists(os.path.join(a, *b))
def check_for_header_and_lib(p):
if exists(p, "extism.h"):
if exists(p, "libextism.so"):
return os.path.join(p, "extism.h"), os.path.join(p, "libextism.so")
if exists(p, "libextism.dylib"):
return os.path.join(p, "extism.h"), os.path.join(
p, "libextism.dylib")
if exists(p, "include", "extism.h"):
if exists(p, "lib", "libextism.so"):
return os.path.join(p, "include", "extism.h"), os.path.join(
p, "lib", "libextism.so")
if exists(p, "lib", "libextism.dylib"):
return os.path.join(p, "include", "extism.h"), os.path.join(
p, "lib", "libextism.dylib")
def locate():
'''Locate extism library and header'''
script_dir = os.path.dirname(__file__)
env = os.getenv("EXTISM_PATH")
if env is not None:
r = check_for_header_and_lib(env)
if r is not None:
return r
r = check_for_header_and_lib(script_dir)
if r is not None:
return r
r = check_for_header_and_lib(".")
if r is not None:
return r
for d in search_dirs:
r = check_for_header_and_lib(d)
if r is not None:
return r
raise Error("Unable to locate the extism library and header file")
# Initialize the C library
ffi = FFI()
header, lib = locate()
with open(header) as f:
lines = []
for line in f.readlines():
if line[0] != '#':
lines.append(line)
ffi.cdef(''.join(lines))
lib = ffi.dlopen(lib)
class Base64Encoder(json.JSONEncoder):
# pylint: disable=method-hidden
def default(self, o):
if isinstance(o, bytes):
return b64encode(o).decode()
return json.JSONEncoder.default(self, o)
class Plugin:
def __init__(self, plugin: Union[str, bytes, dict], wasi=False):
if isinstance(plugin, str) and os.path.exists(plugin):
with open(plugin, 'rb') as f:
wasm = f.read()
elif isinstance(plugin, str):
wasm = plugin.encode()
elif isinstance(plugin, dict):
wasm = json.dumps(plugin, cls=Base64Encoder).encode()
else:
wasm = plugin
# Register plugin
self.plugin = lib.extism_plugin_register(wasm, len(wasm), wasi)
def _check_error(self, rc):
if rc != 0:
error = lib.extism_error(self.plugin)
if error != ffi.NULL:
raise Error(ffi.string(error))
raise Error(f"Error code: {rc}")
def call(self, name: str, data: Union[str, bytes]) -> bytes:
if isinstance(data, str):
data = data.encode()
self._check_error(
lib.extism_call(self.plugin, name.encode(), data, len(data)))
out_len = lib.extism_output_length(self.plugin)
out_buf = ffi.new("uint8_t[]", out_len)
lib.extism_output_get(self.plugin, out_buf, out_len)
return ffi.string(out_buf)

16
python/pyproject.toml Normal file
View File

@@ -0,0 +1,16 @@
[tool.poetry]
name = "extism"
version = "0.1.0"
description = ""
authors = ["Zach Shipko <zachshipko@gmail.com>"]
license = "ISC"
[tool.poetry.dependencies]
python = "^3.7"
cffi = "^1.10.0"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

10
ruby/Gemfile Normal file
View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
source "https://rubygems.org"
# Specify your gem's dependencies in extism.gemspec
gemspec
gem "rake", "~> 13.0"
gem "ffi"

3
ruby/Rakefile Normal file
View File

@@ -0,0 +1,3 @@
# frozen_string_literal: true
require "bundler/gem_tasks"

15
ruby/bin/console Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "extism"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)

8
ruby/bin/setup Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here

10
ruby/example.rb Normal file
View File

@@ -0,0 +1,10 @@
require './lib/extism'
require 'json'
manifest = {
:wasm => [{:path => "../wasm/code.wasm"}]
}
plugin = Plugin.new(manifest)
res = JSON.parse(plugin.call("count_vowels", ARGV[0] || "this is a test"))
puts res['count']

39
ruby/extism.gemspec Normal file
View File

@@ -0,0 +1,39 @@
# frozen_string_literal: true
require_relative "lib/extism/version"
Gem::Specification.new do |spec|
spec.name = "extism"
spec.version = Extism::VERSION
spec.authors = ["zach"]
spec.email = ["zachshipko@gmail.com"]
spec.summary = "Extism WASM SDK"
spec.description = "A library for loading and executing WASM plugins"
spec.homepage = "https://github.com/extism/extism"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
#spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/extism/extism"
spec.metadata["changelog_uri"] = "https://github.com/extism/extism"
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject do |f|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
end
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
# Uncomment to register a new dependency of your gem
# spec.add_dependency "example-gem", "~> 1.0"
# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
end

43
ruby/lib/extism.rb Normal file
View File

@@ -0,0 +1,43 @@
require 'ffi'
require 'json'
module C
extend FFI::Library
ffi_lib "extism"
attach_function :extism_plugin_register, [:pointer, :uint64, :bool], :int32
attach_function :extism_error, [:int32], :string
attach_function :extism_call, [:int32, :string, :pointer, :uint64], :int32
attach_function :extism_output_length, [:int32], :uint64
attach_function :extism_output_get, [:int32, :pointer, :uint64], :void
end
class Error < StandardError
end
class Plugin
def initialize(wasm, wasi=false)
if wasm.class == Hash then
wasm = JSON.generate(wasm)
end
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
code.put_bytes(0, wasm)
@plugin = C.extism_plugin_register(code, wasm.bytesize, wasi)
end
def call(name, data)
input = FFI::MemoryPointer::from_string(data)
rc = C.extism_call(@plugin, name, input, data.bytesize)
if rc != 0 then
err = C.extism_error(@plugin)
if err.empty? then
raise Error.new "extism_call failed"
else raise Error.new err
end
end
out_len = C.extism_output_length(@plugin)
buf = FFI::MemoryPointer.new(:char, out_len)
C.extism_output_get(@plugin, buf, out_len)
return buf.read_string()
end
end

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Extism
VERSION = "0.1.0"
end

4
ruby/sig/extism.rbs Normal file
View File

@@ -0,0 +1,4 @@
module Extism
VERSION: String
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
end

BIN
ruby/user_code.wasm Executable file

Binary file not shown.

37
runtime/Cargo.toml Normal file
View File

@@ -0,0 +1,37 @@
[package]
name = "extism-runtime"
version = "0.0.1-alpha"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"
homepage = "https://extism.org"
repository = "https://github.com/extism/extism"
description = "Extism runtime component"
[lib]
name = "extism"
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasmtime = "0.39.1"
wasmtime-wasi = "0.39.1"
anyhow = "1"
serde = { version = "1", features=["derive"] }
toml = "0.5"
serde_json = "1"
sha2 = "0.10"
ureq = {version = "2.5", optional=true}
extism-manifest = { version = "0.0.1-alpha", path = "../manifest" }
pretty-hex = { version = "0.3", optional = true }
[features]
default = ["http", "register-http", "register-filesystem"]
register-http = ["ureq"] # enables wasm to be downloaded using http
register-filesystem = [] # enables wasm to be loaded from disk
http = ["ureq"] # enables extism_http_request
debug = ["pretty-hex"]
[build-dependencies]
cbindgen = "0.24"

16
runtime/build.rs Normal file
View File

@@ -0,0 +1,16 @@
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
if let Ok(bindings) = cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.with_pragma_once(true)
.rename_item("Size", "ExtismSize")
.rename_item("PluginIndex", "ExtismPlugin")
.generate()
{
bindings.write_to_file("extism.h");
}
}

27
runtime/extism.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef int32_t ExtismPlugin;
typedef uint64_t ExtismSize;
ExtismPlugin extism_plugin_register(const uint8_t *wasm, ExtismSize wasm_size, bool with_wasi);
bool extism_plugin_config(ExtismPlugin plugin, const uint8_t *json, ExtismSize json_size);
bool extism_function_exists(ExtismPlugin plugin, const char *func_name);
int32_t extism_call(ExtismPlugin plugin,
const char *func_name,
const uint8_t *data,
ExtismSize data_len);
const char *extism_error(ExtismPlugin plugin);
ExtismSize extism_output_length(ExtismPlugin plugin);
void extism_output_get(ExtismPlugin plugin, uint8_t *buf, ExtismSize len);

332
runtime/src/export.rs Normal file
View File

@@ -0,0 +1,332 @@
use crate::*;
macro_rules! plugin {
(mut $a:expr) => {
unsafe { (&mut *$a.plugin) }
};
($a:expr) => {
unsafe { (&*$a.plugin) }
};
}
macro_rules! memory {
(mut $a:expr) => {
&mut plugin!(mut $a).memory
};
($a:expr) => {
&plugin!($a).memory
};
}
pub(crate) fn input_offset(
caller: Caller<Internal>,
_input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &Internal = caller.data();
output[0] = Val::I64(data.input_offset as i64);
Ok(())
}
pub(crate) fn output_set(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
data.output_offset = input[0].unwrap_i64() as usize;
data.output_length = input[1].unwrap_i64() as usize;
Ok(())
}
pub(crate) fn alloc(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let offs = memory!(mut data).alloc(input[0].unwrap_i64() as _)?;
output[0] = Val::I64(offs.offset as i64);
Ok(())
}
pub(crate) fn free(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let offset = input[0].unwrap_i64() as usize;
memory!(mut data).free(offset);
Ok(())
}
pub(crate) fn store_u8(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let byte = input[1].unwrap_i32() as u8;
memory!(mut data)
.store_u8(input[0].unwrap_i64() as usize, byte)
.map_err(|_| Trap::new("Write error"))?;
Ok(())
}
pub(crate) fn load_u8(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let byte = memory!(data)
.load_u8(input[0].unwrap_i64() as usize)
.map_err(|_| Trap::new("Read error"))?;
output[0] = Val::I32(byte as i32);
Ok(())
}
pub(crate) fn store_u32(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let b = input[1].unwrap_i32() as u32;
memory!(mut data)
.store_u32(input[0].unwrap_i64() as usize, b)
.map_err(|_| Trap::new("Write error"))?;
Ok(())
}
pub(crate) fn load_u32(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let b = memory!(data)
.load_u32(input[0].unwrap_i64() as usize)
.map_err(|_| Trap::new("Read error"))?;
output[0] = Val::I32(b as i32);
Ok(())
}
pub(crate) fn store_u64(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let b = input[1].unwrap_i64() as u64;
memory!(mut data)
.store_u64(input[0].unwrap_i64() as usize, b)
.map_err(|_| Trap::new("Write error"))?;
Ok(())
}
pub(crate) fn load_u64(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let byte = memory!(data)
.load_u64(input[0].unwrap_i64() as usize)
.map_err(|_| Trap::new("Read error"))?;
output[0] = Val::I64(byte as i64);
Ok(())
}
pub(crate) fn error_set(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let offset = input[0].unwrap_i64() as usize;
let length = match memory!(data).block_length(offset) {
Some(x) => x,
None => return Err(Trap::new("Invalid offset in call to error_set")),
};
let handle = MemoryBlock { offset, length };
if handle.offset == 0 {
plugin!(mut data).clear_error();
return Ok(());
}
let buf = memory!(data).get(handle);
let s = unsafe { std::str::from_utf8_unchecked(buf) };
plugin!(mut data).set_error(s);
Ok(())
}
pub(crate) fn config_get(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let offset = input[0].unwrap_i64() as usize;
let length = match memory!(data).block_length(offset) {
Some(x) => x,
None => return Err(Trap::new("Invalid offset in call to config_get")),
};
let buf = memory!(data).get((offset, length));
let str = unsafe { std::str::from_utf8_unchecked(buf) };
let val = plugin!(data).manifest.as_ref().config.get(str);
let mem = match val {
Some(f) => memory!(mut data).alloc_bytes(f.as_bytes())?,
None => return Err(Trap::new("Invalid config key")),
};
output[0] = Val::I64(mem.offset as i64);
Ok(())
}
pub(crate) fn var_get(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let offset = input[0].unwrap_i64() as usize;
let length = match memory!(data).block_length(offset) {
Some(x) => x,
None => return Err(Trap::new("Invalid offset in call to var_get")),
};
let buf = memory!(data).get((offset, length));
let str = unsafe { std::str::from_utf8_unchecked(buf) };
let val = data.vars.get(str);
let mem = match val {
Some(f) => memory!(mut data).alloc_bytes(&f)?,
None => {
output[0] = Val::I64(0);
return Ok(());
}
};
output[0] = Val::I64(mem.offset as i64);
Ok(())
}
pub(crate) fn var_set(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let mut size = 0;
for v in data.vars.values() {
size += v.len();
}
let offset1 = input[1].unwrap_i64() as usize;
// If the store is larger than 100MB then stop adding things
if size > 1024 * 1024 * 100 && offset1 != 0 {
return Err(Trap::new("Variable store is full"));
}
let offset = input[0].unwrap_i64() as usize;
let length = match memory!(data).block_length(offset) {
Some(x) => x,
None => return Err(Trap::new("Invalid offset in call to var_set")),
};
let kbuf = memory!(data).get((offset, length));
let kstr = unsafe { std::str::from_utf8_unchecked(kbuf) };
let length1 = match memory!(data).block_length(offset) {
Some(x) => x,
None => return Err(Trap::new("Invalid offset in call to var_set")),
};
if offset1 == 0 {
data.vars.remove(kstr);
return Ok(());
}
let vbuf = memory!(data).get((offset1, length1));
data.vars.insert(kstr.to_string(), vbuf.to_vec());
Ok(())
}
#[derive(serde::Serialize, serde::Deserialize)]
struct HttpRequest {
url: String,
#[serde(default)]
header: std::collections::BTreeMap<String, String>,
method: Option<String>,
}
pub(crate) fn http_request(
#[allow(unused_mut)] mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
#[cfg(not(feature = "http"))]
{
let _ = (caller, input, output);
panic!("HTTP requests have been disabled");
}
#[cfg(feature = "http")]
{
use std::io::Read;
let data: &mut Internal = caller.data_mut();
let offset = input[0].unwrap_i64() as usize;
let length = match memory!(data).block_length(offset) {
Some(x) => x,
None => return Err(Trap::new("Invalid offset in call to config_get")),
};
let buf = memory!(data).get((offset, length));
let req: HttpRequest =
serde_json::from_slice(buf).map_err(|_| Trap::new("Invalid http request"))?;
let mut r = ureq::request(req.method.as_deref().unwrap_or("GET"), &req.url);
for (k, v) in req.header.iter() {
r = r.set(k, v);
}
let mut r = r
.call()
.map_err(|e| Trap::new(format!("{:?}", e)))?
.into_reader();
let mut buf = Vec::new();
r.read_to_end(&mut buf)
.map_err(|e| Trap::new(format!("{:?}", e)))?;
let mem = memory!(mut data).alloc_bytes(buf)?;
output[0] = Val::I64(mem.offset as i64);
Ok(())
}
}
pub(crate) fn length(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Trap> {
let data: &mut Internal = caller.data_mut();
let offset = input[0].unwrap_i64() as usize;
let length = match memory!(data).block_length(offset) {
Some(x) => x,
None => return Err(Trap::new("Unable to find length for offset")),
};
output[0] = Val::I64(length as i64);
Ok(())
}

17
runtime/src/lib.rs Normal file
View File

@@ -0,0 +1,17 @@
pub use anyhow::Error;
pub(crate) use wasmtime::*;
pub(crate) mod export;
pub mod manifest;
mod memory;
mod plugin;
mod plugin_ref;
pub mod sdk;
pub use manifest::Manifest;
pub use memory::{MemoryBlock, PluginMemory};
pub use plugin::{Internal, Plugin, PLUGINS};
pub use plugin_ref::PluginRef;
pub type Size = u64;
pub type PluginIndex = i32;

221
runtime/src/manifest.rs Normal file
View File

@@ -0,0 +1,221 @@
use std::collections::BTreeMap;
use std::fmt::Write as FmtWrite;
use std::io::Read;
use sha2::Digest;
use crate::*;
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Manifest(extism_manifest::Manifest);
fn hex(data: &[u8]) -> String {
let mut s = String::new();
for &byte in data {
write!(&mut s, "{:02x}", byte).unwrap();
}
s
}
#[allow(unused)]
fn cache_add_file(hash: &str, data: &[u8]) -> Result<(), Error> {
let cache_dir = std::env::temp_dir().join("exitsm-cache");
let _ = std::fs::create_dir(&cache_dir);
let file = cache_dir.join(hash);
if file.exists() {
return Ok(());
}
std::fs::write(file, data)?;
Ok(())
}
fn cache_get_file(hash: &str) -> Result<Option<Vec<u8>>, Error> {
let cache_dir = std::env::temp_dir().join("exitsm-cache");
let file = cache_dir.join(hash);
if file.exists() {
let r = std::fs::read(file)?;
return Ok(Some(r));
}
Ok(None)
}
fn check_hash(hash: &Option<String>, data: &[u8]) -> Result<(), Error> {
match hash {
None => Ok(()),
Some(hash) => {
let digest = sha2::Sha256::digest(data);
let hex = hex(&digest);
if &hex != hash {
return Err(anyhow::format_err!(
"Hash mismatch, found {} but expected {}",
hex,
hash
));
}
Ok(())
}
}
}
fn hash_url(url: &str) -> String {
let digest = sha2::Sha256::digest(url.as_bytes());
hex(&digest)
}
fn to_module(
engine: &Engine,
wasm: &extism_manifest::ManifestWasm,
) -> Result<(String, Module), Error> {
match wasm {
extism_manifest::ManifestWasm::File { path, name, hash } => {
if cfg!(not(feature = "register-filesystem")) {
return Err(anyhow::format_err!("File-based registration is disabled"));
}
let name = match name {
None => {
let name = path.with_extension("");
name.file_name().unwrap().to_string_lossy().to_string()
}
Some(n) => n.clone(),
};
let mut buf = Vec::new();
let mut file = std::fs::File::open(path)?;
file.read_to_end(&mut buf)?;
check_hash(hash, &buf)?;
Ok((name, Module::new(engine, buf)?))
}
extism_manifest::ManifestWasm::Data { name, data, hash } => {
check_hash(hash, data)?;
Ok((
name.as_deref().unwrap_or("main").to_string(),
Module::new(engine, data)?,
))
}
#[allow(unused)]
extism_manifest::ManifestWasm::Url {
name,
url,
header,
method,
hash,
} => {
let file_name = url.split('/').last().unwrap();
let name = match name {
Some(name) => name.as_str(),
None => {
let mut name = "main";
if let Some(n) = file_name.strip_suffix(".wasm") {
name = n;
}
if let Some(n) = file_name.strip_suffix(".wast") {
name = n;
}
name
}
};
let url_hash = hash_url(url);
if let Some(h) = hash {
if let Ok(Some(data)) = cache_get_file(h) {
check_hash(hash, &data)?;
let module = Module::new(engine, data)?;
return Ok((name.to_string(), module));
}
}
#[cfg(not(feature = "register-http"))]
{
return Err(anyhow::format_err!("HTTP registration is disabled"));
}
#[cfg(feature = "register-http")]
{
let url_hash = hash_url(url);
let mut req = ureq::request(method.as_deref().unwrap_or("GET"), url);
for (k, v) in header.iter() {
req = req.set(k, v);
}
// Fetch WASM code
let mut r = req.call()?.into_reader();
let mut data = Vec::new();
r.read_to_end(&mut data)?;
if let Some(hash) = hash {
cache_add_file(hash, &data);
}
check_hash(hash, &data)?;
// Convert fetched data to module
let module = Module::new(engine, data)?;
Ok((name.to_string(), module))
}
}
}
}
const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d];
impl Manifest {
pub fn new(engine: &Engine, data: &[u8]) -> Result<(Self, BTreeMap<String, Module>), Error> {
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
let is_wast = data.starts_with(b"(module") || data.starts_with(b";;");
if !has_magic && !is_wast {
if let Ok(t) = toml::from_slice::<Self>(data) {
let m = t.modules(engine)?;
return Ok((t, m));
}
if let Ok(t) = serde_json::from_slice::<Self>(data) {
let m = t.modules(engine)?;
return Ok((t, m));
}
}
let m = Module::new(engine, data)?;
let mut modules = BTreeMap::new();
modules.insert("main".to_string(), m);
Ok((Manifest::default(), modules))
}
fn modules(&self, engine: &Engine) -> Result<BTreeMap<String, Module>, Error> {
if self.0.wasm.is_empty() {
return Err(anyhow::format_err!("No wasm files specified"));
}
let mut modules = BTreeMap::new();
if self.0.wasm.len() == 1 {
let (_, m) = to_module(engine, &self.0.wasm[0])?;
modules.insert("main".to_string(), m);
return Ok(modules);
}
for f in &self.0.wasm {
let (name, m) = to_module(engine, f)?;
modules.insert(name, m);
}
Ok(modules)
}
}
impl AsRef<extism_manifest::Manifest> for Manifest {
fn as_ref(&self) -> &extism_manifest::Manifest {
&self.0
}
}
impl AsMut<extism_manifest::Manifest> for Manifest {
fn as_mut(&mut self) -> &mut extism_manifest::Manifest {
&mut self.0
}
}

274
runtime/src/memory.rs Normal file
View File

@@ -0,0 +1,274 @@
use std::collections::BTreeMap;
use crate::*;
#[cfg(feature = "debug")]
use pretty_hex::PrettyHex;
/// Handles memory for plugins
pub struct PluginMemory {
pub store: Store<Internal>,
pub memory: Memory,
pub live_blocks: BTreeMap<usize, usize>,
pub free: Vec<MemoryBlock>,
pub position: usize,
}
const PAGE_SIZE: u32 = 65536;
// BLOCK_SIZE_THRESHOLD exists to ensure that free blocks are never split up any
// smaller than this value
const BLOCK_SIZE_THRESHOLD: usize = 32;
impl PluginMemory {
pub fn new(store: Store<Internal>, memory: Memory) -> Self {
PluginMemory {
free: Vec::new(),
live_blocks: BTreeMap::new(),
store,
memory,
position: 1,
}
}
pub(crate) fn store_u8(&mut self, offs: usize, data: u8) -> Result<(), MemoryAccessError> {
if offs >= self.size() {
// This should raise MemoryAccessError
let buf = &mut [0];
self.memory.read(&self.store, offs, buf)?;
return Ok(());
}
self.memory.data_mut(&mut self.store)[offs] = data;
Ok(())
}
/// Read from memory
pub(crate) fn load_u8(&self, offs: usize) -> Result<u8, MemoryAccessError> {
if offs >= self.size() {
// This should raise MemoryAccessError
let buf = &mut [0];
self.memory.read(&self.store, offs, buf)?;
return Ok(0);
}
Ok(self.memory.data(&self.store)[offs])
}
pub(crate) fn store_u32(&mut self, offs: usize, data: u32) -> Result<(), MemoryAccessError> {
let handle = MemoryBlock {
offset: offs,
length: 4,
};
self.write(handle, &data.to_ne_bytes())?;
Ok(())
}
/// Read from memory
pub(crate) fn load_u32(&self, offs: usize) -> Result<u32, MemoryAccessError> {
let mut buf = [0; 4];
let handle = MemoryBlock {
offset: offs,
length: 4,
};
self.read(handle, &mut buf)?;
Ok(u32::from_ne_bytes(buf))
}
pub(crate) fn store_u64(&mut self, offs: usize, data: u64) -> Result<(), MemoryAccessError> {
let handle = MemoryBlock {
offset: offs,
length: 8,
};
self.write(handle, &data.to_ne_bytes())?;
Ok(())
}
pub(crate) fn load_u64(&self, offs: usize) -> Result<u64, MemoryAccessError> {
let mut buf = [0; 8];
let handle = MemoryBlock {
offset: offs,
length: 8,
};
self.read(handle, &mut buf)?;
Ok(u64::from_ne_bytes(buf))
}
/// Write to memory
pub fn write(
&mut self,
pos: impl Into<MemoryBlock>,
data: impl AsRef<[u8]>,
) -> Result<(), MemoryAccessError> {
let pos = pos.into();
assert!(data.as_ref().len() <= pos.length);
self.memory
.write(&mut self.store, pos.offset, data.as_ref())
}
/// Read from memory
pub fn read(
&self,
pos: impl Into<MemoryBlock>,
mut data: impl AsMut<[u8]>,
) -> Result<(), MemoryAccessError> {
let pos = pos.into();
assert!(data.as_mut().len() <= pos.length);
self.memory.read(&self.store, pos.offset, data.as_mut())
}
/// Size of memory in bytes
pub fn size(&self) -> usize {
self.memory.data_size(&self.store)
}
/// Size of memory in pages
pub fn pages(&self) -> u32 {
self.memory.size(&self.store) as u32
}
/// Reserve `n` bytes of memory
pub fn alloc(&mut self, n: usize) -> Result<MemoryBlock, Error> {
for (i, block) in self.free.iter_mut().enumerate() {
if block.length == n {
let block = self.free.swap_remove(i);
self.live_blocks.insert(block.offset, block.length);
return Ok(block);
} else if block.length - n >= BLOCK_SIZE_THRESHOLD {
let handle = MemoryBlock {
offset: block.offset,
length: n,
};
block.offset += n;
block.length -= n;
self.live_blocks.insert(handle.offset, handle.length);
return Ok(handle);
}
}
// If there aren't enough bytes, try to grow the memory size
if self.position + n >= self.size() {
let bytes_needed = (self.position as f64 + n as f64
- self.memory.data_size(&self.store) as f64)
/ PAGE_SIZE as f64;
let mut pages_needed = bytes_needed.ceil() as u64;
if pages_needed == 0 {
pages_needed = 1
}
// This will fail if we've already allocated the maximum amount of memory allowed
self.memory.grow(&mut self.store, pages_needed)?;
}
let mem = MemoryBlock {
offset: self.position,
length: n,
};
self.live_blocks.insert(mem.offset, mem.length);
self.position += n;
Ok(mem)
}
/// Allocate and copy `data` into the wasm memory
pub fn alloc_bytes(&mut self, data: impl AsRef<[u8]>) -> Result<MemoryBlock, Error> {
let handle = self.alloc(data.as_ref().len())?;
self.write(handle, data)?;
Ok(handle)
}
/// Free the block allocated at `offset`
pub fn free(&mut self, offset: usize) {
if let Some(length) = self.live_blocks.remove(&offset) {
self.free.push(MemoryBlock { offset, length });
} else {
return;
}
let free_size: usize = self.free.iter().map(|x| x.length).sum();
// Perform compaction if there is at least 1kb of free memory available
if free_size >= 1024 {
let mut last: Option<MemoryBlock> = None;
let mut free = Vec::new();
for block in self.free.iter() {
match last {
None => {
free.push(*block);
}
Some(last) => {
if last.offset + last.length == block.offset {
free.push(MemoryBlock {
offset: last.offset,
length: last.length + block.length,
});
}
}
}
last = Some(*block);
}
self.free = free;
}
}
#[cfg(feature = "debug")]
pub fn dump(&self) {
let data = self.memory.data(&self.store);
println!("{:?}", data[..self.position].hex_dump());
}
/// Reset memory
pub fn reset(&mut self) {
self.free.clear();
self.live_blocks.clear();
self.position = 1;
}
/// Get memory as a slice of bytes
pub fn data(&self) -> &[u8] {
self.memory.data(&self.store)
}
/// Get memory as a mutable slice of bytes
pub fn data_mut(&mut self) -> &[u8] {
self.memory.data_mut(&mut self.store)
}
/// Get bytes occupied by the provided memory handle
pub fn get(&self, handle: impl Into<MemoryBlock>) -> &[u8] {
let handle = handle.into();
&self.memory.data(&self.store)[handle.offset..handle.offset + handle.length]
}
/// Get mutable bytes occupied by the provided memory handle
pub fn get_mut(&mut self, handle: impl Into<MemoryBlock>) -> &mut [u8] {
let handle = handle.into();
&mut self.memory.data_mut(&mut self.store)[handle.offset..handle.offset + handle.length]
}
/// Get the length of the block starting at `offs`
pub fn block_length(&self, offs: usize) -> Option<usize> {
self.live_blocks.get(&offs).cloned()
}
}
#[derive(Clone, Copy)]
pub struct MemoryBlock {
pub offset: usize,
pub length: usize,
}
impl From<(usize, usize)> for MemoryBlock {
fn from(x: (usize, usize)) -> Self {
MemoryBlock {
offset: x.0,
length: x.1,
}
}
}
impl MemoryBlock {
pub fn new(offset: usize, length: usize) -> Self {
MemoryBlock { offset, length }
}
}

193
runtime/src/plugin.rs Normal file
View File

@@ -0,0 +1,193 @@
use std::collections::BTreeMap;
use crate::*;
/// Plugin contains everything needed to execute a WASM function
pub struct Plugin {
pub module: Module,
pub linker: Linker<Internal>,
pub instance: Instance,
pub last_error: Option<std::ffi::CString>,
pub memory: PluginMemory,
pub manifest: Manifest,
}
pub struct Internal {
pub input_offset: usize,
pub input_length: usize,
pub output_offset: usize,
pub output_length: usize,
pub vars: BTreeMap<String, Vec<u8>>,
pub wasi: wasmtime_wasi::WasiCtx,
pub plugin: *mut Plugin,
}
impl Internal {
fn new(manifest: &Manifest) -> Result<Self, Error> {
let mut wasi = wasmtime_wasi::WasiCtxBuilder::new();
for (k, v) in manifest.as_ref().config.iter() {
wasi = wasi.env(k, v)?;
}
Ok(Internal {
input_offset: 0,
input_length: 0,
output_offset: 0,
output_length: 0,
wasi: wasi.build(),
vars: BTreeMap::new(),
plugin: std::ptr::null_mut(),
})
}
}
const EXPORT_MODULE_NAME: &str = "env";
impl Plugin {
/// Create a new plugin from the given WASM code
pub fn new(wasm: impl AsRef<[u8]>, with_wasi: bool) -> Result<Plugin, Error> {
let engine = Engine::default();
let (manifest, modules) = Manifest::new(&engine, wasm.as_ref())?;
let mut store = Store::new(&engine, Internal::new(&manifest)?);
let memory = Memory::new(&mut store, MemoryType::new(4, manifest.as_ref().memory.max))?;
let mut memory = PluginMemory::new(store, memory);
let mut linker = Linker::new(&engine);
linker.allow_shadowing(true);
if with_wasi {
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut Internal| &mut x.wasi)?;
}
// Get the `main` module, or the last one if `main` doesn't exist
let (main_name, main) = modules.get("main").map(|x| ("main", x)).unwrap_or_else(|| {
let entry = modules.iter().last().unwrap();
(entry.0.as_str(), entry.1)
});
// Collect exports
let mut exports = BTreeMap::new();
for (_name, module) in modules.iter() {
for export in module.exports() {
exports.insert(export.name(), export);
}
}
macro_rules! define_funcs {
($m:expr, { $($name:ident($($args:expr),*) $(-> $($r:expr),*)?);* $(;)?}) => {
match $m {
$(
concat!("extism_", stringify!($name)) => {
let t = FuncType::new([$($args),*], [$($($r),*)?]);
let f = Func::new(&mut memory.store, t, export::$name);
linker.define(EXPORT_MODULE_NAME, concat!("extism_", stringify!($name)), Extern::Func(f))?;
continue
}
)*
_ => ()
}
};
}
// Add builtins
for (_name, module) in modules.iter() {
for import in module.imports() {
let m = import.module();
let n = import.name();
use ValType::*;
if m == EXPORT_MODULE_NAME {
define_funcs!(n, {
alloc(I64) -> I64;
free(I64);
load_u8(I64) -> I32;
load_u32(I64) -> I32;
load_u64(I64) -> I64;
store_u8(I64, I32);
store_u32(I64, I32);
store_u64(I64, I64);
input_offset() -> I64;
output_set(I64, I64);
error_set(I64);
config_get(I64) -> I64;
var_get(I64) -> I64;
var_set(I64, I64);
http_request(I64) -> I64;
length(I64) -> I64;
});
}
// Define memory or check to ensure the symbol is exported by another module
// since it doesn't match one of our known exports
match (m, n) {
("env", "memory") => {
linker.define(m, n, Extern::Memory(memory.memory))?;
}
(module_name, name) => {
if !module_name.starts_with("wasi") && !exports.contains_key(name) {
panic!("Invalid export: {m}::{n}")
}
}
}
}
}
// Add modules to linker
for (name, module) in modules.iter() {
if name != main_name {
linker.module(&mut memory.store, name, module)?;
linker.alias_module(name, "env")?;
}
}
let instance = linker.instantiate(&mut memory.store, main)?;
Ok(Plugin {
module: main.clone(),
linker,
memory,
instance,
last_error: None,
manifest,
})
}
/// Get a function by name
pub fn get_func(&mut self, function: impl AsRef<str>) -> Option<Func> {
self.instance
.get_func(&mut self.memory.store, function.as_ref())
}
/// Set `last_error` field
pub fn set_error(&mut self, e: impl std::fmt::Debug) {
let x = format!("{:?}", e).into_bytes();
let e = unsafe { std::ffi::CString::from_vec_unchecked(x) };
self.last_error = Some(e);
}
pub fn error<E>(&mut self, e: impl std::fmt::Debug, x: E) -> E {
self.set_error(e);
x
}
/// Unset `last_error` field
pub fn clear_error(&mut self) {
self.last_error = None;
}
/// Store input in memory and initialize `Internal` pointer
pub fn set_input(&mut self, handle: MemoryBlock) {
let ptr = self as *mut _;
let internal = self.memory.store.data_mut();
internal.input_offset = handle.offset;
internal.input_length = handle.length;
internal.plugin = ptr;
}
#[cfg(feature = "debug")]
pub fn dump_memory(&self) {
self.memory.dump();
}
}
/// A registry for plugins
pub static mut PLUGINS: std::sync::Mutex<Vec<Plugin>> = std::sync::Mutex::new(Vec::new());

51
runtime/src/plugin_ref.rs Normal file
View File

@@ -0,0 +1,51 @@
use crate::*;
// PluginRef is used to access a plugin from the global plugin registry
pub struct PluginRef<'a> {
pub plugins: std::sync::MutexGuard<'a, Vec<Plugin>>,
plugin: *mut Plugin,
}
impl<'a> PluginRef<'a> {
pub fn init(mut self) -> Self {
// Initialize
self.as_mut().clear_error();
self.as_mut().memory.reset();
self
}
/// # Safety
///
/// This function is used to access the static `PLUGINS` registry
pub unsafe fn new(plugin: PluginIndex) -> Self {
let mut plugins = PLUGINS
.lock()
.expect("Unable to acquire lock on plugin registry");
if plugin < 0 || plugin as usize >= plugins.len() {
panic!("Invalid PluginIndex {plugin}")
}
let plugin = plugins.get_unchecked_mut(plugin as usize) as *mut _;
PluginRef { plugins, plugin }
}
}
impl<'a> AsRef<Plugin> for PluginRef<'a> {
fn as_ref(&self) -> &Plugin {
unsafe { &*self.plugin }
}
}
impl<'a> AsMut<Plugin> for PluginRef<'a> {
fn as_mut(&mut self) -> &mut Plugin {
unsafe { &mut *self.plugin }
}
}
impl<'a> Drop for PluginRef<'a> {
fn drop(&mut self) {
// Cleanup?
}
}

150
runtime/src/sdk.rs Normal file
View File

@@ -0,0 +1,150 @@
#![allow(clippy::missing_safety_doc)]
use std::os::raw::c_char;
use crate::*;
#[no_mangle]
pub unsafe extern "C" fn extism_plugin_register(
wasm: *const u8,
wasm_size: Size,
with_wasi: bool,
) -> PluginIndex {
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
let plugin = match Plugin::new(data, with_wasi) {
Ok(x) => x,
Err(e) => {
eprintln!("Error creating Plugin: {:?}", e);
return -1;
}
};
// Acquire lock and add plugin to registry
if let Ok(mut plugins) = PLUGINS.lock() {
plugins.push(plugin);
return (plugins.len() - 1) as PluginIndex;
}
-1
}
#[no_mangle]
pub unsafe extern "C" fn extism_plugin_config(
plugin: PluginIndex,
json: *const u8,
json_size: Size,
) -> bool {
let mut plugin = PluginRef::new(plugin);
let data = std::slice::from_raw_parts(json, json_size as usize);
let json: std::collections::BTreeMap<String, String> = match serde_json::from_slice(data) {
Ok(x) => x,
Err(e) => {
plugin.as_mut().set_error(e);
return false;
}
};
let plugin = plugin.as_mut();
let wasi = &mut plugin.memory.store.data_mut().wasi;
let config = &mut plugin.manifest.as_mut().config;
for (k, v) in json.into_iter() {
let _ = wasi.push_env(&k, &v);
config.insert(k, v);
}
true
}
#[no_mangle]
pub unsafe extern "C" fn extism_function_exists(
plugin: PluginIndex,
func_name: *const c_char,
) -> bool {
let mut plugin = PluginRef::new(plugin);
let name = std::ffi::CStr::from_ptr(func_name);
let name = name.to_str().expect("Invalid function name");
plugin.as_mut().get_func(name).is_some()
}
#[no_mangle]
pub unsafe extern "C" fn extism_call(
plugin: PluginIndex,
func_name: *const c_char,
data: *const u8,
data_len: Size,
) -> i32 {
let mut plugin = PluginRef::new(plugin).init();
let plugin = plugin.as_mut();
// Find function
let name = std::ffi::CStr::from_ptr(func_name);
let name = name.to_str().expect("Invalid function name");
let func = plugin
.get_func(name)
.unwrap_or_else(|| panic!("Function not found {name}"));
// Write input to memory
let data = std::slice::from_raw_parts(data, data_len as usize);
let handle = match plugin.memory.alloc_bytes(data) {
Ok(x) => x,
Err(e) => return plugin.error(e.context("Unable to allocate bytes"), -1),
};
#[cfg(feature = "debug")]
plugin.dump_memory();
// Always needs to be called before `func.call()`
plugin.set_input(handle);
// Call function with offset+length pointing to input data.
// TODO: In the future this could be a JSON or Protobuf payload.
let mut results = vec![Val::I32(0)];
match func.call(&mut plugin.memory.store, &[], results.as_mut_slice()) {
Ok(r) => r,
Err(e) => {
#[cfg(feature = "debug")]
plugin.dump_memory();
return plugin.error(e.context("Invalid write"), -1);
}
};
#[cfg(feature = "debug")]
plugin.dump_memory();
// Return result to caller
results[0].unwrap_i32()
}
#[no_mangle]
pub unsafe extern "C" fn extism_error(plugin: PluginIndex) -> *const c_char {
let plugin = PluginRef::new(plugin);
match &plugin.as_ref().last_error {
Some(e) => e.as_ptr() as *const _,
None => std::ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn extism_output_length(plugin: PluginIndex) -> Size {
let plugin = PluginRef::new(plugin);
plugin.as_ref().memory.store.data().output_length as Size
}
#[no_mangle]
pub unsafe extern "C" fn extism_output_get(plugin: PluginIndex, buf: *mut u8, len: Size) {
let plugin = PluginRef::new(plugin);
let data = plugin.as_ref().memory.store.data();
let slice = std::slice::from_raw_parts_mut(buf, len as usize);
plugin
.as_ref()
.memory
.read(
MemoryBlock::new(data.output_offset, data.output_length),
slice,
)
.expect("Out of bounds read in extism_output_get");
}

14
rust/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "extism"
version = "0.0.1-alpha"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"
links = "extism"
homepage = "https://extism.org"
repository = "https://github.com/extism/extism"
description = "Extism Host SDK for Rust"
[dependencies]
extism-manifest = { version = "0.0.1-alpha", path = "../manifest" }
serde_json = "1"

2
rust/Makefile Normal file
View File

@@ -0,0 +1,2 @@
bindings:
bindgen ../core/extism.h --allowlist-function extism.* > src/bindings.rs

20
rust/build.rs Normal file
View File

@@ -0,0 +1,20 @@
fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();
if std::path::PathBuf::from("libextism.so").exists() {
std::process::Command::new("cp")
.arg("libextism.so")
.arg(&out_dir)
.status()
.unwrap();
} else {
std::process::Command::new("cp")
.arg("libextism.dylib")
.arg(&out_dir)
.status()
.unwrap();
}
println!("cargo:rustc-link-search={}", out_dir);
println!("cargo:rustc-link-lib=extism");
println!("cargo:rerun-if-changed=libextism.so");
println!("cargo:rerun-if-changed=libextism.dylib");
}

44
rust/src/bindings.rs Normal file
View File

@@ -0,0 +1,44 @@
/* automatically generated by rust-bindgen 0.60.1 */
pub type __uint8_t = ::std::os::raw::c_uchar;
pub type __int32_t = ::std::os::raw::c_int;
pub type __uint64_t = ::std::os::raw::c_ulong;
pub type ExtismPlugin = i32;
pub type ExtismSize = u64;
extern "C" {
pub fn extism_plugin_register(
wasm: *const u8,
wasm_size: ExtismSize,
with_wasi: bool,
) -> ExtismPlugin;
}
extern "C" {
pub fn extism_plugin_config(
plugin: ExtismPlugin,
json: *const u8,
json_size: ExtismSize,
) -> bool;
}
extern "C" {
pub fn extism_function_exists(
plugin: ExtismPlugin,
func_name: *const ::std::os::raw::c_char,
) -> bool;
}
extern "C" {
pub fn extism_call(
plugin: ExtismPlugin,
func_name: *const ::std::os::raw::c_char,
data: *const u8,
data_len: ExtismSize,
) -> i32;
}
extern "C" {
pub fn extism_error(plugin: ExtismPlugin) -> *const ::std::os::raw::c_char;
}
extern "C" {
pub fn extism_output_length(plugin: ExtismPlugin) -> ExtismSize;
}
extern "C" {
pub fn extism_output_get(plugin: ExtismPlugin, buf: *mut u8, len: ExtismSize);
}

189
rust/src/lib.rs Normal file
View File

@@ -0,0 +1,189 @@
use std::collections::BTreeMap;
use extism_manifest::Manifest;
#[allow(non_camel_case_types)]
mod bindings;
#[repr(transparent)]
pub struct Plugin(isize);
#[derive(Debug)]
pub enum Error {
UnableToLoadPlugin,
Message(String),
Json(serde_json::Error),
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Json(e)
}
}
impl Plugin {
pub fn new_with_manifest(manifest: &Manifest, wasi: bool) -> Result<Plugin, Error> {
let data = serde_json::to_vec(&manifest)?;
Self::new(data, wasi)
}
pub fn new(data: impl AsRef<[u8]>, wasi: bool) -> Result<Plugin, Error> {
let plugin = unsafe {
bindings::extism_plugin_register(
data.as_ref().as_ptr(),
data.as_ref().len() as u64,
wasi,
)
};
if plugin < 0 {
return Err(Error::UnableToLoadPlugin);
}
Ok(Plugin(plugin as isize))
}
pub fn set_config(&self, config: &BTreeMap<String, String>) -> Result<(), Error> {
let encoded = serde_json::to_vec(config)?;
unsafe {
bindings::extism_plugin_config(
self.0 as i32,
encoded.as_ptr() as *const _,
encoded.len() as u64,
)
};
Ok(())
}
pub fn has_function(&self, name: impl AsRef<str>) -> bool {
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
unsafe { bindings::extism_function_exists(self.0 as i32, name.as_ptr() as *const _) }
}
pub fn call(&self, name: impl AsRef<str>, input: impl AsRef<[u8]>) -> Result<Vec<u8>, Error> {
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
let rc = unsafe {
bindings::extism_call(
self.0 as i32,
name.as_ptr() as *const _,
input.as_ref().as_ptr() as *const _,
input.as_ref().len() as u64,
)
};
if rc != 0 {
let err = unsafe { bindings::extism_error(self.0 as i32) };
if !err.is_null() {
let s = unsafe { std::ffi::CStr::from_ptr(err) };
return Err(Error::Message(s.to_str().unwrap().to_string()));
}
return Err(Error::Message("extism_call failed".to_string()));
}
let out_len = unsafe { bindings::extism_output_length(self.0 as i32) };
let mut out_buf = vec![0; out_len as usize];
unsafe {
bindings::extism_output_get(self.0 as i32, out_buf.as_mut_ptr() as *mut _, out_len)
}
Ok(out_buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
#[test]
fn it_works() {
let wasm = include_bytes!("../../wasm/code.wasm");
let wasm_start = Instant::now();
let plugin = Plugin::new(wasm, false).unwrap();
println!("register loaded plugin: {:?}", wasm_start.elapsed());
let repeat = 1182;
let input = "aeiouAEIOU____________________________________&smtms_y?".repeat(repeat);
let data = plugin.call("count_vowels", &input).unwrap();
assert_eq!(
data,
b"{\"count\": 11820}",
"expecting vowel count of {}, input size: {}, output size: {}",
10 * repeat,
input.len(),
data.len()
);
println!(
"register plugin + function call: {:?}, sent input size: {} bytes",
wasm_start.elapsed(),
input.len()
);
println!("--------------");
let test_times = (0..100)
.map(|_| {
let test_start = Instant::now();
plugin.call("count_vowels", &input).unwrap();
test_start.elapsed()
})
.collect::<Vec<_>>();
let native_test = || {
let native_start = Instant::now();
// let native_vowel_count = input
// .chars()
// .filter(|c| match c {
// 'A' | 'E' | 'I' | 'O' | 'U' | 'a' | 'e' | 'i' | 'o' | 'u' => true,
// _ => false,
// })
// .collect::<Vec<_>>()
// .len();
let mut _native_vowel_count = 0;
let input: &[u8] = input.as_ref();
for i in 0..input.len() {
if input[i] == b'A'
|| input[i] == b'E'
|| input[i] == b'I'
|| input[i] == b'O'
|| input[i] == b'U'
|| input[i] == b'a'
|| input[i] == b'e'
|| input[i] == b'i'
|| input[i] == b'o'
|| input[i] == b'u'
{
_native_vowel_count += 1;
}
}
native_start.elapsed()
};
let native_test_times = (0..100).map(|_| native_test());
let native_num_tests = native_test_times.len();
let native_sum: std::time::Duration = native_test_times
.into_iter()
.reduce(|accum: std::time::Duration, elapsed| accum + elapsed)
.unwrap();
let native_avg: std::time::Duration = native_sum / native_num_tests as u32;
println!(
"native function call (avg, N = {}): {:?}",
native_num_tests, native_avg
);
let num_tests = test_times.len();
let sum: std::time::Duration = test_times
.into_iter()
.reduce(|accum: std::time::Duration, elapsed| accum + elapsed)
.unwrap();
let avg: std::time::Duration = sum / num_tests as u32;
println!("wasm function call (avg, N = {}): {:?}", num_tests, avg);
}
}

8
wasm/Makefile Normal file
View File

@@ -0,0 +1,8 @@
SRC=count_vowels.c
PROJECT=code
rust:
cd rust-pdk && $(MAKE)
c:
emcc -o code.wasm count_vowels.c --no-entry -Wl,--export-all -sERROR_ON_UNDEFINED_SYMBOLS=0

84
wasm/c-pdk/extism-pdk.h Normal file
View File

@@ -0,0 +1,84 @@
#pragma once
#include <stdint.h>
typedef unsigned long size_t;
#define IMPORT(a, b) __attribute__((import_module(a), import_name(b)))
IMPORT("env", "extism_input_offset") extern uint64_t extism_input_offset();
IMPORT("env", "extism_length") extern uint64_t extism_length(uint64_t);
IMPORT("env", "extism_alloc") extern uint64_t extism_alloc(uint64_t);
IMPORT("env", "extism_free") extern void extism_free(uint64_t);
IMPORT("env", "extism_output_set")
extern void extism_output_set(uint64_t, uint64_t);
IMPORT("env", "extism_error_set")
extern void extism_error_set(uint64_t);
IMPORT("env", "extism_config_get")
extern uint64_t extism_config_get(uint64_t);
IMPORT("env", "extism_kv_get")
extern uint64_t extism_kv_get(uint64_t);
IMPORT("env", "extism_kv_set")
extern void extism_kv_set(uint64_t, uint64_t);
IMPORT("env", "extism_store_u8")
extern void extism_store_u8(uint64_t, uint8_t);
IMPORT("env", "extism_load_u8")
extern uint8_t extism_load_u8(uint64_t);
IMPORT("env", "extism_store_u32")
extern void extism_store_u32(uint64_t, uint32_t);
IMPORT("env", "extism_load_u32")
extern uint32_t extism_load_u32(uint64_t);
IMPORT("env", "extism_store_u64")
extern void extism_store_u64(uint64_t, uint64_t);
IMPORT("env", "extism_load_u64")
extern uint64_t extism_load_u64(uint64_t);
IMPORT("env", "extism_file_read")
extern uint64_t extism_file_read(int32_t);
IMPORT("env", "extism_file_write")
extern void extism_file_write(int32_t, uint64_t);
static void extism_load(uint64_t offs, uint8_t *buffer, size_t length) {
uint64_t n;
size_t left = 0;
for (size_t i = 0; i < length; i += 1) {
left = length - i;
if (left < 8) {
buffer[i] = extism_load_u8(offs + i);
continue;
}
n = extism_load_u64(offs + i);
*((uint64_t *)buffer + (i / 8)) = n;
i += 7;
}
}
static void extism_store(uint64_t offs, const uint8_t *buffer, size_t length) {
uint64_t n;
size_t left = 0;
for (size_t i = 0; i < length; i++) {
left = length - i;
if (left < 8) {
extism_store_u8(offs + i, buffer[i]);
continue;
}
n = *((uint64_t *)buffer + (i / 8));
extism_store_u64(offs + i, n);
i += 7;
}
}

BIN
wasm/code.wasm Executable file

Binary file not shown.

34
wasm/count_vowels.c Normal file
View File

@@ -0,0 +1,34 @@
#include "c-pdk/extism-pdk.h"
#include "printf.h"
int32_t count_vowels() {
uint64_t offs = extism_input_offset();
uint64_t length = extism_length(offs);
if (offs == 0) {
return 0;
}
char input[length];
extism_load(offs, (uint8_t *)input, length);
int64_t count = 0;
for (int64_t i = 0; i < length; i++) {
if (input[i] == 'a' || input[i] == 'e' || input[i] == 'i' ||
input[i] == 'o' || input[i] == 'u' || input[i] == 'A' ||
input[i] == 'E' || input[i] == 'I' || input[i] == 'O' ||
input[i] == 'U') {
count += 1;
}
}
char out[128];
int n = snprintf(out, 128, "{\"count\": %d}", count);
uint64_t offs_ = extism_alloc(n);
extism_store(offs_, (const uint8_t *)out, n);
extism_output_set(offs_, n);
return 0;
}

View File

@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

13
wasm/rust-pdk/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "extism-pdk"
version = "0.0.1-alpha"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"
homepage = "https://extism.org"
repository = "https://github.com/extism/extism"
description = "Extism Plug-in Development Kit (PDK) for Rust"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

3
wasm/rust-pdk/Makefile Normal file
View File

@@ -0,0 +1,3 @@
count_vowels:
cargo build --release --example count_vowels
cp target/wasm32-unknown-unknown/release/examples/count_vowels.wasm ../code.wasm

View File

@@ -0,0 +1,21 @@
#![no_main]
use extism_pdk::*;
const VOWELS: &[char] = &['a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U'];
#[no_mangle]
unsafe fn count_vowels() -> i32 {
let host = Host::new();
let s = host.input_str();
let mut count = 0;
for ch in s.chars() {
if VOWELS.contains(&ch) {
count += 1;
}
}
host.output(&format!(r#"{{"count": {count}}}"#));
0
}

View File

@@ -0,0 +1,67 @@
extern "C" {
pub fn extism_input_offset() -> u64;
pub fn extism_length(offs: u64) -> u64;
pub fn extism_alloc(length: u64) -> u64;
pub fn extism_free(offs: u64);
pub fn extism_output_set(offs: u64, length: u64);
pub fn extism_error_set(offs: u64);
pub fn extism_store_u8(offs: u64, data: u8);
pub fn extism_load_u8(offs: u64) -> u8;
pub fn extism_store_u32(offs: u64, data: u32);
pub fn extism_load_u32(offs: u64) -> u32;
pub fn extism_store_u64(offs: u64, data: u64);
pub fn extism_load_u64(offs: u64) -> u64;
pub fn extism_file_read(fd: i32) -> u64;
pub fn extism_file_write(fd: i32, offs: u64);
pub fn extism_config_get(offs: u64) -> u64;
pub fn extism_kv_get(offs: u64) -> u64;
pub fn extism_kv_set(offs: u64, offs1: u64);
}
/// # Safety
///
/// This function is used to access WASM memory
pub unsafe fn extism_load(offs: u64, data: &mut [u8]) {
let ptr = data.as_mut_ptr();
let mut index = 0;
let mut left;
let len = data.len();
while index < len {
left = len - index;
if left < 8 {
data[index] = extism_load_u8(offs + index as u64);
index += 1;
continue;
}
let x = extism_load_u64(offs + index as u64);
(ptr as *mut u64).add(index / 8).write(x);
index += 8;
}
}
/// # Safety
///
/// This function is used to access WASM memory
pub unsafe fn extism_store(offs: u64, data: &[u8]) {
let ptr = data.as_ptr();
let mut index = 0;
let mut left;
let len = data.len();
while index < len {
left = len - index;
if left < 8 {
extism_store_u8(offs + index as u64, data[index]);
index += 1;
continue;
}
extism_store_u64(
offs + index as u64,
(ptr as *const u64).add(index / 8).read(),
);
index += 8;
}
}

135
wasm/rust-pdk/src/lib.rs Normal file
View File

@@ -0,0 +1,135 @@
pub mod bindings;
use bindings::*;
pub struct Host {
input: Vec<u8>,
}
impl Default for Host {
fn default() -> Self {
Host::new()
}
}
pub struct Vars<'a>(&'a Host);
pub struct Memory {
pub offset: u64,
pub length: u64,
}
impl Memory {
pub fn load(&self, mut buf: impl AsMut<[u8]>) {
let buf = buf.as_mut();
unsafe { extism_load(self.offset, &mut buf[0..self.length as usize]) }
}
pub fn store(&mut self, buf: impl AsRef<[u8]>) {
let buf = buf.as_ref();
unsafe { extism_store(self.offset, &buf[0..self.length as usize]) }
}
}
impl Drop for Memory {
fn drop(&mut self) {
unsafe { extism_free(self.offset) }
}
}
impl<'a> Vars<'a> {
pub fn new(host: &'a Host) -> Self {
Vars(host)
}
pub fn get(&self, key: impl AsRef<str>) -> Option<Vec<u8>> {
let mem = self.0.alloc_bytes(key.as_ref().as_bytes());
let offset = unsafe { extism_kv_get(mem.offset) };
let len = unsafe { extism_length(offset) };
if offset == 0 || len == 0 {
return None;
}
let mut buf = vec![0; len as usize];
unsafe {
extism_load(offset, &mut buf);
}
Some(buf)
}
pub fn set(&mut self, key: impl AsRef<str>, val: impl AsRef<[u8]>) {
let key = self.0.alloc_bytes(key.as_ref().as_bytes());
let val = self.0.alloc_bytes(val.as_ref());
unsafe { extism_kv_set(key.offset, val.offset) }
}
pub fn remove(&mut self, key: impl AsRef<str>) {
let key = self.0.alloc_bytes(key.as_ref().as_bytes());
unsafe { extism_kv_set(key.offset, 0) }
}
}
impl Host {
pub fn new() -> Host {
unsafe {
let input_offset = extism_input_offset();
let input_length = extism_length(input_offset);
let mut input = vec![0; input_length as usize];
extism_load(input_offset, &mut input);
Host { input }
}
}
pub fn alloc(&self, length: usize) -> Memory {
let length = length as u64;
let offset = unsafe { extism_alloc(length) };
Memory { offset, length }
}
pub fn alloc_bytes(&self, data: impl AsRef<[u8]>) -> Memory {
let data = data.as_ref();
let length = data.len() as u64;
let offset = unsafe { extism_alloc(length) };
Memory { offset, length }
}
pub fn input(&self) -> &[u8] {
self.input.as_slice()
}
pub fn input_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.input.as_slice()) }
}
pub fn output(&self, data: impl AsRef<[u8]>) {
let len = data.as_ref().len();
unsafe {
let offs = extism_alloc(len as u64);
extism_store(offs, data.as_ref());
extism_output_set(offs, len as u64);
}
}
pub fn config(&self, key: impl AsRef<str>) -> String {
let mem = self.alloc_bytes(key.as_ref().as_bytes());
let offset = unsafe { extism_config_get(mem.offset) };
let len = unsafe { extism_length(offset) };
if offset == 0 || len == 0 {
return String::new();
}
let mut buf = vec![0; len as usize];
unsafe {
extism_load(offset, &mut buf);
String::from_utf8_unchecked(buf)
}
}
pub fn vars(&self) -> Vars {
Vars::new(self)
}
}