feat(dlang): Add a D host SDK (#412)

See [dlang.org](https://dlang.org).

SDK Coverage: ~91.30%	(21/23)

## Issues 

- [x] These symbols are undefined in the latest release
([v0.5.2](https://github.com/extism/extism/releases/tag/v0.5.2)):
    > Undefined symbols for architecture x86_64:
    > "_extism_plugin_id"
    > "_extism_plugin_new_error_free"
    
    Using `libextism` compiled from source does _not_ have this issue.
- [ ] `dub lint` reads many intermediate targets files:
https://github.com/dlang/dub/issues/2700

## Todo List

- [x] Flesh out initial binding
- [ ] Cover host API:
  - [x] `ExtismFunctionType`
    See `FunctionType` alias.
  - [x] `extism_plugin_id`
  - [x] `extism_plugin_new_error_free`
  - [ ] `extism_plugin_error`
- [ ] Add unit tests
- [x] Add usage example

---------

Co-authored-by: zach <zachshipko@gmail.com>
This commit is contained in:
Chance Snow
2023-10-23 16:00:17 -05:00
committed by GitHub
parent 30322dba23
commit abb31ee7de
12 changed files with 551 additions and 0 deletions

36
.github/workflows/ci-d.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
on:
pull_request:
paths:
- .github/actions/extism/**
- .github/workflows/ci-d.yml
- manifest/**
- runtime/**
- libextism/**
- d/**
workflow_dispatch:
name: D CI
jobs:
d:
name: D
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
# TODO: Use multiple versions once stable
d_version: [ldc-1.33.0]
rust:
- stable
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup D environment
uses: dlang-community/setup-dlang@v1
with:
compiler: ${{ matrix.d_version }}
- name: Test D Host SDK
run: |
dub --version
LD_LIBRARY_PATH=/usr/local/lib dub test

2
.gitignore vendored
View File

@@ -15,6 +15,8 @@ python/poetry.lock
c/main
cpp/test/test
cpp/example
.dub
dub.selections.json
go/main
ruby/.bundle/
ruby/.yardoc

7
d/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by host system's C preprocessor
# See https://dlang.org/spec/importc.html#manual-cpp
runtime.i
docs.json
extism-test-*
*.lst

42
d/README.md Normal file
View File

@@ -0,0 +1,42 @@
# Extism D SDK
D bindings to the Extism host SDK.
## Development
1. Ensure a [D compiler](https://dlang.org/download) is installed and available in your `PATH`.
> Note: The compiler's frontend must be at least version `2.102`.
2. Ensure the extism shared library is installed *before* running tests or examples:
See [Installing Extism](https://extism.org/docs/install) documentation.
```sh
extism install latest
extism link
```
> Note: The `extism install` command may require `sudo`.
### Lint Sources
```sh
dub lint
```
> Note: Until [dlang/dub#2700](https://github.com/dlang/dub/issues/2700) is fixed, ensure all non-D sources (e.g. intermediate `*.d` files in `targets`) are cleaned from the repo.
>
> Run `cargo clean` to avoid seeing many invalid lint issues.
### Running Tests
```sh
dub test
```
## Examples
### Hello World
```sh
dub run extism:hello
```

17
d/examples/hello/.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
.dub
docs.json
__dummy.html
docs/
/extism_hello
/hello
hello.so
hello.dylib
hello.dll
hello.a
hello.lib
hello-test-*
*.exe
*.pdb
*.o
*.obj
*.lst

15
d/examples/hello/dub.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "hello",
"description": "A minimal Extism usage example",
"license": "BSD-3-Clause",
"authors": [
"Chance Snow <git@chancesnow.me>",
"Extism contributors"
],
"copyright": "Copyright © 2023, Extism contributors",
"dependencies": {
"extism": {
"path": "../../../"
}
}
}

View File

@@ -0,0 +1,33 @@
import std.conv: castFrom, to;
import std.file;
import std.functional: toDelegate;
import std.stdio;
import std.string: representation;
import std.typecons : Yes;
import extism;
void main() {
auto wasm = cast(ubyte[]) read("wasm/code-functions.wasm");
// FIXME: Creating the plugin results in EXC_BAD_ACCESS (segfault?) from `extism_plugin_new`
auto plugin = new Plugin(wasm, [
Function!string(
"hello_world", /* Inputs */ [ValType.i64], /* Outputs */ [ValType.i64], toDelegate(&helloWorld), "Hello, again!"
),
], Yes.withWasi);
auto input = "aeiou";
plugin.call("count_vowels", cast(ubyte[]) input.representation);
writeln(plugin.outputData);
}
///
void helloWorld(CurrentPlugin plugin, const Val[] inputs, Val[] outputs, void *data) {
writeln("Hello from D!");
writeln(data);
ExtismSize ptr_offs = inputs[0].v.i64;
auto buf = plugin.memory(ptr_offs);
writeln(buf);
outputs[0].v.i64 = inputs[0].v.i64;
}

