mirror of
https://github.com/extism/extism.git
synced 2026-01-08 21:38:13 -05:00
105
.github/workflows/ci.yml
vendored
Normal file
105
.github/workflows/ci.yml
vendored
Normal 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
217
.github/workflows/release.yml
vendored
Normal 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
28
.gitignore
vendored
Normal 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
11
LICENSE
Normal 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
40
Makefile
Normal 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
51
README.md
Normal 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) & 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
2
c/Makefile
Normal file
@@ -0,0 +1,2 @@
|
||||
build:
|
||||
clang -o main main.c -lextism -L .
|
||||
56
c/main.c
Normal file
56
c/main.c
Normal 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
3
cpp/Makefile
Normal file
@@ -0,0 +1,3 @@
|
||||
build:
|
||||
clang++ -std=c++11 -o example example.cpp -lextism -L .
|
||||
|
||||
30
cpp/example.cpp
Normal file
30
cpp/example.cpp
Normal 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
1
cpp/extism.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../core/extism.h
|
||||
56
cpp/extism.hpp
Normal file
56
cpp/extism.hpp
Normal 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
104
extism.go
Normal 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
|
||||
}
|
||||
7
go/go.mod
Normal file
7
go/go.mod
Normal 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
41
go/main.go
Normal 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
10
libextism.pc
Normal 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
13
manifest/Cargo.toml
Normal 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
55
manifest/src/lib.rs
Normal 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
7
node/example.js
Normal 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
46
node/index.js
Normal 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
193
node/package-lock.json
generated
Normal 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
16
node/package.json
Normal 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
BIN
node/user_code.wasm
Executable file
Binary file not shown.
1
ocaml/.ocamlformat
Normal file
1
ocaml/.ocamlformat
Normal file
@@ -0,0 +1 @@
|
||||
version = 0.24.1
|
||||
4
ocaml/bin/dune
Normal file
4
ocaml/bin/dune
Normal file
@@ -0,0 +1,4 @@
|
||||
(executable
|
||||
(public_name extism)
|
||||
(name main)
|
||||
(libraries extism))
|
||||
10
ocaml/bin/main.ml
Normal file
10
ocaml/bin/main.ml
Normal 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
26
ocaml/dune-project
Normal 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
34
ocaml/extism.opam
Normal 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
233
ocaml/extism.opam.locked
Normal 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
4
ocaml/lib/dune
Normal file
@@ -0,0 +1,4 @@
|
||||
(library
|
||||
(name extism)
|
||||
(libraries ctypes.foreign bigstringaf)
|
||||
(preprocess (pps ppx_yojson_conv)))
|
||||
161
ocaml/lib/extism.ml
Normal file
161
ocaml/lib/extism.ml
Normal 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
48
ocaml/lib/extism.mli
Normal 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
34
python/example.py
Normal 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))
|
||||
1
python/extism/__init__.py
Normal file
1
python/extism/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .extism import Error, Plugin
|
||||
120
python/extism/extism.py
Normal file
120
python/extism/extism.py
Normal 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
16
python/pyproject.toml
Normal 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
10
ruby/Gemfile
Normal 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
3
ruby/Rakefile
Normal file
@@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/gem_tasks"
|
||||
15
ruby/bin/console
Executable file
15
ruby/bin/console
Executable 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
8
ruby/bin/setup
Executable 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
10
ruby/example.rb
Normal 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
39
ruby/extism.gemspec
Normal 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
43
ruby/lib/extism.rb
Normal 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
|
||||
5
ruby/lib/extism/version.rb
Normal file
5
ruby/lib/extism/version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Extism
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
4
ruby/sig/extism.rbs
Normal file
4
ruby/sig/extism.rbs
Normal 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
BIN
ruby/user_code.wasm
Executable file
Binary file not shown.
37
runtime/Cargo.toml
Normal file
37
runtime/Cargo.toml
Normal 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
16
runtime/build.rs
Normal 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
27
runtime/extism.h
Normal 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
332
runtime/src/export.rs
Normal 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
17
runtime/src/lib.rs
Normal 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
221
runtime/src/manifest.rs
Normal 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
274
runtime/src/memory.rs
Normal 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
193
runtime/src/plugin.rs
Normal 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
51
runtime/src/plugin_ref.rs
Normal 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
150
runtime/src/sdk.rs
Normal 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
14
rust/Cargo.toml
Normal 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
2
rust/Makefile
Normal file
@@ -0,0 +1,2 @@
|
||||
bindings:
|
||||
bindgen ../core/extism.h --allowlist-function extism.* > src/bindings.rs
|
||||
20
rust/build.rs
Normal file
20
rust/build.rs
Normal 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
44
rust/src/bindings.rs
Normal 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
189
rust/src/lib.rs
Normal 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
8
wasm/Makefile
Normal 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
84
wasm/c-pdk/extism-pdk.h
Normal 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
BIN
wasm/code.wasm
Executable file
Binary file not shown.
34
wasm/count_vowels.c
Normal file
34
wasm/count_vowels.c
Normal 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;
|
||||
}
|
||||
2
wasm/rust-pdk/.cargo/config
Normal file
2
wasm/rust-pdk/.cargo/config
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
13
wasm/rust-pdk/Cargo.toml
Normal file
13
wasm/rust-pdk/Cargo.toml
Normal 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
3
wasm/rust-pdk/Makefile
Normal 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
|
||||
21
wasm/rust-pdk/examples/count_vowels.rs
Normal file
21
wasm/rust-pdk/examples/count_vowels.rs
Normal 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
|
||||
}
|
||||
67
wasm/rust-pdk/src/bindings.rs
Normal file
67
wasm/rust-pdk/src/bindings.rs
Normal 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
135
wasm/rust-pdk/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user