lib/extism.rb in extism-0.0.1.rc4 vs lib/extism.rb in extism-0.0.1.rc6

- old
+ new

@@ -1,183 +1,245 @@ -require 'ffi' -require 'json' +require "ffi" +require "json" +require_relative "./extism/version" 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 + # + # @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 - def self.set_log_file(name, level=nil) - if level then + # @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) + if level 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 + if !x.nil? 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 + if !x.nil? C.extism_context_free($CONTEXTS[id]) $CONTEXTS.delete(id) end } - # Context is used to manage plugins + # A Context is needed to create plugins. The Context + # is where your plugins live. Freeing the context + # frees all of the plugins in its scope. + # + # @example Create and free a context + # ctx = Extism::Context.new + # plugin = ctx.plugin(my_manifest) + # puts plugin.call("my_func", "my-input") + # ctx.free # frees any plugins + # + # @example Use with_context to auto-free + # Extism.with_context do |ctx| + # plugin = ctx.plugin(my_manifest) + # puts plugin.call("my_func", "my-input") + # end # frees context after exiting this block + # + # @attr_reader pointer [FFI::Pointer] Pointer to the Extism context. *Used internally*. class Context - attr_accessor :pointer + attr_reader :pointer + # Initialize a new context def initialize @pointer = C.extism_context_new() $CONTEXTS[self.object_id] = @pointer - ObjectSpace.define_finalizer(self, $FREE_CONTEXT) + ObjectSpace.define_finalizer(self, $FREE_CONTEXT) end - # Remove all registered plugins + # Remove all registered plugins in this context + # @return [void] def reset C.extism_context_reset(@pointer) end # Free the context, this should be called when it is no longer needed + # @return [void] def free - if @pointer.nil? then - return - end + return if @pointer.nil? + $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) + # + # @param wasm [Hash, String] The manifest for the plugin. See https://extism.org/docs/concepts/manifest/. + # @param wasi [Boolean] Enable WASI support + # @param config [Hash] The plugin config + # @return [Plugin] + def plugin(wasm, wasi = false, config = nil) + Plugin.new(self, wasm, wasi, config) end end - + # A context manager to create contexts and ensure that they get freed. + # + # @example Use with_context to auto-free + # Extism.with_context do |ctx| + # plugin = ctx.plugin(my_manifest) + # puts plugin.call("my_func", "my-input") + # end # frees context after exiting this block + # + # @yield [ctx] Yields the created Context + # @return [Object] returns whatever your block returns def self.with_context(&block) ctx = Context.new begin x = block.call(ctx) return x ensure ctx.free end end + # A Plugin represents an instance of your WASM program from the given manifest. class Plugin - def initialize(context, wasm, wasi=false, config=nil) - if wasm.class == Hash then + # Intialize a plugin + # + # @see Extism::Context#plugin + # @param context [Context] The context to manager this 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(context, wasm, wasi = false, config = nil) + @context = context + if wasm.class == Hash 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 + if @plugin < 0 + err = C.extism_error(@context.pointer, -1) + if err&.empty? raise Error.new "extism_plugin_new failed" - else raise Error.new err + 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 + $PLUGINS[self.object_id] = { :plugin => @plugin, :context => context } + ObjectSpace.define_finalizer(self, $FREE_PLUGIN) + if config != nil and @plugin >= 0 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 + # + # @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 + # @return [void] + def update(wasm, wasi = false, config = nil) + if wasm.class == Hash 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 + if !ok + err = C.extism_error(@context.pointer, @plugin) + if err&.empty? raise Error.new "extism_plugin_update failed" - else raise Error.new err + else + raise Error.new err end end - if config != nil then + if config != nil 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) + # + # @param name [String] The name of the function + # @return [Boolean] Returns true if function exists + def has_function?(name) + C.extism_plugin_function_exists(@context.pointer, @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) } + 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 + if rc != 0 err = C.extism_error(@context.pointer, @plugin) - if err&.empty? then + if err&.empty? raise Error.new "extism_call failed" - else raise Error.new err + 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) + block.call(buf, out_len) end # Free a plugin, this should be called when the plugin is no longer needed + # + # @return [void] def free - if @context.pointer.nil? then - return - end + return if @context.pointer.nil? $PLUGINS.delete(self.object_id) C.extism_plugin_free(@context.pointer, @plugin) @plugin = -1 end + end + private + + # 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" + 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 end