extism 0.5.0 → 1.0.0.pre.rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -2
- data/Gemfile +7 -6
- data/Gemfile.lock +43 -0
- data/LICENSE +11 -0
- data/Makefile +39 -33
- data/README.md +191 -0
- data/Rakefile +12 -12
- data/lib/extism/current_plugin.rb +125 -0
- data/lib/extism/host_environment.rb +28 -0
- data/lib/extism/libextism.rb +77 -0
- data/lib/extism/plugin.rb +86 -0
- data/lib/extism/version.rb +1 -1
- data/lib/extism/wasm.rb +102 -0
- data/lib/extism.rb +16 -245
- data/sig/extism.rbs +4 -4
- data/wasm/count_vowels.wasm +0 -0
- data/wasm/reflect.wasm +0 -0
- data/wasm/store_credit.wasm +0 -0
- metadata +15 -6
- data/GETTING_STARTED.md +0 -35
- data/user_code.wasm +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed731bd8ff622c67eee229b14588ce7e6311337d5b3c888aca2e116f77758b4b
|
4
|
+
data.tar.gz: e0bea72460c008c16dc4747c39ebb09643c48ddecf6c5a5efc0bae754609ebdb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81e70c7e6b7b649733f68b4c284104155ccec044f9604882b99cb54b626a777aa4e7dcfdc51a224b00600c6ee705f4a9b5761c85b5fb73ebff542be94ae5627c
|
7
|
+
data.tar.gz: 39ee1905ea234277c040698eae6098dfcc5d0ffac53185598536ac915dc4999c316bdab88d5ccb716895bf006d2cb25fc6b636640a5402b5b8aaf5b658653fd7
|
data/.yardopts
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
--readme
|
2
|
-
- GETTING_STARTED.md
|
1
|
+
--readme README.md
|
data/Gemfile
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source 'https://rubygems.org'
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in extism.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
gem
|
9
|
-
gem
|
8
|
+
gem 'ffi', '~> 1.15.5'
|
9
|
+
gem 'rake', '~> 13.0'
|
10
10
|
|
11
11
|
group :development do
|
12
|
-
gem
|
13
|
-
gem
|
14
|
-
gem
|
12
|
+
gem 'debug'
|
13
|
+
gem 'minitest', '~> 5.20.0'
|
14
|
+
gem 'rufo', '~> 0.13.0'
|
15
|
+
gem 'yard', '~> 0.9.28'
|
15
16
|
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
extism (1.0.0.pre.rc.1)
|
5
|
+
ffi (>= 1.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
debug (1.8.0)
|
11
|
+
irb (>= 1.5.0)
|
12
|
+
reline (>= 0.3.1)
|
13
|
+
ffi (1.15.5)
|
14
|
+
io-console (0.6.0)
|
15
|
+
irb (1.8.1)
|
16
|
+
rdoc
|
17
|
+
reline (>= 0.3.8)
|
18
|
+
minitest (5.20.0)
|
19
|
+
psych (5.1.0)
|
20
|
+
stringio
|
21
|
+
rake (13.0.6)
|
22
|
+
rdoc (6.5.0)
|
23
|
+
psych (>= 4.0.0)
|
24
|
+
reline (0.3.8)
|
25
|
+
io-console (~> 0.5)
|
26
|
+
rufo (0.13.0)
|
27
|
+
stringio (3.0.8)
|
28
|
+
yard (0.9.34)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
arm64-darwin-22
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
debug
|
35
|
+
extism!
|
36
|
+
ffi (~> 1.15.5)
|
37
|
+
minitest (~> 5.20.0)
|
38
|
+
rake (~> 13.0)
|
39
|
+
rufo (~> 0.13.0)
|
40
|
+
yard (~> 0.9.28)
|
41
|
+
|
42
|
+
BUNDLED WITH
|
43
|
+
2.4.10
|
data/LICENSE
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Copyright 2022 Dylibso, Inc.
|
2
|
+
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
4
|
+
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
6
|
+
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
8
|
+
|
9
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
10
|
+
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/Makefile
CHANGED
@@ -1,33 +1,39 @@
|
|
1
|
-
RUBYGEMS_API_KEY ?=
|
2
|
-
|
3
|
-
.PHONY: prepare test
|
4
|
-
|
5
|
-
prepare:
|
6
|
-
bundle install
|
7
|
-
bundle binstubs --all
|
8
|
-
|
9
|
-
test: prepare
|
10
|
-
bundle exec rake test
|
11
|
-
|
12
|
-
clean:
|
13
|
-
rm -f extism-*.gem
|
14
|
-
|
15
|
-
publish-local: clean prepare
|
16
|
-
gem build extism.gemspec
|
17
|
-
gem push extism-*.gem
|
18
|
-
|
19
|
-
publish: clean prepare
|
20
|
-
gem build extism.gemspec
|
21
|
-
GEM_HOST_API_KEY=$(RUBYGEMS_API_KEY) gem push extism-*.gem
|
22
|
-
|
23
|
-
lint:
|
24
|
-
bundle exec rufo --check .
|
25
|
-
|
26
|
-
format:
|
27
|
-
bundle exec rufo .
|
28
|
-
|
29
|
-
docs:
|
30
|
-
bundle exec yard
|
31
|
-
|
32
|
-
show-docs: docs
|
33
|
-
open doc/index.html
|
1
|
+
RUBYGEMS_API_KEY ?=
|
2
|
+
|
3
|
+
.PHONY: prepare test
|
4
|
+
|
5
|
+
prepare:
|
6
|
+
bundle install
|
7
|
+
bundle binstubs --all
|
8
|
+
|
9
|
+
test: prepare
|
10
|
+
bundle exec rake test
|
11
|
+
|
12
|
+
clean:
|
13
|
+
rm -f extism-*.gem
|
14
|
+
|
15
|
+
publish-local: clean prepare
|
16
|
+
gem build extism.gemspec
|
17
|
+
gem push extism-*.gem
|
18
|
+
|
19
|
+
publish: clean prepare
|
20
|
+
gem build extism.gemspec
|
21
|
+
GEM_HOST_API_KEY=$(RUBYGEMS_API_KEY) gem push extism-*.gem
|
22
|
+
|
23
|
+
lint:
|
24
|
+
bundle exec rufo --check .
|
25
|
+
|
26
|
+
format:
|
27
|
+
bundle exec rufo .
|
28
|
+
|
29
|
+
docs:
|
30
|
+
bundle exec yard
|
31
|
+
|
32
|
+
show-docs: docs
|
33
|
+
open doc/index.html
|
34
|
+
|
35
|
+
seed:
|
36
|
+
curl -L https://github.com/extism/plugins/releases/latest/download/count_vowels.debug.wasm > wasm/count_vowels.wasm
|
37
|
+
curl -L https://github.com/extism/plugins/releases/latest/download/reflect.debug.wasm > wasm/reflect.wasm
|
38
|
+
curl -L https://github.com/extism/plugins/releases/latest/download/store_credit.debug.wasm > wasm/store_credit.wasm
|
39
|
+
|
data/README.md
ADDED
@@ -0,0 +1,191 @@
|
|
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
|
+
```
|
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
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Extism
|
2
|
+
# Represents a reference to a plugin in a host function
|
3
|
+
# Use this class to read and write to the memory of the plugin
|
4
|
+
# These methods allow you to get data in and out of the plugin
|
5
|
+
# in a host function
|
6
|
+
class CurrentPlugin
|
7
|
+
# let's not let people construct these since it comes from a pointer
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
# Initialize a CurrentPlugin given an pointer
|
11
|
+
#
|
12
|
+
# @param ptr [FFI::Pointer] the raw pointer to the plugin
|
13
|
+
def initialize(ptr)
|
14
|
+
@ptr = ptr
|
15
|
+
end
|
16
|
+
|
17
|
+
# Allocates a memory block in the plugin
|
18
|
+
#
|
19
|
+
# @param amount [Integer] The amount in bytes to allocate
|
20
|
+
# @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)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Frees the memory block
|
27
|
+
#
|
28
|
+
# @param memory [Extism::Memory] The memory object you wish to free
|
29
|
+
# @return [Extism::Memory] The reference to the freshly allocated memory
|
30
|
+
def free(memory)
|
31
|
+
LibExtism.extism_current_plugin_memory_free(@ptr, memory.offset)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Gets the memory block at a given offset
|
35
|
+
#
|
36
|
+
# @raise [Extism::Error] if memory block could not be found
|
37
|
+
#
|
38
|
+
# @param offset [Integer] The offset pointer to the memory. This is relative to the plugin not the host.
|
39
|
+
# @return [Extism::Memory] The reference to the memory block if found
|
40
|
+
def memory_at_offset(offset)
|
41
|
+
len = LibExtism.extism_current_plugin_memory_length(@ptr, offset)
|
42
|
+
raise Extism::Error, "Could not find memory block at offset #{offset}" if len.zero?
|
43
|
+
|
44
|
+
Memory.new(offset, len)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Gets the input as a string
|
48
|
+
#
|
49
|
+
# @raise [Extism::Error] if memory block could not be found
|
50
|
+
#
|
51
|
+
# @param input [Extism::Val] The input val from the host function
|
52
|
+
# @return [String] raw bytes as a string
|
53
|
+
def input_as_string(input)
|
54
|
+
raise ArgumentError, 'input is not an Extism::Val' unless input.instance_of? Extism::Val
|
55
|
+
|
56
|
+
mem = memory_at_offset(input.value)
|
57
|
+
memory_ptr(mem).read_bytes(mem.len)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Gets the input as a string
|
61
|
+
#
|
62
|
+
# @raise [Extism::Error] if memory block could not be found
|
63
|
+
#
|
64
|
+
# @param input [Extism::Val] The input val from the host function
|
65
|
+
# @return [Hash] The Hash object
|
66
|
+
def input_as_json(input)
|
67
|
+
raise ArgumentError, 'input is not an Extism::Val' unless input.instance_of? Extism::Val
|
68
|
+
|
69
|
+
mem = memory_at_offset(input.value)
|
70
|
+
str = memory_ptr(mem).read_bytes(mem.len)
|
71
|
+
JSON.parse(str)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sets string to the return of the host function
|
75
|
+
#
|
76
|
+
# @raise [Extism::Error] if memory block could not be found
|
77
|
+
#
|
78
|
+
# @param output [Extism::Val] The output val from the host function
|
79
|
+
# @param bytes [String] The bytes to set
|
80
|
+
def output_string(output, bytes)
|
81
|
+
mem = alloc(bytes.length)
|
82
|
+
memory_ptr(mem).put_bytes(0, bytes)
|
83
|
+
set_return(output, mem.offset)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Sets json to the return of the host function
|
87
|
+
#
|
88
|
+
# @raise [Extism::Error] if memory block could not be found
|
89
|
+
#
|
90
|
+
# @param output [Extism::Val] The output val from the host function
|
91
|
+
# @param obj [Hash] The hash object to turn to JSON
|
92
|
+
def output_json(output, obj)
|
93
|
+
bytes = JSON.generate(obj)
|
94
|
+
mem = alloc(bytes.length)
|
95
|
+
memory_ptr(mem).put_bytes(0, bytes)
|
96
|
+
set_return(output, mem.offset)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Sets the return value parameter
|
100
|
+
#
|
101
|
+
# @raise [Extism::Error] if memory block could not be found
|
102
|
+
#
|
103
|
+
# @param output [Extism::Val] The output val from the host function
|
104
|
+
# @param value [Integer | Float] The i32 value
|
105
|
+
def set_return(output, value)
|
106
|
+
case output.type
|
107
|
+
when :i32, :i64, :f32, :f64
|
108
|
+
output.value = value
|
109
|
+
else
|
110
|
+
raise ArgumentError, "Don't know how to set output type #{output.type}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# 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.
|
118
|
+
# This is a pointer in host memory so it could read outside of the plugin
|
119
|
+
# if manipulated
|
120
|
+
def memory_ptr(mem)
|
121
|
+
plugin_ptr = LibExtism.extism_current_plugin_memory(@ptr)
|
122
|
+
FFI::Pointer.new(plugin_ptr.address + mem.offset)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Extism
|
2
|
+
module HostEnvironment
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.class_variable_set(:@@import_funcs, [])
|
6
|
+
end
|
7
|
+
|
8
|
+
def host_functions
|
9
|
+
import_funcs = self.class.class_variable_get(:@@import_funcs)
|
10
|
+
import_funcs.map do |f|
|
11
|
+
name, params, returns = f
|
12
|
+
Extism::Function.new(
|
13
|
+
name.to_s,
|
14
|
+
params,
|
15
|
+
returns,
|
16
|
+
method(name).to_proc
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
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]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Extism
|
2
|
+
# Private module used to interface with the Extism runtime.
|
3
|
+
# *Warning*: Do not use or rely on this directly
|
4
|
+
# improperly using this interface may enable exploits and the interface
|
5
|
+
# might change over time.
|
6
|
+
module LibExtism
|
7
|
+
extend FFI::Library
|
8
|
+
ffi_lib 'extism'
|
9
|
+
|
10
|
+
def self.from_int_array(ruby_array)
|
11
|
+
ptr = FFI::MemoryPointer.new(:int, ruby_array.length)
|
12
|
+
ptr.write_array_of_int(ruby_array)
|
13
|
+
ptr
|
14
|
+
end
|
15
|
+
|
16
|
+
typedef :uint64, :ExtismMemoryHandle
|
17
|
+
typedef :uint64, :ExtismSize
|
18
|
+
|
19
|
+
enum :ExtismValType, %i[I32 I64 F32 F64 V128 FuncRef ExternRef]
|
20
|
+
|
21
|
+
class ExtismValUnion < FFI::Union
|
22
|
+
layout :i32, :int32,
|
23
|
+
:i64, :int64,
|
24
|
+
:f32, :float,
|
25
|
+
:f64, :double
|
26
|
+
end
|
27
|
+
|
28
|
+
class ExtismVal < FFI::Struct
|
29
|
+
layout :t, :ExtismValType,
|
30
|
+
:v, ExtismValUnion
|
31
|
+
end
|
32
|
+
|
33
|
+
class ExtismFunction < FFI::Struct
|
34
|
+
layout :name, :string,
|
35
|
+
:inputs, :pointer,
|
36
|
+
:n_inputs, :uint64,
|
37
|
+
:outputs, :pointer,
|
38
|
+
:n_outputs, :uint64,
|
39
|
+
:data, :pointer
|
40
|
+
end
|
41
|
+
|
42
|
+
callback :ExtismFunctionType, [
|
43
|
+
:pointer, # plugin
|
44
|
+
:pointer, # inputs
|
45
|
+
:ExtismSize, # n_inputs
|
46
|
+
:pointer, # outputs
|
47
|
+
:ExtismSize, # n_outputs
|
48
|
+
:pointer # user_data
|
49
|
+
], :void
|
50
|
+
|
51
|
+
callback :ExtismFreeFunctionType, [], :void
|
52
|
+
|
53
|
+
attach_function :extism_plugin_id, [:pointer], :pointer
|
54
|
+
attach_function :extism_current_plugin_memory, [:pointer], :pointer
|
55
|
+
attach_function :extism_current_plugin_memory_alloc, %i[pointer ExtismSize], :ExtismMemoryHandle
|
56
|
+
attach_function :extism_current_plugin_memory_length, %i[pointer ExtismMemoryHandle], :ExtismSize
|
57
|
+
attach_function :extism_current_plugin_memory_free, %i[pointer ExtismMemoryHandle], :void
|
58
|
+
attach_function :extism_function_new,
|
59
|
+
%i[string pointer ExtismSize pointer ExtismSize ExtismFunctionType ExtismFreeFunctionType pointer], :pointer
|
60
|
+
attach_function :extism_function_free, [:pointer], :void
|
61
|
+
attach_function :extism_function_set_namespace, %i[pointer string], :void
|
62
|
+
attach_function :extism_plugin_new, %i[pointer ExtismSize pointer ExtismSize bool pointer], :pointer
|
63
|
+
attach_function :extism_plugin_new_error_free, [:pointer], :void
|
64
|
+
attach_function :extism_plugin_free, [:pointer], :void
|
65
|
+
attach_function :extism_plugin_cancel_handle, [:pointer], :pointer
|
66
|
+
attach_function :extism_plugin_cancel, [:pointer], :bool
|
67
|
+
attach_function :extism_plugin_config, %i[pointer pointer ExtismSize], :bool
|
68
|
+
attach_function :extism_plugin_function_exists, %i[pointer string], :bool
|
69
|
+
attach_function :extism_plugin_call, %i[pointer string pointer ExtismSize], :int32
|
70
|
+
attach_function :extism_error, [:pointer], :string
|
71
|
+
attach_function :extism_plugin_error, [:pointer], :string
|
72
|
+
attach_function :extism_plugin_output_length, [:pointer], :ExtismSize
|
73
|
+
attach_function :extism_plugin_output_data, [:pointer], :pointer
|
74
|
+
attach_function :extism_log_file, %i[string string], :bool
|
75
|
+
attach_function :extism_version, [], :string
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Extism
|
2
|
+
# A Plugin represents an instance of your WASM program
|
3
|
+
# created from the given manifest.
|
4
|
+
class Plugin
|
5
|
+
# Intialize a plugin
|
6
|
+
#
|
7
|
+
# @param wasm [Hash, String] The manifest as a Hash or WASM binary as a String. See https://extism.org/docs/concepts/manifest/.
|
8
|
+
# @param wasi [Boolean] Enable WASI support
|
9
|
+
# @param config [Hash] The plugin config
|
10
|
+
def initialize(wasm, environment: nil, functions: [], wasi: false, config: nil)
|
11
|
+
wasm = JSON.generate(wasm) if wasm.instance_of?(Hash)
|
12
|
+
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
|
13
|
+
errmsg = FFI::MemoryPointer.new(:pointer)
|
14
|
+
code.put_bytes(0, wasm)
|
15
|
+
if functions.empty? && environment
|
16
|
+
unless environment.respond_to?(:host_functions)
|
17
|
+
raise ArgumentError 'environment should implement host_functions method'
|
18
|
+
end
|
19
|
+
|
20
|
+
functions = environment.host_functions
|
21
|
+
end
|
22
|
+
funcs_ptr = FFI::MemoryPointer.new(LibExtism::ExtismFunction)
|
23
|
+
funcs_ptr.write_array_of_pointer(functions.map { |f| f.send(:pointer) })
|
24
|
+
@plugin = LibExtism.extism_plugin_new(code, wasm.bytesize, funcs_ptr, functions.length, wasi, errmsg)
|
25
|
+
if @plugin.null?
|
26
|
+
err = errmsg.read_pointer.read_string
|
27
|
+
LibExtism.extism_plugin_new_error_free errmsg.read_pointer
|
28
|
+
raise Error, err
|
29
|
+
end
|
30
|
+
$PLUGINS[object_id] = { plugin: @plugin }
|
31
|
+
ObjectSpace.define_finalizer(self, $FREE_PLUGIN)
|
32
|
+
return unless !config.nil? and @plugin.null?
|
33
|
+
|
34
|
+
s = JSON.generate(config)
|
35
|
+
ptr = FFI::MemoryPointer.from_string(s)
|
36
|
+
LibExtism.extism_plugin_config(@plugin, ptr, s.bytesize)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if a function exists
|
40
|
+
#
|
41
|
+
# @param name [String] The name of the function
|
42
|
+
# @return [Boolean] Returns true if function exists
|
43
|
+
def has_function?(name)
|
44
|
+
LibExtism.extism_plugin_function_exists(@plugin, name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Call a function by name
|
48
|
+
#
|
49
|
+
# @param name [String] The function name
|
50
|
+
# @param data [String] The input data for the function
|
51
|
+
# @return [String] The output from the function in String form
|
52
|
+
def call(name, data, &block)
|
53
|
+
# If no block was passed then use Pointer::read_string
|
54
|
+
block ||= ->(buf, len) { buf.read_string(len) }
|
55
|
+
input = FFI::MemoryPointer.from_string(data)
|
56
|
+
rc = LibExtism.extism_plugin_call(@plugin, name, input, data.bytesize)
|
57
|
+
if rc != 0
|
58
|
+
err = LibExtism.extism_plugin_error(@plugin)
|
59
|
+
raise Error, 'extism_call failed' if err&.empty?
|
60
|
+
|
61
|
+
raise Error, err
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
out_len = LibExtism.extism_plugin_output_length(@plugin)
|
66
|
+
buf = LibExtism.extism_plugin_output_data(@plugin)
|
67
|
+
block.call(buf, out_len)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Free a plugin, this should be called when the plugin is no longer needed
|
71
|
+
#
|
72
|
+
# @return [void]
|
73
|
+
def free
|
74
|
+
return if @plugin.null?
|
75
|
+
|
76
|
+
$PLUGINS.delete(object_id)
|
77
|
+
LibExtism.extism_plugin_free(@plugin)
|
78
|
+
@plugin = nil
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get a CancelHandle for a plugin
|
82
|
+
def cancel_handle
|
83
|
+
CancelHandle.new(LibExtism.extism_plugin_cancel_handle(@plugin))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/extism/version.rb
CHANGED
data/lib/extism/wasm.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Extism
|
2
|
+
module ValType
|
3
|
+
I32 = 0
|
4
|
+
I64 = 1
|
5
|
+
F32 = 2
|
6
|
+
F64 = 3
|
7
|
+
V128 = 4
|
8
|
+
FUNC_REF = 5
|
9
|
+
EXTERN_REF = 6
|
10
|
+
end
|
11
|
+
|
12
|
+
class Val
|
13
|
+
def initialize(ptr)
|
14
|
+
@c_val = LibExtism::ExtismVal.new(ptr)
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
case @c_val[:t]
|
19
|
+
when :I32
|
20
|
+
:i32
|
21
|
+
when :I64
|
22
|
+
:i64
|
23
|
+
when :F32
|
24
|
+
:f32
|
25
|
+
when :F64
|
26
|
+
:f64
|
27
|
+
else
|
28
|
+
raise "Unsupported wasm value type #{type}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def value
|
33
|
+
@c_val[:v][type]
|
34
|
+
end
|
35
|
+
|
36
|
+
def value=(val)
|
37
|
+
@c_val[:v][type] = val
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# A CancelHandle can be used to cancel a running plugin from another thread
|
42
|
+
class CancelHandle
|
43
|
+
def initialize(handle)
|
44
|
+
@handle = handle
|
45
|
+
end
|
46
|
+
|
47
|
+
# Cancel the plugin used to generate the handle
|
48
|
+
def cancel
|
49
|
+
LibExtism.extism_plugin_cancel(@handle)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Represents a host function
|
54
|
+
class Function
|
55
|
+
# Create a new host function
|
56
|
+
#
|
57
|
+
# @param name [String] Must match the import name in Wasm. Doesn't include namespace. All extism host functions are in the env name space
|
58
|
+
# @param params [Array[Extism::ValType]] An array of val types matching the import's params
|
59
|
+
# @param returns [Array[Extism::ValType]] An array of val types matching the import returns
|
60
|
+
# @param func_proc [Proc] A proc that will be executed when the host function is executed
|
61
|
+
# @param user_data [Object] Any reference to object you want to be passed back to you when the func is invoked
|
62
|
+
# @param on_free [Proc] A proc triggered when this function is freed by the runtime. Not guaranteed to trigger.
|
63
|
+
def initialize(name, params, returns, func_proc, user_data: nil, on_free: nil)
|
64
|
+
@name = name
|
65
|
+
@params = params
|
66
|
+
@returns = returns
|
67
|
+
@func = func_proc
|
68
|
+
@user_data = user_data
|
69
|
+
@on_free = on_free
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Gets the pointer to this function.
|
75
|
+
# Warning: This should not be used
|
76
|
+
def pointer
|
77
|
+
return @_pointer if @_pointer
|
78
|
+
|
79
|
+
free = @on_free || proc {}
|
80
|
+
args = LibExtism.from_int_array(@params)
|
81
|
+
returns = LibExtism.from_int_array(@returns)
|
82
|
+
@_pointer = LibExtism.extism_function_new(@name, args, @params.length, returns, @returns.length, c_func, free,
|
83
|
+
nil)
|
84
|
+
end
|
85
|
+
|
86
|
+
def c_func
|
87
|
+
@c_func ||= proc do |plugin_ptr, inputs_ptr, inputs_size, outputs_ptr, outputs_size, _data_ptr|
|
88
|
+
current_plugin = Extism::CurrentPlugin.send(:new, plugin_ptr)
|
89
|
+
val_struct_size = LibExtism::ExtismVal.size
|
90
|
+
|
91
|
+
inputs = (0...inputs_size).map do |i|
|
92
|
+
Val.new(inputs_ptr + i * val_struct_size)
|
93
|
+
end
|
94
|
+
outputs = (0...outputs_size).map do |i|
|
95
|
+
Val.new(outputs_ptr + i * val_struct_size)
|
96
|
+
end
|
97
|
+
|
98
|
+
@func.call(current_plugin, inputs, outputs, @user_data)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/extism.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require_relative
|
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'
|
4
9
|
|
5
10
|
module Extism
|
6
11
|
class Error < StandardError
|
@@ -10,258 +15,24 @@ module Extism
|
|
10
15
|
#
|
11
16
|
# @return [String] The version string of the Extism runtime
|
12
17
|
def self.extism_version
|
13
|
-
|
18
|
+
LibExtism.extism_version
|
14
19
|
end
|
15
20
|
|
16
21
|
# Set log file and level, this is a global configuration
|
17
22
|
# @param name [String] The path to the logfile
|
18
23
|
# @param level [String] The log level. One of {"debug", "error", "info", "trace" }
|
19
24
|
def self.set_log_file(name, level = nil)
|
20
|
-
|
21
|
-
level = FFI::MemoryPointer::from_string(level)
|
22
|
-
end
|
23
|
-
C.extism_log_file(name, level)
|
25
|
+
LibExtism.extism_log_file(name, level)
|
24
26
|
end
|
25
27
|
|
26
28
|
$PLUGINS = {}
|
27
|
-
$FREE_PLUGIN = proc { |
|
28
|
-
x = $PLUGINS[
|
29
|
-
|
30
|
-
|
31
|
-
$PLUGINS.delete(
|
32
|
-
end
|
33
|
-
}
|
34
|
-
|
35
|
-
$CONTEXTS = {}
|
36
|
-
$FREE_CONTEXT = proc { |id|
|
37
|
-
x = $CONTEXTS[id]
|
38
|
-
if !x.nil?
|
39
|
-
C.extism_context_free($CONTEXTS[id])
|
40
|
-
$CONTEXTS.delete(id)
|
29
|
+
$FREE_PLUGIN = proc { |ptr|
|
30
|
+
x = $PLUGINS[ptr]
|
31
|
+
unless x.nil?
|
32
|
+
LibExtism.extism_plugin_free(x[:plugin])
|
33
|
+
$PLUGINS.delete(ptr)
|
41
34
|
end
|
42
35
|
}
|
43
36
|
|
44
|
-
|
45
|
-
# is where your plugins live. Freeing the context
|
46
|
-
# frees all of the plugins in its scope.
|
47
|
-
#
|
48
|
-
# @example Create and free a context
|
49
|
-
# ctx = Extism::Context.new
|
50
|
-
# plugin = ctx.plugin(my_manifest)
|
51
|
-
# puts plugin.call("my_func", "my-input")
|
52
|
-
# ctx.free # frees any plugins
|
53
|
-
#
|
54
|
-
# @example Use with_context to auto-free
|
55
|
-
# Extism.with_context do |ctx|
|
56
|
-
# plugin = ctx.plugin(my_manifest)
|
57
|
-
# puts plugin.call("my_func", "my-input")
|
58
|
-
# end # frees context after exiting this block
|
59
|
-
#
|
60
|
-
# @attr_reader pointer [FFI::Pointer] Pointer to the Extism context. *Used internally*.
|
61
|
-
class Context
|
62
|
-
attr_reader :pointer
|
63
|
-
|
64
|
-
# Initialize a new context
|
65
|
-
def initialize
|
66
|
-
@pointer = C.extism_context_new()
|
67
|
-
$CONTEXTS[self.object_id] = @pointer
|
68
|
-
ObjectSpace.define_finalizer(self, $FREE_CONTEXT)
|
69
|
-
end
|
70
|
-
|
71
|
-
# Remove all registered plugins in this context
|
72
|
-
# @return [void]
|
73
|
-
def reset
|
74
|
-
C.extism_context_reset(@pointer)
|
75
|
-
end
|
76
|
-
|
77
|
-
# Free the context, this should be called when it is no longer needed
|
78
|
-
# @return [void]
|
79
|
-
def free
|
80
|
-
return if @pointer.nil?
|
81
|
-
|
82
|
-
$CONTEXTS.delete(self.object_id)
|
83
|
-
C.extism_context_free(@pointer)
|
84
|
-
@pointer = nil
|
85
|
-
end
|
86
|
-
|
87
|
-
# Create a new plugin from a WASM module or JSON encoded manifest
|
88
|
-
#
|
89
|
-
# @see Plugin#new
|
90
|
-
# @param wasm [Hash, String] The manifest for the plugin. See https://extism.org/docs/concepts/manifest/.
|
91
|
-
# @param wasi [Boolean] Enable WASI support
|
92
|
-
# @param config [Hash] The plugin config
|
93
|
-
# @return [Plugin]
|
94
|
-
def plugin(wasm, wasi = false, config = nil)
|
95
|
-
Plugin.new(wasm, wasi, config, self)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# A context manager to create contexts and ensure that they get freed.
|
100
|
-
#
|
101
|
-
# @example Use with_context to auto-free
|
102
|
-
# Extism.with_context do |ctx|
|
103
|
-
# plugin = ctx.plugin(my_manifest)
|
104
|
-
# puts plugin.call("my_func", "my-input")
|
105
|
-
# end # frees context after exiting this block
|
106
|
-
#
|
107
|
-
# @yield [ctx] Yields the created Context
|
108
|
-
# @return [Object] returns whatever your block returns
|
109
|
-
def self.with_context(&block)
|
110
|
-
ctx = Context.new
|
111
|
-
begin
|
112
|
-
x = block.call(ctx)
|
113
|
-
return x
|
114
|
-
ensure
|
115
|
-
ctx.free
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# A CancelHandle can be used to cancel a running plugin from another thread
|
120
|
-
class CancelHandle
|
121
|
-
def initialize(handle)
|
122
|
-
@handle = handle
|
123
|
-
end
|
124
|
-
|
125
|
-
# Cancel the plugin used to generate the handle
|
126
|
-
def cancel
|
127
|
-
return C.extism_plugin_cancel(@handle)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# A Plugin represents an instance of your WASM program from the given manifest.
|
132
|
-
class Plugin
|
133
|
-
# Intialize a plugin
|
134
|
-
#
|
135
|
-
# @param wasm [Hash, String] The manifest or WASM binary. See https://extism.org/docs/concepts/manifest/.
|
136
|
-
# @param wasi [Boolean] Enable WASI support
|
137
|
-
# @param config [Hash] The plugin config
|
138
|
-
# @param context [Context] The context to manager this plugin
|
139
|
-
def initialize(wasm, wasi = false, config = nil, context = nil)
|
140
|
-
if context.nil? then
|
141
|
-
context = Context.new
|
142
|
-
end
|
143
|
-
@context = context
|
144
|
-
if wasm.class == Hash
|
145
|
-
wasm = JSON.generate(wasm)
|
146
|
-
end
|
147
|
-
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
|
148
|
-
code.put_bytes(0, wasm)
|
149
|
-
@plugin = C.extism_plugin_new(context.pointer, code, wasm.bytesize, nil, 0, wasi)
|
150
|
-
if @plugin < 0
|
151
|
-
err = C.extism_error(@context.pointer, -1)
|
152
|
-
if err&.empty?
|
153
|
-
raise Error.new "extism_plugin_new failed"
|
154
|
-
else
|
155
|
-
raise Error.new err
|
156
|
-
end
|
157
|
-
end
|
158
|
-
$PLUGINS[self.object_id] = { :plugin => @plugin, :context => context }
|
159
|
-
ObjectSpace.define_finalizer(self, $FREE_PLUGIN)
|
160
|
-
if config != nil and @plugin >= 0
|
161
|
-
s = JSON.generate(config)
|
162
|
-
ptr = FFI::MemoryPointer::from_string(s)
|
163
|
-
C.extism_plugin_config(@context.pointer, @plugin, ptr, s.bytesize)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
# Update a plugin with new WASM module or manifest
|
168
|
-
#
|
169
|
-
# @param wasm [Hash, String] The manifest or WASM binary. See https://extism.org/docs/concepts/manifest/.
|
170
|
-
# @param wasi [Boolean] Enable WASI support
|
171
|
-
# @param config [Hash] The plugin config
|
172
|
-
# @return [void]
|
173
|
-
def update(wasm, wasi = false, config = nil)
|
174
|
-
if wasm.class == Hash
|
175
|
-
wasm = JSON.generate(wasm)
|
176
|
-
end
|
177
|
-
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
|
178
|
-
code.put_bytes(0, wasm)
|
179
|
-
ok = C.extism_plugin_update(@context.pointer, @plugin, code, wasm.bytesize, nil, 0, wasi)
|
180
|
-
if !ok
|
181
|
-
err = C.extism_error(@context.pointer, @plugin)
|
182
|
-
if err&.empty?
|
183
|
-
raise Error.new "extism_plugin_update failed"
|
184
|
-
else
|
185
|
-
raise Error.new err
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
if config != nil
|
190
|
-
s = JSON.generate(config)
|
191
|
-
ptr = FFI::MemoryPointer::from_string(s)
|
192
|
-
C.extism_plugin_config(@context.pointer, @plugin, ptr, s.bytesize)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
# Check if a function exists
|
197
|
-
#
|
198
|
-
# @param name [String] The name of the function
|
199
|
-
# @return [Boolean] Returns true if function exists
|
200
|
-
def has_function?(name)
|
201
|
-
C.extism_plugin_function_exists(@context.pointer, @plugin, name)
|
202
|
-
end
|
203
|
-
|
204
|
-
# Call a function by name
|
205
|
-
#
|
206
|
-
# @param name [String] The function name
|
207
|
-
# @param data [String] The input data for the function
|
208
|
-
# @return [String] The output from the function in String form
|
209
|
-
def call(name, data, &block)
|
210
|
-
# If no block was passed then use Pointer::read_string
|
211
|
-
block ||= ->(buf, len) { buf.read_string(len) }
|
212
|
-
input = FFI::MemoryPointer::from_string(data)
|
213
|
-
rc = C.extism_plugin_call(@context.pointer, @plugin, name, input, data.bytesize)
|
214
|
-
if rc != 0
|
215
|
-
err = C.extism_error(@context.pointer, @plugin)
|
216
|
-
if err&.empty?
|
217
|
-
raise Error.new "extism_call failed"
|
218
|
-
else
|
219
|
-
raise Error.new err
|
220
|
-
end
|
221
|
-
end
|
222
|
-
out_len = C.extism_plugin_output_length(@context.pointer, @plugin)
|
223
|
-
buf = C.extism_plugin_output_data(@context.pointer, @plugin)
|
224
|
-
block.call(buf, out_len)
|
225
|
-
end
|
226
|
-
|
227
|
-
# Free a plugin, this should be called when the plugin is no longer needed
|
228
|
-
#
|
229
|
-
# @return [void]
|
230
|
-
def free
|
231
|
-
return if @context.pointer.nil?
|
232
|
-
|
233
|
-
$PLUGINS.delete(self.object_id)
|
234
|
-
C.extism_plugin_free(@context.pointer, @plugin)
|
235
|
-
@plugin = -1
|
236
|
-
end
|
237
|
-
|
238
|
-
# Get a CancelHandle for a plugin
|
239
|
-
def cancel_handle
|
240
|
-
return CancelHandle.new(C.extism_plugin_cancel_handle(@context.pointer, @plugin))
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
private
|
245
|
-
|
246
|
-
# Private module used to interface with the Extism runtime.
|
247
|
-
# *Warning*: Do not use or rely on this directly.
|
248
|
-
module C
|
249
|
-
extend FFI::Library
|
250
|
-
ffi_lib "extism"
|
251
|
-
attach_function :extism_context_new, [], :pointer
|
252
|
-
attach_function :extism_context_free, [:pointer], :void
|
253
|
-
attach_function :extism_plugin_new, [:pointer, :pointer, :uint64, :pointer, :uint64, :bool], :int32
|
254
|
-
attach_function :extism_plugin_update, [:pointer, :int32, :pointer, :uint64, :pointer, :uint64, :bool], :bool
|
255
|
-
attach_function :extism_error, [:pointer, :int32], :string
|
256
|
-
attach_function :extism_plugin_call, [:pointer, :int32, :string, :pointer, :uint64], :int32
|
257
|
-
attach_function :extism_plugin_function_exists, [:pointer, :int32, :string], :bool
|
258
|
-
attach_function :extism_plugin_output_length, [:pointer, :int32], :uint64
|
259
|
-
attach_function :extism_plugin_output_data, [:pointer, :int32], :pointer
|
260
|
-
attach_function :extism_log_file, [:string, :pointer], :void
|
261
|
-
attach_function :extism_plugin_free, [:pointer, :int32], :void
|
262
|
-
attach_function :extism_context_reset, [:pointer], :void
|
263
|
-
attach_function :extism_version, [], :string
|
264
|
-
attach_function :extism_plugin_cancel_handle, [:pointer, :int32], :pointer
|
265
|
-
attach_function :extism_plugin_cancel, [:pointer], :bool
|
266
|
-
end
|
37
|
+
Memory = Struct.new(:offset, :len)
|
267
38
|
end
|
data/sig/extism.rbs
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
module Extism
|
2
|
-
VERSION: String
|
3
|
-
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
4
|
-
end
|
1
|
+
module Extism
|
2
|
+
VERSION: String
|
3
|
+
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
4
|
+
end
|
Binary file
|
data/wasm/reflect.wasm
ADDED
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: 0.
|
4
|
+
version: 1.0.0.pre.rc.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- zach
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -32,15 +32,24 @@ extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
34
|
- ".yardopts"
|
35
|
-
- GETTING_STARTED.md
|
36
35
|
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE
|
37
38
|
- Makefile
|
39
|
+
- README.md
|
38
40
|
- Rakefile
|
39
41
|
- example.rb
|
40
42
|
- lib/extism.rb
|
43
|
+
- lib/extism/current_plugin.rb
|
44
|
+
- lib/extism/host_environment.rb
|
45
|
+
- lib/extism/libextism.rb
|
46
|
+
- lib/extism/plugin.rb
|
41
47
|
- lib/extism/version.rb
|
48
|
+
- lib/extism/wasm.rb
|
42
49
|
- sig/extism.rbs
|
43
|
-
-
|
50
|
+
- wasm/count_vowels.wasm
|
51
|
+
- wasm/reflect.wasm
|
52
|
+
- wasm/store_credit.wasm
|
44
53
|
homepage: https://github.com/extism/extism
|
45
54
|
licenses:
|
46
55
|
- MIT
|
@@ -59,9 +68,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
68
|
version: 2.6.0
|
60
69
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
70
|
requirements:
|
62
|
-
- - "
|
71
|
+
- - ">"
|
63
72
|
- !ruby/object:Gem::Version
|
64
|
-
version:
|
73
|
+
version: 1.3.1
|
65
74
|
requirements: []
|
66
75
|
rubygems_version: 3.4.10
|
67
76
|
signing_key:
|
data/GETTING_STARTED.md
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# Extism
|
2
|
-
|
3
|
-
## Getting Started
|
4
|
-
|
5
|
-
### Example
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
require "extism"
|
9
|
-
require "json"
|
10
|
-
|
11
|
-
Extism.with_context do |ctx|
|
12
|
-
manifest = {
|
13
|
-
:wasm => [{ :path => "../wasm/code.wasm" }],
|
14
|
-
}
|
15
|
-
plugin = ctx.plugin(manifest)
|
16
|
-
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
|
17
|
-
puts res["count"] # => 4
|
18
|
-
end
|
19
|
-
```
|
20
|
-
|
21
|
-
### API
|
22
|
-
|
23
|
-
There are two primary classes you need to understand:
|
24
|
-
|
25
|
-
* [Context](Extism/Context.html)
|
26
|
-
* [Plugin](Extism/Plugin.html)
|
27
|
-
|
28
|
-
#### Context
|
29
|
-
|
30
|
-
The [Context](Extism/Context.html) can be thought of as a session. You need a context to interact with the Extism runtime. The context holds your plugins and when you free the context, it frees your plugins. We recommend using the [Extism.with_context](Extism.html#with_context-class_method) method to ensure that your plugins are cleaned up. But if you need a long lived context for any reason, you can use the constructor [Extism::Context.new](Extism/Context.html#initialize-instance_method).
|
31
|
-
|
32
|
-
#### Plugin
|
33
|
-
|
34
|
-
The [Plugin](Extism/Plugin.html) represents an instance of your WASM program from the given manifest.
|
35
|
-
The key method to know here is [Extism::Plugin#call](Extism/Plugin.html#call-instance_method) which takes a function name to invoke and some input data, and returns the results from the plugin.
|
data/user_code.wasm
DELETED
Binary file
|