mirror of
https://github.com/extism/extism.git
synced 2026-04-23 03:00:11 -04:00
Adds support for host functions and cleans up some of the FFI code.
## API
To make a host function, you can pass a proc to `Function::new`:
```ruby
func = proc do |current_plugin, inputs, outputs, user_data|
input = current_plugin.input_as_bytes(inputs.first)
current_plugin.return_string(outputs.first, "#{input} #{user_data}")
end
f = Extism::Function.new('transform_string', [Extism::ValType::I64], [Extism::ValType::I64], func, 'My User Data')
plugin = Extism::Plugin.new(host_manifest, [f], true)
result = plugin.call('reflect_string', 'Hello, World!')
assert_equal result, 'Hello, World! My User Data'
```
If your function is in a module or a class, you can use
`method(name).to_proc`. Example:
```ruby
module Test
def self.my_function(current_plugin, inputs, outputs, user_data)
input = current_plugin.input_as_bytes(inputs.first)
current_plugin.return_string(outputs.first, "#{input} #{user_data}")
end
end
func = Test.method(:my_function).to_proc
f = Extism::Function.new('my_function', [Extism::ValType::I64], [Extism::ValType::I64], func, 'My User Data')
```
`current_plugin` is of the type CurrentPlugin which has some helpful
methods:
* `CurrentPlugin#memory_at_offset(int)` returns a `Memory` object given
a memory pointer
* `CurrentPlugin#free(Memory)` frees the memory
* `CurrentPlugin#alloc(int)` allocates new memory and returns a `Memory`
* `CurrentPlugin#input_as_bytes(Value)` returns the bytes for the given
input param
* `CurrentPlugin#return_bytes(Value, Array)` Sets the array of bytes to
the return for the given output value
* `CurrentPlugin#input_as_bytes(Value, String)` Sets the string to the
return for the given output value
316 lines
9.1 KiB
Ruby
316 lines
9.1 KiB
Ruby
require 'ffi'
|
|
require 'json'
|
|
require_relative './extism/version'
|
|
|
|
module Extism
|
|
class Error < StandardError
|
|
end
|
|
|
|
# Return the version of Extism
|
|
#
|
|
# @return [String] The version string of the Extism runtime
|
|
def self.extism_version
|
|
C.extism_version
|
|
end
|
|
|
|
# Set log file and level, this is a global configuration
|
|
# @param name [String] The path to the logfile
|
|
# @param level [String] The log level. One of {"debug", "error", "info", "trace" }
|
|
def self.set_log_file(name, level = nil)
|
|
C.extism_log_file(name, level)
|
|
end
|
|
|
|
$PLUGINS = {}
|
|
$FREE_PLUGIN = proc { |ptr|
|
|
x = $PLUGINS[ptr]
|
|
unless x.nil?
|
|
C.extism_plugin_free(x[:plugin])
|
|
$PLUGINS.delete(ptr)
|
|
end
|
|
}
|
|
|
|
# A CancelHandle can be used to cancel a running plugin from another thread
|
|
class CancelHandle
|
|
def initialize(handle)
|
|
@handle = handle
|
|
end
|
|
|
|
# Cancel the plugin used to generate the handle
|
|
def cancel
|
|
C.extism_plugin_cancel(@handle)
|
|
end
|
|
end
|
|
|
|
# A Plugin represents an instance of your WASM program from the given manifest.
|
|
class Plugin
|
|
# Intialize a plugin
|
|
#
|
|
# @param wasm [Hash, String] The manifest or WASM binary. See https://extism.org/docs/concepts/manifest/.
|
|
# @param wasi [Boolean] Enable WASI support
|
|
# @param config [Hash] The plugin config
|
|
def initialize(wasm, functions = [], wasi = false, config = nil)
|
|
wasm = JSON.generate(wasm) if wasm.instance_of?(Hash)
|
|
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
|
|
errmsg = FFI::MemoryPointer.new(:pointer)
|
|
code.put_bytes(0, wasm)
|
|
funcs_ptr = FFI::MemoryPointer.new(C::ExtismFunction)
|
|
funcs_ptr.write_array_of_pointer(functions.map { |f| f.pointer })
|
|
@plugin = C.extism_plugin_new(code, wasm.bytesize, funcs_ptr, functions.length, wasi, errmsg)
|
|
if @plugin.null?
|
|
err = errmsg.read_pointer.read_string
|
|
C.extism_plugin_new_error_free errmsg.read_pointer
|
|
raise Error, err
|
|
end
|
|
$PLUGINS[object_id] = { plugin: @plugin }
|
|
ObjectSpace.define_finalizer(self, $FREE_PLUGIN)
|
|
return unless !config.nil? and @plugin.null?
|
|
|
|
s = JSON.generate(config)
|
|
ptr = FFI::MemoryPointer.from_string(s)
|
|
C.extism_plugin_config(@plugin, ptr, s.bytesize)
|
|
end
|
|
|
|
# Check if a function exists
|
|
#
|
|
# @param name [String] The name of the function
|
|
# @return [Boolean] Returns true if function exists
|
|
def has_function?(name)
|
|
C.extism_plugin_function_exists(@plugin, name)
|
|
end
|
|
|
|
# Call a function by name
|
|
#
|
|
# @param name [String] The function name
|
|
# @param data [String] The input data for the function
|
|
# @return [String] The output from the function in String form
|
|
def call(name, data, &block)
|
|
# If no block was passed then use Pointer::read_string
|
|
block ||= ->(buf, len) { buf.read_string(len) }
|
|
input = FFI::MemoryPointer.from_string(data)
|
|
rc = C.extism_plugin_call(@plugin, name, input, data.bytesize)
|
|
if rc != 0
|
|
err = C.extism_plugin_error(@plugin)
|
|
raise Error, 'extism_call failed' if err&.empty?
|
|
|
|
raise Error, err
|
|
|
|
end
|
|
|
|
out_len = C.extism_plugin_output_length(@plugin)
|
|
buf = C.extism_plugin_output_data(@plugin)
|
|
block.call(buf, out_len)
|
|
end
|
|
|
|
# Free a plugin, this should be called when the plugin is no longer needed
|
|
#
|
|
# @return [void]
|
|
def free
|
|
return if @plugin.null?
|
|
|
|
$PLUGINS.delete(object_id)
|
|
C.extism_plugin_free(@plugin)
|
|
@plugin = nil
|
|
end
|
|
|
|
# Get a CancelHandle for a plugin
|
|
def cancel_handle
|
|
CancelHandle.new(C.extism_plugin_cancel_handle(@plugin))
|
|
end
|
|
end
|
|
|
|
Memory = Struct.new(:offset, :len)
|
|
|
|
class CurrentPlugin
|
|
def initialize(ptr)
|
|
@ptr = ptr
|
|
end
|
|
|
|
def alloc(amount)
|
|
offset = C.extism_current_plugin_memory_alloc(@ptr, amount)
|
|
Memory.new(offset, amount)
|
|
end
|
|
|
|
def free(memory)
|
|
C.extism_current_plugin_memory_free(@ptr, memory.offset)
|
|
end
|
|
|
|
def memory_at_offset(offset)
|
|
len = C.extism_current_plugin_memory_length(@ptr, offset)
|
|
Memory.new(offset, len)
|
|
end
|
|
|
|
def input_as_bytes(input)
|
|
# TODO: should assert that this is an int input
|
|
mem = memory_at_offset(input.value)
|
|
memory_ptr(mem).read_bytes(mem.len)
|
|
end
|
|
|
|
def return_bytes(output, bytes)
|
|
mem = alloc(bytes.length)
|
|
memory_ptr(mem).put_bytes(0, bytes)
|
|
output.value = mem.offset
|
|
end
|
|
|
|
def return_string(output, string)
|
|
return_bytes(output, string)
|
|
end
|
|
|
|
private
|
|
|
|
def memory_ptr(mem)
|
|
plugin_ptr = C.extism_current_plugin_memory(@ptr)
|
|
FFI::Pointer.new(plugin_ptr.address + mem.offset)
|
|
end
|
|
end
|
|
|
|
module ValType
|
|
I32 = 0
|
|
I64 = 1
|
|
F32 = 2
|
|
F64 = 3
|
|
V128 = 4
|
|
FUNC_REF = 5
|
|
EXTERN_REF = 6
|
|
end
|
|
|
|
class Val
|
|
def initialize(ptr)
|
|
@c_val = C::ExtismVal.new(ptr)
|
|
end
|
|
|
|
def type
|
|
case @c_val[:t]
|
|
when :I32
|
|
:i32
|
|
when :I64
|
|
:i64
|
|
when :F32
|
|
:f32
|
|
when :F64
|
|
:f64
|
|
else
|
|
raise "Unsupported wasm value type #{type}"
|
|
end
|
|
end
|
|
|
|
def value
|
|
@c_val[:v][type]
|
|
end
|
|
|
|
def value=(val)
|
|
@c_val[:v][type] = val
|
|
end
|
|
end
|
|
|
|
class Function
|
|
def initialize(name, args, returns, func_proc, user_data)
|
|
@name = name
|
|
@args = args
|
|
@returns = returns
|
|
@func = func_proc
|
|
@user_data = user_data
|
|
end
|
|
|
|
def pointer
|
|
return @pointer if @pointer
|
|
|
|
free = proc { puts 'freeing ' }
|
|
args = C.from_int_array(@args)
|
|
returns = C.from_int_array(@returns)
|
|
@pointer = C.extism_function_new(@name, args, @args.length, returns, @returns.length, c_func, free, nil)
|
|
end
|
|
|
|
private
|
|
|
|
def c_func
|
|
@c_func ||= proc do |plugin_ptr, inputs_ptr, inputs_size, outputs_ptr, outputs_size, _data_ptr|
|
|
current_plugin = CurrentPlugin.new(plugin_ptr)
|
|
val_struct_size = C::ExtismVal.size
|
|
|
|
inputs = (0...inputs_size).map do |i|
|
|
Val.new(inputs_ptr + i * val_struct_size)
|
|
end
|
|
outputs = (0...outputs_size).map do |i|
|
|
Val.new(outputs_ptr + i * val_struct_size)
|
|
end
|
|
|
|
@func.call(current_plugin, inputs, outputs, @user_data)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Private module used to interface with the Extism runtime.
|
|
# *Warning*: Do not use or rely on this directly.
|
|
module C
|
|
extend FFI::Library
|
|
ffi_lib 'extism'
|
|
|
|
def self.from_int_array(ruby_array)
|
|
ptr = FFI::MemoryPointer.new(:int, ruby_array.length)
|
|
ptr.write_array_of_int(ruby_array)
|
|
ptr
|
|
end
|
|
|
|
typedef :uint64, :ExtismMemoryHandle
|
|
typedef :uint64, :ExtismSize
|
|
|
|
enum :ExtismValType, %i[I32 I64 F32 F64 V128 FuncRef ExternRef]
|
|
|
|
class ExtismValUnion < FFI::Union
|
|
layout :i32, :int32,
|
|
:i64, :int64,
|
|
:f32, :float,
|
|
:f64, :double
|
|
end
|
|
|
|
class ExtismVal < FFI::Struct
|
|
layout :t, :ExtismValType,
|
|
:v, ExtismValUnion
|
|
end
|
|
|
|
class ExtismFunction < FFI::Struct
|
|
layout :name, :string,
|
|
:inputs, :pointer,
|
|
:n_inputs, :uint64,
|
|
:outputs, :pointer,
|
|
:n_outputs, :uint64,
|
|
:data, :pointer
|
|
end
|
|
|
|
callback :ExtismFunctionType, [
|
|
:pointer, # plugin
|
|
:pointer, # inputs
|
|
:ExtismSize, # n_inputs
|
|
:pointer, # outputs
|
|
:ExtismSize, # n_outputs
|
|
:pointer # user_data
|
|
], :void
|
|
|
|
callback :ExtismFreeFunctionType, [], :void
|
|
|
|
attach_function :extism_plugin_id, [:pointer], :pointer
|
|
attach_function :extism_current_plugin_memory, [:pointer], :pointer
|
|
attach_function :extism_current_plugin_memory_alloc, %i[pointer ExtismSize], :ExtismMemoryHandle
|
|
attach_function :extism_current_plugin_memory_length, %i[pointer ExtismMemoryHandle], :ExtismSize
|
|
attach_function :extism_current_plugin_memory_free, %i[pointer ExtismMemoryHandle], :void
|
|
attach_function :extism_function_new,
|
|
%i[string pointer ExtismSize pointer ExtismSize ExtismFunctionType ExtismFreeFunctionType pointer], :pointer
|
|
attach_function :extism_function_free, [:pointer], :void
|
|
attach_function :extism_function_set_namespace, %i[pointer string], :void
|
|
attach_function :extism_plugin_new, %i[pointer ExtismSize pointer ExtismSize bool pointer], :pointer
|
|
attach_function :extism_plugin_new_error_free, [:pointer], :void
|
|
attach_function :extism_plugin_free, [:pointer], :void
|
|
attach_function :extism_plugin_cancel_handle, [:pointer], :pointer
|
|
attach_function :extism_plugin_cancel, [:pointer], :bool
|
|
attach_function :extism_plugin_config, %i[pointer pointer ExtismSize], :bool
|
|
attach_function :extism_plugin_function_exists, %i[pointer string], :bool
|
|
attach_function :extism_plugin_call, %i[pointer string pointer ExtismSize], :int32
|
|
attach_function :extism_error, [:pointer], :string
|
|
attach_function :extism_plugin_error, [:pointer], :string
|
|
attach_function :extism_plugin_output_length, [:pointer], :ExtismSize
|
|
attach_function :extism_plugin_output_data, [:pointer], :pointer
|
|
attach_function :extism_log_file, %i[string string], :bool
|
|
attach_function :extism_version, [], :string
|
|
end
|
|
end
|