258
d/extism.d Normal file
View File

@@ -0,0 +1,258 @@
module extism;
import std.conv: castFrom, to;
import std.meta: Alias;
import std.string: fromStringz, toStringz;
import runtime;
/// A list of all possible value types in WebAssembly.
enum ValType {
i32 = I32,
i64 = I64,
f32 = F32,
f64 = F64,
v128 = V128,
funcRef = FuncRef,
externRef = ExternRef,
}
// Opaque Pointers
///
alias CancelHandle = Alias!(void*);
///
alias CurrentPlugin = Alias!(void*);
///
alias ExtismSize = ulong;
/// A union type for host function argument/return values.
union ValUnion {
///
int i32;
///
long i64;
///
float f32;
///
double f64;
}
/// Holds the type and value of a function argument/return.
struct Val {
///
ValType t;
///
ValUnion v;
}
/// Host function signature.
///
/// Used to register host functions with plugins.
/// Params:
/// plugin: the currently executing plugin from within a host function
/// inputs: argument values
/// outputs: return values
/// data: user data associated with the host function
alias FunctionType = void delegate(
CurrentPlugin plugin, const Val[] inputs, Val[] outputs, void* data
);
/// Returns: A slice of an allocated memory block of the currently running plugin.
ubyte[] memory(CurrentPlugin plugin, ulong n) {
auto length = extism_current_plugin_memory_length(cast(ExtismCurrentPlugin*) plugin, n);
return extism_current_plugin_memory(cast(ExtismCurrentPlugin*) plugin)[n .. (n + length)];
}
/// Allocate a memory block in the currently running plugin.
ulong memoryAlloc(CurrentPlugin plugin, ulong n) {
return extism_current_plugin_memory_alloc(cast(ExtismCurrentPlugin*) plugin, n);
}
/// Free an allocated memory block.
void memoryFree(CurrentPlugin plugin, ulong ptr) {
extism_current_plugin_memory_free(cast(ExtismCurrentPlugin*) plugin, ptr);
}
/// A host function, where `T` is the type of its user data.
struct Function(T) {
/// Managed pointer.
ExtismFunction* func;
alias func this;
private string _namespace = null;
/// Create a new host function.
/// Params:
/// name: function name, this should be valid UTF-8
/// inputs: argument types
/// outputs: return types
/// func: the function to call
/// userData: a pointer that will be passed to the function when it's called. This value should live as long as the function exists.
/// freeUserData: a callback to release the `userData`` value when the resulting `Function` is freed.
this(
string name, const ValType[] inputs, const ValType[] outputs, FunctionType func,
T userData, void function(T userData) freeUserData = null
) {
import std.functional: toDelegate;
// Bind the given host function with C linkage
auto funcClosure = ((
ExtismCurrentPlugin* plugin,
const ExtismVal* inputs, ulong numInputs, ExtismVal* outputs, ulong numOutputs,
void* data
) {
func(plugin, (cast(const Val*) inputs)[0 .. numInputs], (cast(Val*) outputs)[0 .. numOutputs], data);
}).toDelegate.bindDelegate;
// Bind the `freeUserData` function with C linkage
auto freeUserDataHandler = freeUserData is null ? null : ((void* userDataPtr) {
if (userDataPtr is null) return;
freeUserData((&userDataPtr).to!T);
}).toDelegate.bindDelegate;
this.func = extism_function_new(
name.toStringz,// See https://dlang.org/spec/importc.html#enums
// See https://forum.dlang.org/post/qmidcpaxctbuphcyvkdc@forum.dlang.org
castFrom!(const(ValType)*)
.to!(typeof(ExtismVal.t)*)(inputs.ptr), inputs.length,
castFrom!(const(ValType)*).to!(typeof(ExtismVal.t)*)(outputs.ptr), outputs.length,
funcClosure,
&userData,
freeUserDataHandler,
);
}
~this() {
extism_function_free(func);
}
/// Get the namespace of this function.
string namespace() {
return this._namespace;
}
/// Set the namespace of this function.
void namespace(string value) {
this._namespace = value;
extism_function_set_namespace(func, value.toStringz);
}
}
///
struct Plugin {
import std.uuid: UUID;
/// Managed pointer.
ExtismPlugin* plugin;
alias plugin this;
/// Create a new plugin.
/// Params:
/// wasm: is a WASM module (wat or wasm) or a JSON encoded manifest
/// functions: is an array of ExtismFunction*
/// withWasi: enables/disables WASI
/// Throws: `Exception` if the plugin could not be created.
this(const ubyte[] wasm, const(ExtismFunction)*[] functions, bool withWasi = false) {
char* errorMsgPtr = null;
plugin = extism_plugin_new(
wasm.ptr, wasm.length, cast(ExtismFunction**) functions.ptr, functions.length, withWasi, &errorMsgPtr
);
// See https://github.com/extism/extism/blob/ddcbeec3debe787293a9957c8be88f80a64b7c22/c/main.c#L67
// Instead of terminating the host process, throw.
if (plugin !is null)
return;
auto errorMsg = errorMsgPtr.fromStringz.idup;
extism_plugin_new_error_free(errorMsgPtr);
// TODO: Subclass `Exception` for better error handling
throw new Exception(errorMsg);
}
~this() {
extism_plugin_free(plugin);
}
/// Get a plugin's ID.
UUID id() {
return UUID(extism_plugin_id(plugin)[0 .. 16]);
}
/// Get the error associated with this `Plugin`.
string error() {
return extism_error(plugin).fromStringz.idup;
}
/// Update plugin config values, this will merge with the existing values.
bool config(ubyte[] json) {
return extism_plugin_config(plugin, json.ptr, json.length);
}
/// See_Also: `cancel`
const(CancelHandle) cancelHandle() {
return extism_plugin_cancel_handle(plugin);
}
/// Cancel a running plugin.
/// See_Also: `cancelHandle`
bool cancel(const CancelHandle handle) {
return extism_plugin_cancel(cast(ExtismCancelHandle*) handle);
}
/// Returns: Whether a function with `name` exists.
bool functionExists(string name) {
return extism_plugin_function_exists(plugin, name.toStringz);
}
/// Call a function.
/// Params:
/// funcName: is the function to call
/// data: is the input data
void call(string funcName, ubyte[] data = null) {
// Returns `0` if the call was successful, otherwise `-1`.
if (extism_plugin_call(plugin, funcName.toStringz, data.ptr, data.length) != 0)
throw new Exception(this.error);
}
/// Get the plugin's output data.
///
/// Note: This copies the data into a managed buffer.
/// Remarks: Use `outputData.length` to retrieve size of plugin output.
ubyte[] outputData() {
import std.algorithm: copy;
auto outputLength = extism_plugin_output_length(plugin);
auto outputData = extism_plugin_output_data(plugin)[0 .. outputLength];
auto buffer = new ubyte[outputLength];
assert(
outputData.copy(buffer).length == 0,
"Output data was not completely copied into buffer, i.e. buffer was not filled."
);
return buffer;
}
}
/// Get the error associated with a `Context` or `Plugin`, if plugin is -1 then the context error will be returned.
/// Set log file and level.
bool setLogFile(string filename, string logLevel) {
return extism_log_file(filename.toStringz, logLevel.toStringz);
}
/// Get the Extism version string.
string version_() {
return extism_version().fromStringz.idup;
}
import std.traits: isDelegate;
/// Transform the given delegate into a static function pointer with C linkage.
/// See_Also: <a href="https://stackoverflow.com/a/22845722/1363247">stackoverflow.com/a/22845722/1363247</a>
package auto bindDelegate(Func)(Func f) if (isDelegate!Func) {
import std.traits: ParameterTypeTuple, ReturnType;
static Func delegate_;
delegate_ = f;
extern (C) static ReturnType!Func func(ParameterTypeTuple!Func args) {
return delegate_(args);
}
return &func;
}

