Files
extism/ruby/lib/extism.rb
2022-10-17 09:25:22 -05:00

184 lines
5.3 KiB
Ruby

require 'ffi'
require 'json'
module Extism
module C
extend FFI::Library
ffi_lib "extism"
attach_function :extism_context_new, [], :pointer
attach_function :extism_context_free, [:pointer], :void
attach_function :extism_plugin_new, [:pointer, :pointer, :uint64, :bool], :int32
attach_function :extism_plugin_update, [:pointer, :int32, :pointer, :uint64, :bool], :bool
attach_function :extism_error, [:pointer, :int32], :string
attach_function :extism_plugin_call, [:pointer, :int32, :string, :pointer, :uint64], :int32
attach_function :extism_plugin_function_exists, [:pointer, :int32, :string], :bool
attach_function :extism_plugin_output_length, [:pointer, :int32], :uint64
attach_function :extism_plugin_output_data, [:pointer, :int32], :pointer
attach_function :extism_log_file, [:string, :pointer], :void
attach_function :extism_plugin_free, [:pointer, :int32], :void
attach_function :extism_context_reset, [:pointer], :void
attach_function :extism_version, [], :string
end
class Error < StandardError
end
# Return the version of Extism
def self.extism_version
C.extism_version
end
# Set log file and level, this is a global configuration
def self.set_log_file(name, level=nil)
if level then
level = FFI::MemoryPointer::from_string(level)
end
C.extism_log_file(name, level)
end
$PLUGINS = {}
$FREE_PLUGIN = proc { |id|
x = $PLUGINS[id]
if !x.nil? then
C.extism_plugin_free(x[:context].pointer, x[:plugin])
$PLUGINS.delete(id)
end
}
$CONTEXTS = {}
$FREE_CONTEXT = proc { |id|
x = $CONTEXTS[id]
if !x.nil? then
C.extism_context_free($CONTEXTS[id])
$CONTEXTS.delete(id)
end
}
# Context is used to manage plugins
class Context
attr_accessor :pointer
def initialize
@pointer = C.extism_context_new()
$CONTEXTS[self.object_id] = @pointer
ObjectSpace.define_finalizer(self, $FREE_CONTEXT)
end
# Remove all registered plugins
def reset
C.extism_context_reset(@pointer)
end
# Free the context, this should be called when it is no longer needed
def free
if @pointer.nil? then
return
end
$CONTEXTS.delete(self.object_id)
C.extism_context_free(@pointer)
@pointer = nil
end
# Create a new plugin from a WASM module or JSON encoded manifest
def plugin(wasm, wasi=false, config=nil)
return Plugin.new(self, wasm, wasi, config)
end
end
def self.with_context(&block)
ctx = Context.new
begin
x = block.call(ctx)
return x
ensure
ctx.free
end
end
class Plugin
def initialize(context, wasm, wasi=false, config=nil)
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_new(context.pointer, code, wasm.bytesize, wasi)
if @plugin < 0 then
err = C.extism_error(-1)
if err&.empty? then
raise Error.new "extism_plugin_new failed"
else raise Error.new err
end
end
@context = context
$PLUGINS[self.object_id] = {:plugin => @plugin, :context => context}
ObjectSpace.define_finalizer(self, $FREE_PLUGIN)
if config != nil and @plugin >= 0 then
s = JSON.generate(config)
ptr = FFI::MemoryPointer::from_string(s)
C.extism_plugin_config(@context.pointer, @plugin, ptr, s.bytesize)
end
end
# Update a plugin with new WASM module or manifest
def update(wasm, wasi=false, config=nil)
if wasm.class == Hash then
wasm = JSON.generate(wasm)
end
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
code.put_bytes(0, wasm)
ok = C.extism_plugin_update(@context.pointer, @plugin, code, wasm.bytesize, wasi)
if !ok then
err = C.extism_error(-1)
if err&.empty? then
raise Error.new "extism_plugin_update failed"
else raise Error.new err
end
end
if config != nil then
s = JSON.generate(config)
ptr = FFI::MemoryPointer::from_string(s)
C.extism_plugin_config(@context.pointer, @plugin, ptr, s.bytesize)
end
end
# Check if a function exists
def function_exists(name)
return C.extism_function_exists(@context.pointer, @plugin, name)
end
# Call a function by name
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(@context.pointer, @plugin, name, input, data.bytesize)
if rc != 0 then
err = C.extism_error(@context.pointer, @plugin)
if err&.empty? then
raise Error.new "extism_call failed"
else raise Error.new err
end
end
out_len = C.extism_plugin_output_length(@context.pointer, @plugin)
buf = C.extism_plugin_output_data(@context.pointer, @plugin)
return block.call(buf, out_len)
end
# Free a plugin, this should be called when the plugin is no longer needed
def free
if @context.pointer.nil? then
return
end
$PLUGINS.delete(self.object_id)
C.extism_plugin_free(@context.pointer, @plugin)
@plugin = -1
end
end
end