extism 1.0.0.pre.rc.1 → 1.0.0.pre.rc.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed731bd8ff622c67eee229b14588ce7e6311337d5b3c888aca2e116f77758b4b
4
- data.tar.gz: e0bea72460c008c16dc4747c39ebb09643c48ddecf6c5a5efc0bae754609ebdb
3
+ metadata.gz: ff531578509d2c71f3db9d20e01fdbbba922e342f76dbb84aafa69ce94db3350
4
+ data.tar.gz: 163c31c8cddeec68fd774af387effcbd13e3a8e9ea40ab26445ab94fa6db75a3
5
5
  SHA512:
6
- metadata.gz: 81e70c7e6b7b649733f68b4c284104155ccec044f9604882b99cb54b626a777aa4e7dcfdc51a224b00600c6ee705f4a9b5761c85b5fb73ebff542be94ae5627c
7
- data.tar.gz: 39ee1905ea234277c040698eae6098dfcc5d0ffac53185598536ac915dc4999c316bdab88d5ccb716895bf006d2cb25fc6b636640a5402b5b8aaf5b658653fd7
6
+ metadata.gz: a5b97afbbeddff3e52a0627832fa92b860ef921cee1b82d0f1a4753b2b9a47ae89810e2f2c8b061f94aff9fa639283a7886d06026e3c98a588693f86369cbdb1
7
+ data.tar.gz: 5399c2a2d9f0183bad275c212f7009b73ef9234ddc2cc4a90433972a3decb36926c967fd3935f20e58f2cb18966c9e76546e967db4882ecc01936eb0593092ab
data/.yardopts CHANGED
@@ -1 +1,2 @@
1
1
  --readme README.md
2
+ --exclude lib/extism/libextism.rb
data/Gemfile CHANGED
@@ -1,16 +1,16 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- # Specify your gem's dependencies in extism.gemspec
6
- gemspec
7
-
8
- gem 'ffi', '~> 1.15.5'
9
- gem 'rake', '~> 13.0'
10
-
11
- group :development do
12
- gem 'debug'
13
- gem 'minitest', '~> 5.20.0'
14
- gem 'rufo', '~> 0.13.0'
15
- gem 'yard', '~> 0.9.28'
16
- end
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in extism.gemspec
6
+ gemspec
7
+
8
+ gem 'ffi', '~> 1.15.5'
9
+ gem 'rake', '~> 13.0'
10
+
11
+ group :development do
12
+ gem 'debug'
13
+ gem 'minitest', '~> 5.20.0'
14
+ gem 'rufo', '~> 0.13.0'
15
+ gem 'yard', '~> 0.9.28'
16
+ end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extism (1.0.0.pre.rc.1)
4
+ extism (1.0.0.pre.rc.3)
5
5
  ffi (>= 1.0.0)
6
6
 
7
7
  GEM
@@ -29,6 +29,7 @@ GEM
29
29
 
30
30
  PLATFORMS
31
31
  arm64-darwin-22
32
+ x86_64-linux
32
33
 
33
34
  DEPENDENCIES
34
35
  debug