1
d/runtime.c Normal file
View File

@@ -0,0 +1 @@
#include "runtime/extism.h"

116
dscanner.ini Normal file
View File

@@ -0,0 +1,116 @@
; Configure which static analysis checks are enabled
[analysis.config.StaticAnalysisConfig]
; Check variable, class, struct, interface, union, and function names against t
; he Phobos style guide
style_check="enabled"
; Check for array literals that cause unnecessary allocation
enum_array_literal_check="enabled"
; Check for poor exception handling practices
exception_check="enabled"
; Check for use of the deprecated 'delete' keyword
delete_check="enabled"
; Check for use of the deprecated floating point operators
float_operator_check="enabled"
; Check number literals for readability
number_style_check="enabled"
; Checks that opEquals, opCmp, toHash, and toString are either const, immutable
; , or inout.
object_const_check="enabled"
; Checks for .. expressions where the left side is larger than the right.
backwards_range_check="enabled"
; Checks for if statements whose 'then' block is the same as the 'else' block
if_else_same_check="enabled"
; Checks for some problems with constructors
constructor_check="enabled"
; Checks for unused variables
unused_variable_check="enabled"
; Checks for unused labels
unused_label_check="enabled"
; Checks for unused function parameters
unused_parameter_check="enabled"
; Checks for duplicate attributes
duplicate_attribute="enabled"
; Checks that opEquals and toHash are both defined or neither are defined
opequals_tohash_check="enabled"
; Checks for subtraction from .length properties
length_subtraction_check="enabled"
; Checks for methods or properties whose names conflict with built-in propertie
; s
builtin_property_names_check="enabled"
; Checks for confusing code in inline asm statements
asm_style_check="enabled"
; Checks for confusing logical operator precedence
logical_precedence_check="enabled"
; Checks for undocumented public declarations
undocumented_declaration_check="enabled"
; Checks for poor placement of function attributes
function_attribute_check="enabled"
; Checks for use of the comma operator
comma_expression_check="enabled"
; Checks for local imports that are too broad. Only accurate when checking code
; used with D versions older than 2.071.0
local_import_check="disabled"
; Checks for variables that could be declared immutable
could_be_immutable_check="enabled"
; Checks for redundant expressions in if statements
redundant_if_check="enabled"
; Checks for redundant parenthesis
redundant_parens_check="enabled"
; Checks for mismatched argument and parameter names
mismatched_args_check="enabled"
; Checks for labels with the same name as variables
label_var_same_name_check="enabled"
; Checks for lines longer than `max_line_length` characters
long_line_check="enabled"
; The maximum line length used in `long_line_check`.
max_line_length="120"
; Checks for assignment to auto-ref function parameters
auto_ref_assignment_check="enabled"
; Checks for incorrect infinite range definitions
incorrect_infinite_range_check="enabled"
; Checks for asserts that are always true
useless_assert_check="enabled"
; Check for uses of the old-style alias syntax
alias_syntax_check="enabled"
; Checks for else if that should be else static if
static_if_else_check="enabled"
; Check for unclear lambda syntax
lambda_return_check="enabled"
; Check for auto function without return statement
auto_function_check="enabled"
; Check for sortedness of imports
imports_sortedness="disabled"
; Check for explicitly annotated unittests
explicitly_annotated_unittests="disabled"
; Check for properly documented public functions (Returns, Params)
properly_documented_public_functions="disabled"
; Check for useless usage of the final attribute
final_attribute_check="enabled"
; Check for virtual calls in the class constructors
vcall_in_ctor="enabled"
; Check for useless user defined initializers
useless_initializer="disabled"
; Check allman brace style
allman_braces_check="disabled"
; Check for redundant attributes
redundant_attributes_check="enabled"
; Check public declarations without a documented unittest
has_public_example="disabled"
; Check for asserts without an explanatory message
assert_without_msg="disabled"
; Check indent of if constraints
if_constraints_indent="disabled"
; Check for @trusted applied to a bigger scope than a single function
trust_too_much="enabled"
; Check for redundant storage classes on variable declarations
redundant_storage_classes="enabled"
; Check for unused function return values
unused_result="enabled"
; Enable cyclomatic complexity check
cyclomatic_complexity="disabled"
; Maximum cyclomatic complexity after which to issue warnings
max_cyclomatic_complexity="50"
; Check for function bodies on disabled functions
body_on_disabled_func_check="enabled"
; ModuleFilters for selectively enabling (+std) and disabling (-std.internal) individual checks
[analysis.config.ModuleFilters]

23
dub.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "extism",
"description": "The Universal Plug-in System. Extend anything with WebAssembly (wasm).",
"license": "BSD-3-Clause",
"authors": [
"Chance Snow <git@chancesnow.me>",
"Extism contributors"
],
"copyright": "Copyright © 2023, Extism contributors",
"toolchainRequirements": {
"frontend": ">=2.102"
},
"targetPath": "target",
"sourceFiles": ["d/extism.d"],
"importPaths": ["d"],
"systemDependencies": "extism >= 0.4.0",
"targetType": "sourceLibrary",
"subPackages": [
"d/examples/hello"
],
"dflags": ["-P-I$EXTISM_PACKAGE_DIR"],
"libs": ["extism"]
}

View File

@@ -51,6 +51,7 @@ if __name__ == '__main__':
# Check for missing SDK functions
sdks = [
Lang('cpp', 'hpp'),
Lang('d', 'd'),
Lang('go', 'go', path='..'),
Lang('haskell', 'hs'),
Lang('node', 'ts'),