data/README.md CHANGED
@@ -1,191 +1,169 @@
1
- # Extism Ruby Host SDK
2
-
3
- > **Note**: This houses the 1.0 version of the Ruby SDK and is a work in progress. Please use the ruby SDK in extism/extism until we hit 1.0.
4
-
5
- This repo houses the ruby gem for integrating with the [Extism](https://extism.org/) runtime. Install this library into your host ruby applications to run Extism plugins.
6
-
7
- ## Installation
8
-
9
- You first need to [install the Extism runtime](https://extism.org/docs/install).
10
-
11
- Add this library to your [Gemfile](https://bundler.io/):
12
-
13
- ```ruby
14
- gem 'extism', '1.0.0-rc.1'
15
- ```
16
-
17
- Or if installing on the system level:
18
-
19
- ```
20
- gem install extism
21
- ```
22
-
23
- ## Getting Started
24
-
25
- First you should require `"extism"`:
26
-
27
- ```ruby
28
- require "extism"
29
- ```
30
-
31
- ### Creating A Plug-in
32
-
33
- The primary concept in Extism is the plug-in. You can think of a plug-in as a code module. It has imports and it has exports. These imports and exports define the interface, or your API. You decide what they are called and typed, and what they do. Then the plug-in developer implements them and you can call them.
34
-
35
- The code for a plug-in exist as a binary wasm module. We can load this with the raw bytes or we can use the manifest to tell Extism how to load it from disk or the web.
36
-
37
- For simplicity let's load one from the web:
38
-
39
- ```ruby
40
- manifest = {
41
- wasm: [
42
- { url: "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm" }
43
- ]
44
- }
45
- plugin = Extism::Plugin.new(manifest)
46
- ```
47
-
48
- > **Note**: The schema for this manifest can be found here: https://extism.org/docs/concepts/manifest/
49
-
50
- ### Calling A Plug-in's Exports
51
-
52
- This plug-in was written in C and it does one thing, it counts vowels in a string. As such it exposes one "export" function: `count_vowels`. We can call exports using `Extism::Plugin#call`:
53
-
54
- ```ruby
55
- plugin.call("count_vowels", "Hello, World!")
56
- # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
57
- ```
58
-
59
- All exports have a simple interface of optional bytes in, and optional bytes out. This plug-in happens to take a string and return a JSON encoded string with a report of results.
60
-
61
-
62
- ### Plug-in State
63
-
64
- Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export:
65
-
66
- ```ruby
67
- plugin.call("count_vowels", "Hello, World!")
68
- # => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
69
- plugin.call("count_vowels", "Hello, World!")
70
- # => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"}
71
- ```
72
-
73
- These variables will persist until this plug-in is freed or you initialize a new one.
74
-
75
- ### Configuration
76
-
77
- Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example:
78
-
79
- ```ruby
80
- plugin = Extism::Plugin.new(manifest)
81
- plugin.call("count_vowels", "Yellow, World!")
82
- # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
83
-
84
- plugin = Extism::Plugin.new(manifest, config: { vowels: "aeiouyAEIOUY" })
85
- plugin.call("count_vowels", "Yellow, World!")
86
- # => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"}
87
- ```
88
-
89
- ### Host Functions
90
-
91
- Host functions allow us to grant new capabilities to our plug-ins from our application. They are simply some ruby methods you write which can be passed to and invoked from any language inside the plug-in.
92
-
93
- > *Note*: Host functions can be a complicated topic. Please review this [concept doc](https://extism.org/docs/concepts/host-functions) if you are unsure how they work.
94
-
95
- ### Host Functions Example
96
-
97
- We've created a contrived, but familiar example to illustrate this. Suppose you are a stripe-like payments platform.
98
- When a [charge.succeeded](https://stripe.com/docs/api/events/types#event_types-charge.succeeded) event occurs, we will call the `on_charge_succeeded` function on our merchant's plug-in and let them decide what to do with it. Here our merchant has some very specific requirements, if the account has spent more than $100, their currency is USD, and they have no credits on their account, it will add $10 credit to their account and then send them an email.
99
-
100
- > *Note*: The source code for this is [here](https://github.com/extism/plugins/blob/main/store_credit/src/lib.rs) and is written in rust, but it could be written in any of our PDK languages.
101
-
102
- First let's create the manifest for our plug-in like usual but load up the store_credit plug-in:
103
-
104
- ```ruby
105
- manifest = {
106
- wasm: [
107
- { url: "https://github.com/extism/plugins/releases/latest/download/store_credit.wasm" }
108
- ]
109
- }
110
- ```
111
-
112
- But, unlike our original plug-in, this plug-in expects you to provide host functions that satisfy our plug-ins imports.
113
-
114
- In the ruby sdk, we have a concept for this call an "host environment". An environment is just an object that responds to `host_functions` and returns an array of `Extism::Function`s. We want to expose two capabilities to our plugin, `add_credit(customer_id, amount)` which adds credit to an account and `send_email(customer_id, email)` which sends them an email.
115
-
116
- ```ruby
117
-
118
- # This is global is just for demo purposes but would in
119
- # reality be in a database or something
120
- CUSTOMER = {
121
- full_name: 'John Smith',
122
- customer_id: 'abcd1234',
123
- total_spend: {
124
- currency: 'USD',
125
- amount_in_cents: 20_000
126
- },
127
- credit: {
128
- currency: 'USD',
129
- amount_in_cents: 0
130
- }
131
- }
132
-
133
- class Environment
134
- include Extism::HostEnvironment
135
-
136
- register_import :add_credit, [Extism::ValType::I64, Extism::ValType::I64], [Extism::ValType::I64]
137
- register_import :send_email, [Extism::ValType::I64, Extism::ValType::I64], []
138
-
139
- def add_credit(plugin, inputs, outputs, _user_data)
140
- # add_credit takes a string `customer_id` as the first parameter
141
- customer_id = plugin.input_as_string(inputs.first)
142
- # it takes an object `amount` { amount_in_cents: int, currency: string } as the second parameter
143
- amount = plugin.input_as_json(inputs[1])
144
-
145
- # we're just going to print it out and add to the CUSTOMER global
146
- puts "Adding Credit #{amount} to customer #{customer_id}"
147
- CUSTOMER[:credit][:amount_in_cents] += amount['amount_in_cents']
148
-
149
- # add_credit returns a Json object with the new customer details
150
- plugin.return_json(outputs.first, CUSTOMER)
151
- end
152
-
153
- def send_email(plugin, inputs, _outputs, _user_data)
154
- # send_email takes a string `customer_id` as the first parameter
155
- customer_id = plugin.input_as_string(inputs.first)
156
- # it takes an object `email` { subject: string, body: string } as the second parameter
157
- email = plugin.input_as_json(inputs[1])
158
-
159
- # we'll just print it but you could imagine we'd put something
160
- # in a database or call an internal api to send this email
161
- puts "Sending email #{email} to customer #{customer_id}"
162
-
163
- # it doesn't return anything
164
- end
165
- end
166
- ```
167
-
168
- Now we just need to create a new host environment and pass it in when loading the plug-in. Here our environment initializer takes no arguments, but you could imagine putting some merchant specific instance variables in there:
169
-
170
- ```ruby
171
- env = Environment.new
172
- plugin = Extism::Plugin.new(manifest, environment: env)
173
- ```
174
-
175
- Now we can invoke the event:
176
-
177
- ```ruby
178
- event = {
179
- event_type: 'charge.succeeded',
180
- customer: CUSTOMER
181
- }
182
- result = plugin.call('on_charge_succeeded', JSON.generate(event))
183
- ```
184
-
185
- This will print:
186
-
187
- ```
188
- Adding Credit {"amount_in_cents"=>1000, "currency"=>"USD"} for customer abcd1234
189
- Sending email {"subject"=>"A gift for you John Smith", "body"=>"You have received $10 in store credi
190
- t!"} to customer abcd1234
191
- ```
1
+ # Extism Ruby Host SDK
2
+
3
+ This repo contains the ruby gem for integrating with the [Extism](https://extism.org/) runtime. Install this library into your host ruby application to run Extism plug-ins.
4
+
5
+ > **Note**: This repo is 1.0 alpha version of the Ruby SDK and we may push breaking changes in between versions until we hit 1.0.0 in December, 2023. However, it is ready to use and you should use this if you're building a new integration. We'd love any feedback on it.
6
+
7
+ ## Installation
8
+
9
+ ### Install the Extism Runtime Dependency
10
+
11
+ For this library, you first need to install the Extism Runtime. You can [download the shared object directly from a release](https://github.com/extism/extism/releases) or use the [Extism CLI](https://github.com/extism/cli) to install it:
12
+
13
+ ```bash
14
+ sudo extism lib install latest
15
+
16
+ #=> Fetching https://github.com/extism/extism/releases/download/v0.5.2/libextism-aarch64-apple-darwin-v0.5.2.tar.gz
17
+ #=> Copying libextism.dylib to /usr/local/lib/libextism.dylib
18
+ #=> Copying extism.h to /usr/local/include/extism.h
19
+ ```
20
+
21
+ > **Note**: This library has breaking changes and targets 1.0 of the runtime. For the time being, install the runtime from our nightly development builds on git: `sudo extism lib install --version git`.
22
+
23
+ ### Install the Gem
24
+
25
+ Add this library to your [Gemfile](https://bundler.io/):
26
+
27
+ ```ruby
28
+ gem 'extism', '1.0.0.pre.rc.3'
29
+ ```
30
+
31
+ Or if installing on the system level:
32
+
33
+ ```
34
+ gem install extism --pre
35
+ ```
36
+
37
+ ## Getting Started
38
+
39
+ This guide should walk you through some of the concepts in Extism and this ruby library.
40
+
41
+ > *Note*: You should be able to follow this guide by copy pasting the code into `irb`.
42
+
43
+ ### Creating A Plug-in
44
+
45
+ The primary concept in Extism is the [plug-in](https://extism.org/docs/concepts/plug-in). You can think of a plug-in as a code module stored in a `.wasm` file.
46
+
47
+ Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web:
48
+
49
+ ```ruby
50
+ # First require the library
51
+ require "extism"
52
+
53
+ url = "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"
54
+ manifest = Extism::Manifest.from_url url
55
+ plugin = Extism::Plugin.new(manifest)
56
+ ```
57
+
58
+ > **Note**: See [the Manifest docs](https://extism.github.io/ruby-sdk/Extism/Manifest.html) as it has a rich schema and a lot of options.
59
+
60
+ ### Calling A Plug-in's Exports
61
+
62
+ This plug-in was written in Rust and it does one thing, it counts vowels in a string. As such, it exposes one "export" function: `count_vowels`. We can call exports using [Extism::Plugin#call](https://extism.github.io/ruby-sdk/Extism/Plugin.html#call-instance_method):
63
+
64
+ ```ruby
65
+ plugin.call("count_vowels", "Hello, World!")
66
+ # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
67
+ ```
68
+
69
+ All exports have a simple interface of bytes-in and bytes-out. This plug-in happens to take a string and return a JSON encoded string with a report of results.
70
+
71
+ ### Plug-in State
72
+
73
+ Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export:
74
+
75
+ ```ruby
76
+ plugin.call("count_vowels", "Hello, World!")
77
+ # => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
78
+ plugin.call("count_vowels", "Hello, World!")
79
+ # => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"}
80
+ ```
81
+
82
+ These variables will persist until this plug-in is freed or you initialize a new one.
83
+
84
+ ### Configuration
85
+
86
+ Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example:
87
+
88
+ ```ruby
89
+ plugin = Extism::Plugin.new(manifest)
90
+ plugin.call("count_vowels", "Yellow, World!")
91
+ # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
92
+
93
+ plugin = Extism::Plugin.new(manifest, config: { vowels: "aeiouyAEIOUY" })
94
+ plugin.call("count_vowels", "Yellow, World!")
95
+ # => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"}
96
+ ```
97
+
98
+ ### Host Functions
99
+
100
+ Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var, let's store it in a persistent key-value store!
101
+
102
+ Wasm can't use our KV store on it's own. This is where [Host Functions](https://extism.org/docs/concepts/host-functions) come in.
103
+
104
+ [Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. They are simply some ruby methods you write which can be passed down and invoked from any language inside the plug-in.
105
+
106
+ Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in:
107
+
108
+ ```ruby
109
+ url = "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"
110
+ manifest = Extism::Manifest.from_url(url)
111
+ ```
112
+
113
+ > *Note*: The source code for this is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs) and is written in rust, but it could be written in any of our PDK languages.
114
+
115
+ Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our its import interface for a KV store.
116
+
117
+ In the ruby sdk, we have a concept for this called a [Host Environment](https://extism.github.io/ruby-sdk/Extism/HostEnvironment.html). An environment is an instance of a class that implements any host functions your plug-in needs.
118
+
119
+ We want to expose two functions to our plugin, `kv_write(key: String, value: Bytes)` which writes a bytes value to a key and `kv_read(key: String) -> Bytes` which reads the bytes at the given `key`.
120
+
121
+ ```ruby
122
+ # pretend this is Redis or something :)
123
+ KV_STORE = {}
124
+
125
+ class KvEnvironment
126
+ include Extism::HostEnvironment
127
+
128
+ # We need to describe the wasm function signature of each host function
129
+ # to register them to this environment
130
+ register_import :kv_read, [Extism::ValType::PTR], [Extism::ValType::PTR]
131
+ register_import :kv_write, [Extism::ValType::PTR, Extism::ValType::PTR], []
132
+
133
+ def kv_read(plugin, inputs, outputs, _user_data)
134
+ key = plugin.input_as_string(inputs.first)
135
+ val = KV_STORE[key] || [0].pack('V') # get 4 LE bytes for 0 default
136
+ puts "Read from key=#{key}"
137
+ plugin.output_string(outputs.first, val)
138
+ end
139
+
140
+ def kv_write(plugin, inputs, _outputs, _user_data)
141
+ key = plugin.input_as_string(inputs.first)
142
+ val = plugin.input_as_string(inputs[1])
143
+ puts "Writing value=#{val.unpack1('V')} from key=#{key}"
144
+ KV_STORE[key] = val
145
+ end
146
+ end
147
+ ```
148
+
149
+ > *Note*: In order to write host functions you should get familiar with the methods on the [Extism::CurrentPlugin](https://extism.github.io/ruby-sdk/Extism/CurrentPlugin.html) class. The `plugin` parameter is an instance of this class.
150
+
151
+ Now we just need to create a new host environment and pass it in when loading the plug-in. Here our environment initializer takes no arguments, but you could imagine putting some customer specific instance variables in there:
152
+
153
+ ```ruby
154
+ env = KvEnvironment.new
155
+ plugin = Extism::Plugin.new(manifest, environment: env)
156
+ ```
157
+
158
+ Now we can invoke the event:
159
+
160
+ ```ruby
161
+ plugin.call("count_vowels", "Hello, World!")
162
+ # => Read from key=count-vowels"
163
+ # => Writing value=3 from key=count-vowels"
164
+ # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
165
+ plugin.call("count_vowels", "Hello, World!")
166
+ # => Read from key=count-vowels"
167
+ # => Writing value=6 from key=count-vowels"
168
+ # => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
169
+ ```
data/Rakefile CHANGED
@@ -1,12 +1,12 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
5
-
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/test_*.rb"]
10
- end
11
-
12
- task default: :test
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -16,22 +16,34 @@ module Extism
16
16
 
17
17
  # Allocates a memory block in the plugin
18
18
  #
19
- # @param amount [Integer] The amount in bytes to allocate
19
+ # @example Allocate 1kB
20
+ # mem = current_plugin.alloc(1_024)
21
+ # current_plugin.free(mem)
22
+ #
23
+ # @param num_bytes [Integer] The amount in bytes to allocate
20
24
  # @return [Extism::Memory] The reference to the freshly allocated memory
21
- def alloc(amount)
22
- offset = LibExtism.extism_current_plugin_memory_alloc(@ptr, amount)
23
- Memory.new(offset, amount)
25
+ def alloc(num_bytes)
26
+ offset = LibExtism.extism_current_plugin_memory_alloc(@ptr, num_bytes)
27
+ Memory.new(offset, num_bytes)
24
28
  end
25
29
 
26
30
  # Frees the memory block
27
31
  #
32
+ # @example
33
+ # mem = current_plugin.alloc(1_024)
34
+ # current_plugin.free(mem)
35
+ #
28
36
  # @param memory [Extism::Memory] The memory object you wish to free
29
37
  # @return [Extism::Memory] The reference to the freshly allocated memory
30
38
  def free(memory)
31
39
  LibExtism.extism_current_plugin_memory_free(@ptr, memory.offset)
32
40
  end
33
41
 
34
- # Gets the memory block at a given offset
42
+ # Gets the memory block at a given offset. Note: try to use input_* and output_* methods where possible.
43
+ #
44
+ # @example
45
+ # mem = current_plugin.memory_at_offset(123456789)
46
+ # current_plugin.free(mem)
35
47
  #
36
48
  # @raise [Extism::Error] if memory block could not be found
37
49
  #
@@ -48,6 +60,10 @@ module Extism
48
60
  #
49
61
  # @raise [Extism::Error] if memory block could not be found
50
62
  #
63
+ # @example
64
+ # param1 = current_plugin.input_as_string(inputs.first)
65
+ # raise "Failed" unless param1 == "First param from plug-in host function call"
66
+ #
51
67
  # @param input [Extism::Val] The input val from the host function
52
68
  # @return [String] raw bytes as a string
53
69
  def input_as_string(input)
@@ -57,10 +73,14 @@ module Extism
57
73
  memory_ptr(mem).read_bytes(mem.len)
58
74
  end
59
75
 
60
- # Gets the input as a string
76
+ # Gets the input as a JSON parsed Hash
61
77
  #
62
78
  # @raise [Extism::Error] if memory block could not be found
63
79
  #
80
+ # @example
81
+ # param1 = current_plugin.input_as_json(inputs.first)
82
+ # raise "Failed" unless param1 == {hello: "world"}
83
+ #
64
84
  # @param input [Extism::Val] The input val from the host function
65
85
  # @return [Hash] The Hash object
66
86
  def input_as_json(input)
@@ -73,6 +93,10 @@ module Extism
73
93
 
74
94
  # Sets string to the return of the host function
75
95
  #
96
+ # @example
97
+ # msg = "A string returned from the host function"
98
+ # current_plugin.output_string(outputs.first, msg)
99
+ #
76
100
  # @raise [Extism::Error] if memory block could not be found
77
101
  #
78
102
  # @param output [Extism::Val] The output val from the host function
@@ -85,6 +109,10 @@ module Extism
85
109
 
86
110
  # Sets json to the return of the host function
87
111
  #
112
+ # @example
113
+ # msg = {hello: "world"}
114
+ # current_plugin.output_json(outputs.first, msg)
115
+ #
88
116
  # @raise [Extism::Error] if memory block could not be found
89
117
  #
90
118
  # @param output [Extism::Val] The output val from the host function
@@ -114,7 +142,7 @@ module Extism
114
142
  private
115
143
 
116
144
  # Returns a raw pointer (absolute to the host) to the given memory block
117
- # Be careful with this. it's not exposed for a reason.
145
+ # **Danger**: Be careful with this. it's not exposed for a reason.
118
146
  # This is a pointer in host memory so it could read outside of the plugin
119
147
  # if manipulated
120
148
  def memory_ptr(mem)
@@ -1,10 +1,32 @@
1
1
  module Extism
2
+ # Represents an "environment" that can be imported to a plug-in
3
+ #
4
+ # @example
5
+ # class MyEnvironment
6
+ # include Extism::HostEnvironment
7
+ # # we need to register each import that the plug-in expects and match the Wasm signature
8
+ # # register_import takes the name, the param types, and the return types
9
+ # register_import :reflect, [Extism::ValType::I64], [Extism::ValType::I64]
10
+ #
11
+ # # reflect just takes a string from the plug-in and reflects it back in return
12
+ # def reflect(plugin, inputs, outputs, _user_data)
13
+ # msg = plugin.input_as_string(inputs.first)
14
+ # plugin.output_string(outputs.first, msg)
15
+ # end
16
+ # end
17
+ #
2
18
  module HostEnvironment
3
19
  def self.included(base)
4
20
  base.extend ClassMethods
5
21
  base.class_variable_set(:@@import_funcs, [])
6
22
  end
7
23
 
24
+ # Creates the host functions to pass to the plug-in on intialization.
25
+ # Used internally by the Plugin initializer
26
+ #
27
+ # @see Extism::Plugin::new
28
+ #
29
+ # @return [Array<Extism::Function>]
8
30
  def host_functions
9
31
  import_funcs = self.class.class_variable_get(:@@import_funcs)
10
32
  import_funcs.map do |f|
@@ -17,12 +39,21 @@ module Extism
17
39
  )
18
40
  end
19
41
  end
20
- end
21
42
 
22
- module ClassMethods
23
- def register_import(func_name, parameters, returns)
24
- import_funcs = class_variable_get(:@@import_funcs)
25
- import_funcs << [func_name, parameters, returns]
43
+ module ClassMethods
44
+ # Register an import by name. You must know the wasm signature
45
+ # of the function to do this.
46
+ #
47
+ # @example
48
+ # register_import :my_func, [Extism::ValType::I64], [Extism::ValType::F64]
49
+ #
50
+ # @param [Symbol | String] func_name The name of the wasm import function. Assumes `env` namespace.
51
+ # @param [Array<Extism::ValType>] parameters The Wasm types of the parameters that the import takes
52
+ # @param [Array<Extism::ValType>] returns The Wasm types of the returns that the import returns. Will usually be just be one of these.
53
+ def register_import(func_name, parameters, returns)
54
+ import_funcs = class_variable_get(:@@import_funcs)
55
+ import_funcs << [func_name, parameters, returns]
56
+ end
26
57
  end
27
58
  end
28
59
  end
@@ -0,0 +1,68 @@
1
+ module Extism
2
+ # The manifest represents a recipe to build a plug-in.
3
+ # It generally consists of a path to one wasm module
4
+ # but could contain more. It also helps you define some
5
+ # options and restrictions on the runtime behavior of the plug-in.
6
+ # See https://extism.org/docs/concepts/manifest for more info.
7
+ class Manifest
8
+ attr_reader :manifest_data
9
+
10
+ # Create a manifest of a single wasm from url.
11
+ # Look at {Manifest#initialize} for an interface with more control
12
+ #
13
+ # @see Manifest::initialize
14
+ # @param [String] url The url to the wasm module
15
+ # @param [String | nil] hash An optional sha256 integrity hash. Defaults to nil
16
+ # @param [String | nil] name An optional name. Defaults to nil
17
+ # @return [Extism::Manifest]
18
+ def self.from_url(url, hash: nil, name: nil)
19
+ wasm = { url: url }
20
+ wasm[:hash] = hash unless hash.nil?
21
+ wasm[:name] = name unless hash.nil?
22
+
23
+ Manifest.new({ wasm: [wasm] })
24
+ end
25
+
26
+ # Create a manifest of a single wasm from file path.
27
+ # Look at {Manifest#initialize} for an interface with more control
28
+ #
29
+ # @see Manifest::initialize
30
+ # @param [String] path The path to the wasm module on disk
31
+ # @param [String | nil] hash An optional sha256 integrity hash. Defaults to nil
32
+ # @param [String | nil] name An optional name. Defaults to nil
33
+ # @return [Extism::Manifest]
34
+ def self.from_path(path, hash: nil, name: nil)
35
+ wasm = { path: path }
36
+ wasm[:hash] = hash unless hash.nil?
37
+ wasm[:name] = name unless hash.nil?
38
+
39
+ Manifest.new({ wasm: [wasm] })
40
+ end
41
+
42
+ # Create a manifest of a single wasm module with raw binary data.
43
+ # Look at {Manifest#initialize} for an interface with more control
44
+ # Consider using a file path instead of the raw wasm binary in memory.
45
+ # The performance is often better letting the runtime load the binary itself.
46
+ #
47
+ # @see Manifest::initialize
48
+ # @param [String] data The binary data of the wasm module
49
+ # @param [String | nil] hash An optional sha256 integrity hash. Defaults to nil
50
+ # @param [String | nil] name An optional name. Defaults to nil
51
+ # @return [Extism::Manifest]
52
+ def self.from_bytes(data, hash: nil, name: nil)
53
+ wasm = { data: data }
54
+ wasm[:hash] = hash unless hash.nil?
55
+ wasm[:name] = name unless hash.nil?
56
+
57
+ Manifest.new({ wasm: [wasm] })
58
+ end
59
+
60
+ # Initialize a manifest
61
+ # See https://extism.org/docs/concepts/manifest for schema
62
+ #
63
+ # @param [Hash] data The Hash data that conforms the Manifest schema
64
+ def initialize(data)
65
+ @manifest_data = data
66
+ end
67
+ end
68
+ end
data/lib/extism/plugin.rb CHANGED
@@ -4,11 +4,29 @@ module Extism
4
4
  class Plugin
5
5
  # Intialize a plugin
6
6
  #
7
- # @param wasm [Hash, String] The manifest as a Hash or WASM binary as a String. See https://extism.org/docs/concepts/manifest/.
7
+ # @example Initialize a plugin from a url
8
+ # manifest = Extism::Manifest.from_url "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"
9
+ # plugin = Extism::Plugin.new(manifest)
10
+ #
11
+ # @example Pass a config object to configure the plug-in
12
+ # plugin = Extism::Plugin.new(manifest, config: { hello: "world" })
13
+ #
14
+ # @example Initalize a plug-in that needs WASI
15
+ # plugin = Extism::Plugin.new(manifest, wasi: true)
16
+ #
17
+ # @param wasm [Hash, String, Manifest] The manifest as a Hash or WASM binary as a String. See https://extism.org/docs/concepts/manifest/.
8
18
  # @param wasi [Boolean] Enable WASI support
9
19
  # @param config [Hash] The plugin config
10
20
  def initialize(wasm, environment: nil, functions: [], wasi: false, config: nil)
11
- wasm = JSON.generate(wasm) if wasm.instance_of?(Hash)
21
+ wasm = case wasm
22
+ when Hash
23
+ JSON.generate(wasm)
24
+ when Manifest
25
+ JSON.generate(wasm.manifest_data)
26
+ else
27
+ wasm
28
+ end
29
+
12
30
  code = FFI::MemoryPointer.new(:char, wasm.bytesize)
13
31
  errmsg = FFI::MemoryPointer.new(:pointer)
14
32
  code.put_bytes(0, wasm)
@@ -46,6 +64,11 @@ module Extism
46
64
 
47
65
  # Call a function by name
48
66
  #
67
+ # @example
68
+ # input = JSON.generate({hello: "world"})
69
+ # result = plugin.call("my_func", input)
70
+ # output = JSON.parse(result)
71
+ #
49
72
  # @param name [String] The function name
50
73
  # @param data [String] The input data for the function
51
74
  # @return [String] The output from the function in String form
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
- module Extism
4
- VERSION = '1.0.0-rc.1'
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Extism
4
+ VERSION = '1.0.0-rc.3'
5
+ end
data/lib/extism/wasm.rb CHANGED
@@ -1,7 +1,13 @@
1
1
  module Extism
2
+ # Extism specific values for Wasm types. Useful when you need to describe
3
+ # something in pure wasm like host function signatures.
4
+ #
5
+ # @example
6
+ # register_import :hostfunc, [Extism::ValType::I32, Extism::ValType::F64], [Extism::ValType::I64]
2
7
  module ValType
3
8
  I32 = 0
4
9
  I64 = 1
10
+ PTR = 1
5
11
  F32 = 2
6
12
  F64 = 3
7
13
  V128 = 4
@@ -9,6 +15,7 @@ module Extism
9
15
  EXTERN_REF = 6
10
16
  end
11
17
 
18
+ # A raw Wasm value. Contains the type and the data
12
19
  class Val
13
20
  def initialize(ptr)
14
21
  @c_val = LibExtism::ExtismVal.new(ptr)
@@ -20,6 +27,8 @@ module Extism
20
27
  :i32
21
28
  when :I64
22
29
  :i64
30
+ when :PTR
31
+ :i64
23
32
  when :F32
24
33
  :f32
25
34
  when :F64
@@ -50,7 +59,10 @@ module Extism
50
59
  end
51
60
  end
52
61
 
53
- # Represents a host function
62
+ # Represents a host function. This is mostly for internal use and you should
63
+ # try to use HostEnvironment instead
64
+ #
65
+ # @see Extism::HostEnvironment
54
66
  class Function
55
67
  # Create a new host function
56
68
  #
@@ -81,6 +93,9 @@ module Extism
81
93
  returns = LibExtism.from_int_array(@returns)
82
94
  @_pointer = LibExtism.extism_function_new(@name, args, @params.length, returns, @returns.length, c_func, free,
83
95
  nil)
96
+ $FUNCTIONS[object_id] = { function: @_pointer}
97
+ ObjectSpace.define_finalizer(self, $FREE_FUNCTION)
98
+ return @_pointer
84
99
  end
85
100
 
86
101
  def c_func
data/lib/extism.rb CHANGED
@@ -1,38 +1,59 @@
1
- require 'ffi'
2
- require 'json'
3
- require_relative './extism/version'
4
- require_relative './extism/plugin'
5
- require_relative './extism/current_plugin'
6
- require_relative './extism/libextism'
7
- require_relative './extism/wasm'
8
- require_relative './extism/host_environment'
9
-
10
- module Extism
11
- class Error < StandardError
12
- end
13
-
14
- # Return the version of Extism
15
- #
16
- # @return [String] The version string of the Extism runtime
17
- def self.extism_version
18
- LibExtism.extism_version
19
- end
20
-
21
- # Set log file and level, this is a global configuration
22
- # @param name [String] The path to the logfile
23
- # @param level [String] The log level. One of {"debug", "error", "info", "trace" }
24
- def self.set_log_file(name, level = nil)
25
- LibExtism.extism_log_file(name, level)
26
- end
27
-
28
- $PLUGINS = {}
29
- $FREE_PLUGIN = proc { |ptr|
30
- x = $PLUGINS[ptr]
31
- unless x.nil?
32
- LibExtism.extism_plugin_free(x[:plugin])
33
- $PLUGINS.delete(ptr)
34
- end
35
- }
36
-
37
- Memory = Struct.new(:offset, :len)
38
- end
1
+ require 'ffi'
2
+ require 'json'
3
+ require_relative './extism/manifest'
4
+ require_relative './extism/version'
5
+ require_relative './extism/plugin'
6
+ require_relative './extism/current_plugin'
7
+ require_relative './extism/libextism'
8
+ require_relative './extism/wasm'
9
+ require_relative './extism/host_environment'
10
+
11
+ module Extism
12
+ class Error < StandardError
13
+ end
14
+
15
+ # Return the version of Extism
16
+ #
17
+ # @return [String] The version string of the Extism runtime
18
+ def self.extism_version
19
+ LibExtism.extism_version
20
+ end
21
+
22
+ # Set log file and level, this is a global configuration
23
+ # @param name [String] The path to the logfile
24
+ # @param level [String] The log level. One of {"debug", "error", "info", "trace" }
25
+ def self.set_log_file(name, level = nil)
26
+ LibExtism.extism_log_file(name, level)
27
+ end
28
+
29
+ $PLUGINS = {}
30
+ $FREE_PLUGIN = proc { |ptr|
31
+ x = $PLUGINS[ptr]
32
+ unless x.nil?
33
+ LibExtism.extism_plugin_free(x[:plugin])
34
+ $PLUGINS.delete(ptr)
35
+ end
36
+ }
37
+
38
+
39
+ $FUNCTIONS = {}
40
+ $FREE_FUNCTION = proc { |ptr|
41
+ x = $FUNCTIONS[ptr]
42
+ unless x.nil?
43
+ LibExtism.extism_function_free(x[:function])
44
+ $FUNCTIONS.delete(ptr)
45
+ end
46
+ }
47
+
48
+ # Represents a "block" of memory in Extism.
49
+ # This memory is in the communication buffer b/w the
50
+ # guest in the host and technically lives in host memory.
51
+ class Memory
52
+ attr_reader :offset, :len
53
+
54
+ def initialize(offset, len)
55
+ @offset = offset
56
+ @len = len
57
+ end
58
+ end
59
+ end
Binary file
Binary file
data/wasm/reflect.wasm CHANGED
Binary file
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extism
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.rc.1
4
+ version: 1.0.0.pre.rc.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - zach
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-21 00:00:00.000000000 Z
11
+ date: 2023-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -38,16 +38,17 @@ files:
38
38
  - Makefile
39
39
  - README.md
40
40
  - Rakefile
41
- - example.rb
42
41
  - lib/extism.rb
43
42
  - lib/extism/current_plugin.rb
44
43
  - lib/extism/host_environment.rb
45
44
  - lib/extism/libextism.rb
45
+ - lib/extism/manifest.rb
46
46
  - lib/extism/plugin.rb
47
47
  - lib/extism/version.rb
48
48
  - lib/extism/wasm.rb
49
49
  - sig/extism.rbs
50
50
  - wasm/count_vowels.wasm
51
+ - wasm/count_vowels_kvstore.wasm
51
52
  - wasm/reflect.wasm
52
53
  - wasm/store_credit.wasm
53
54
  homepage: https://github.com/extism/extism
data/example.rb DELETED
@@ -1,11 +0,0 @@
1
- require "./lib/extism"
2
- require "json"
3
-
4
- manifest = {
5
- :wasm => [{ :path => "../wasm/code.wasm" }],
6
- }
7
-
8
- plugin = Extism::Plugin.new(manifest)
9
- res = JSON.parse(plugin.call("count_vowels", ARGV[0] || "this is a test"))
10
-
11
- puts res["count"]