wasmer 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Cargo.toml ADDED
@@ -0,0 +1,21 @@
1
+ [package]
2
+ publish = false
3
+ name = "ruby-ext-wasm"
4
+ version = "0.1.1"
5
+ authors = ["Ivan Enderlin <ivan.enderlin@hoa-project.net>"]
6
+ edition = "2018"
7
+ description = "Ruby extension to run WebAssembly binaries"
8
+ readme = "README.md"
9
+ repository = "https://github.com/wasmerio/ruby-ext-wasm"
10
+ keywords = ["ruby", "extension", "webassembly"]
11
+ categories = ["wasm"]
12
+
13
+ [lib]
14
+ name = "wasmer"
15
+ crate-type = ["dylib"]
16
+
17
+ [dependencies]
18
+ wasmer-runtime = "0.3.0"
19
+ wasmer-runtime-core = "0.3.0"
20
+ rutie = "0.5.4"
21
+ lazy_static = "1.3.0"
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'minitest', '~> 5.10'
7
+ gem 'minitest-reporters', '~> 1.1'
8
+ gem 'color_pound_spec_reporter', '~> 0.0.6'
9
+ end
data/README.md ADDED
@@ -0,0 +1,279 @@
1
+ <p align="center">
2
+ <a href="https://wasmer.io" target="_blank" rel="noopener noreferrer">
3
+ <img width="400" src="https://raw.githubusercontent.com/wasmerio/wasmer/master/logo.png" alt="Wasmer logo">
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://spectrum.chat/wasmer">
9
+ <img src="https://withspectrum.github.io/badge/badge.svg" alt="Join the Wasmer Community"></a>
10
+ <a href="https://github.com/wasmerio/wasmer/blob/master/LICENSE">
11
+ <img src="https://img.shields.io/github/license/wasmerio/wasmer.svg" alt="License"></a>
12
+ </p>
13
+
14
+ # The Ruby extension to run WebAssembly
15
+
16
+ The goal of the project is to be able to run WebAssembly binaries from
17
+ Ruby directly. So much fun coming!
18
+
19
+ _Under Development, don't use it in product yet_.
20
+
21
+ ## What is WebAssembly?
22
+
23
+ Quoting [the WebAssembly site](https://webassembly.org/):
24
+
25
+ > WebAssembly (abbreviated Wasm) is a binary instruction format for a
26
+ > stack-based virtual machine. Wasm is designed as a portable target
27
+ > for compilation of high-level languages like C/C++/Rust, enabling
28
+ > deployment on the web for client and server applications.
29
+
30
+ About speed:
31
+
32
+ > WebAssembly aims to execute at native speed by taking advantage of
33
+ > [common hardware
34
+ > capabilities](https://webassembly.org/docs/portability/#assumptions-for-efficient-execution)
35
+ > available on a wide range of platforms.
36
+
37
+ About safety:
38
+
39
+ > WebAssembly describes a memory-safe, sandboxed [execution
40
+ > environment](https://webassembly.org/docs/semantics/#linear-memory) […].
41
+
42
+ ## Example
43
+
44
+ There is a toy program in `examples/simple.rs`, written in Rust (or
45
+ any other language that compiles to Wasm):
46
+
47
+ ```rust
48
+ #[no_mangle]
49
+ pub extern fn sum(x: i32, y: i32) -> i32 {
50
+ x + y
51
+ }
52
+ ```
53
+
54
+ Once this program compiled to WebAssembly, we end up with a
55
+ `examples/simple.wasm` binary file.
56
+
57
+ Then, we can execute it in Ruby (!) with the `examples/simple.rb` file:
58
+
59
+ ```rb
60
+ require "wasmer"
61
+
62
+ bytes = IO.read "simple.wasm", mode: "rb"
63
+ instance = Instance.new bytes
64
+ puts instance.exports.sum 1, 2
65
+ ```
66
+
67
+ And then, finally, enjoy by running:
68
+
69
+ ```sh
70
+ $ ruby simple.rb
71
+ 3
72
+ ```
73
+
74
+ ## API documentation
75
+
76
+ ### The `Instance` class
77
+
78
+ Instantiates a WebAssembly module represented by bytes, and calls exported functions on it:
79
+
80
+ ```ruby
81
+ require "wasmer"
82
+
83
+ # Get the Wasm module as bytes.
84
+ wasm_bytes = IO.read "my_program.wasm", mode: "rb"
85
+
86
+ # Instantiates the Wasm module.
87
+ instance = Instance.new wasm_bytes
88
+
89
+ # Call a function on it.
90
+ result = instance.exports.sum 1, 2
91
+
92
+ puts result # 3
93
+ ```
94
+
95
+ All exported functions are accessible on the `exports`
96
+ getter. Arguments of these functions are automatically casted to
97
+ WebAssembly values.
98
+
99
+ The `memory` getter exposes the `Memory` class representing the memory
100
+ of that particular instance, e.g.:
101
+
102
+ ```ruby
103
+ view = instance.memory.uint8_view
104
+ ```
105
+
106
+ See below for more information.
107
+
108
+ ### The `Memory` class
109
+
110
+ A WebAssembly instance has its own memory, represented by the `Memory`
111
+ class. It is accessible by the `Instance.memory` getter.
112
+
113
+ The `Memory` class offers methods to create views of the memory
114
+ internal buffer, e.g. `uint8_view`, `int8_view`, `uint16_view`
115
+ etc. All these methods accept one optional argument: `offset`, to
116
+ subset the memory buffer at a particular offset. These methods return
117
+ respectively a `*Array` object, i.e. `uint8_view` returns a
118
+ `Uint8Array` object etc.
119
+
120
+ ```ruby
121
+ offset = 7
122
+ view = instance.memory.uint8_view offset
123
+
124
+ puts view[0]
125
+ ```
126
+
127
+ #### The `*Array` classes
128
+
129
+ These classes represent views over a memory buffer of an instance.
130
+
131
+ | Class | View buffer as a sequence of… | Bytes per element |
132
+ |-|-|-|
133
+ | `Int8Array` | `int8` | 1 |
134
+ | `Uint8Array` | `uint8` | 1 |
135
+ | `Int16Array` | `int16` | 2 |
136
+ | `Uint16Array` | `uint16` | 2 |
137
+ | `Int32Array` | `int32` | 4 |
138
+ | `Uint32Array` | `uint32` | 4 |
139
+
140
+ All these classes share the same implementation. Taking the example of
141
+ `Uint8Array`, the class looks like this:
142
+
143
+ ```ruby
144
+ class Uint8Array
145
+ def bytes_per_element
146
+ def length
147
+ def [](index)
148
+ def []=(index, value)
149
+ end
150
+ ```
151
+
152
+ Let's see it in action:
153
+
154
+ ```ruby
155
+ require "wasmer"
156
+
157
+ # Get the Wasm module as bytes.
158
+ wasm_bytes = IO.read "my_program.wasm", mode: "rb"
159
+
160
+ # Instantiates the Wasm module.
161
+ instance = Instance.new wasm_bytes
162
+
163
+ # Call a function that returns a pointer to a string for instance.
164
+ pointer = instance.exports.return_string
165
+
166
+ # Get the memory view, with the offset set to `pointer` (default is 0).
167
+ memory = instance.memory.uint8_view pointer
168
+
169
+ # Read the string pointed by the pointer.
170
+ nth = 0
171
+ string = ""
172
+
173
+ while true
174
+ char = memory[nth]
175
+
176
+ if 0 == char
177
+ break
178
+ end
179
+
180
+ string += char.chr
181
+ nth += 1
182
+ end
183
+
184
+ puts string # Hello, World!
185
+ ```
186
+
187
+ Notice that `*Array` treat bytes in little-endian, as required by the
188
+ WebAssembly specification, [Chapter Structure, Section Instructions,
189
+ Sub-Section Memory
190
+ Instructions](https://webassembly.github.io/spec/core/syntax/instructions.html#memory-instructions):
191
+
192
+ > All values are read and written in [little
193
+ > endian](https://en.wikipedia.org/wiki/Endianness#Little-endian) byte
194
+ > order.
195
+
196
+ Each view shares the same memory buffer internally. Let's have some fun:
197
+
198
+ ```ruby
199
+ int8 = instance.memory.int8_view
200
+ int16 = instance.memory.int16_view
201
+ int32 = instance.memory.int32_view
202
+
203
+ b₁
204
+ ┌┬┬┬┬┬┬┐
205
+ int8[0] = 0b00000001
206
+ b₂
207
+ ┌┬┬┬┬┬┬┐
208
+ int8[1] = 0b00000100
209
+ b₃
210
+ ┌┬┬┬┬┬┬┐
211
+ int8[2] = 0b00010000
212
+ b₄
213
+ ┌┬┬┬┬┬┬┐
214
+ int8[3] = 0b01000000
215
+
216
+ // No surprise with the following assertions.
217
+ b₁
218
+ ┌┬┬┬┬┬┬┐
219
+ assert_equal 0b00000001, int8[0]
220
+ b₂
221
+ ┌┬┬┬┬┬┬┐
222
+ assert_equal 0b00000100, int8[1]
223
+ b₃
224
+ ┌┬┬┬┬┬┬┐
225
+ assert_equal 0b00010000, int8[2]
226
+ b₄
227
+ ┌┬┬┬┬┬┬┐
228
+ assert_equal 0b01000000, int8[3]
229
+
230
+ // The `int16` view reads 2 bytes.
231
+ b₂ b₁
232
+ ┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐
233
+ assert_equal 0b00000100_00000001, int16[0]
234
+ b₄ b₃
235
+ ┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐
236
+ assert_equal 0b01000000_00010000, int16[1]
237
+
238
+ // The `int32` view reads 4 bytes.
239
+ b₄ b₃ b₂ b₁
240
+ ┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐
241
+ assert_equal 0b01000000_00010000_00000100_00000001, int32[0]
242
+ ```
243
+
244
+ ### The `Module` class
245
+
246
+ The `Module` class contains one static method `validate`, that checks
247
+ whether the given bytes represent valid WebAssembly bytes:
248
+
249
+ ```ruby
250
+ require "wasmer"
251
+
252
+ wasm_bytes = IO.read "my_program.wasm", mode: "rb"
253
+
254
+ if not Module.validate wasm_bytes
255
+ puts "The program seems corrupted."
256
+ end
257
+ ```
258
+
259
+ This function returns a boolean.
260
+
261
+ ## Install and Testing
262
+
263
+ To compile the entire project, run the following commands:
264
+
265
+ ```sh
266
+ $ just build
267
+ $ just test
268
+ $ ruby examples/simple.rb
269
+ ```
270
+
271
+ (Yes, you need [`just`](https://github.com/casey/just/)).
272
+
273
+ ## License
274
+
275
+ The entire project is under the BSD-3-Clause license. Please read [the
276
+ `LICENSE` file][license].
277
+
278
+
279
+ [license]: https://github.com/wasmerio/wasmer/blob/master/LICENSE
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require "rbconfig"
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ desc 'Build the Rust extension'
6
+ task :build_lib do
7
+ sh 'cargo build --release'
8
+ end
9
+
10
+ desc 'Install the bundle'
11
+ task :bundle_install do
12
+ sh 'bundle install'
13
+ end
14
+
15
+ Rake::TestTask.new(test: [:bundle_install, :build_lib]) do |t|
16
+ t.libs << "tests"
17
+ t.libs << "lib"
18
+ t.test_files = FileList["tests/**/*_test.rb"]
19
+ end
20
+
21
+ task :default => :test
data/justfile ADDED
@@ -0,0 +1,23 @@
1
+ # Compile and install the Ruby extension.
2
+ build:
3
+ rake build_lib
4
+
5
+ # Run the tests.
6
+ test:
7
+ rake test
8
+
9
+ # Build the `.gem` file.
10
+ gem:
11
+ rake build
12
+
13
+ # Clean the project.
14
+ clean:
15
+ cargo clean
16
+ rake clean
17
+ rake clobber
18
+
19
+
20
+ # Local Variables:
21
+ # mode: makefile
22
+ # End:
23
+ # vim: set ft=make :
data/lib/wasmer.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'wasmer/version'
2
+ require 'rutie'
3
+
4
+ module Wasmer
5
+ Rutie.new(:wasmer).init 'Init_wasmer', __dir__
6
+ end
@@ -0,0 +1,3 @@
1
+ module Wasmer
2
+ VERSION = "0.1.1"
3
+ end
data/src/instance.rs ADDED
@@ -0,0 +1,282 @@
1
+ //! The `Instance` WebAssembly class.
2
+
3
+ use crate::memory::{Memory, RubyMemory, MEMORY_WRAPPER};
4
+ use lazy_static::lazy_static;
5
+ use rutie::{
6
+ class, methods,
7
+ rubysys::{class, value::ValueType},
8
+ types::{Argc, Value},
9
+ util::str_to_cstring,
10
+ wrappable_struct, AnyException, AnyObject, Array, Class, Exception, Fixnum, Float, Object,
11
+ RString, Symbol, VM,
12
+ };
13
+ use std::{mem, rc::Rc};
14
+ use wasmer_runtime::{self as runtime, imports, Export};
15
+ use wasmer_runtime_core::types::Type;
16
+
17
+ /// The `ExportedFunctions` Ruby class.
18
+ pub struct ExportedFunctions {
19
+ /// The WebAssembly runtime.
20
+ instance: Rc<runtime::Instance>,
21
+ }
22
+
23
+ impl ExportedFunctions {
24
+ /// Create a new instance of the `ExportedFunctions` Ruby class.
25
+ pub fn new(instance: Rc<runtime::Instance>) -> Self {
26
+ Self { instance }
27
+ }
28
+
29
+ /// Call an exported function on the given WebAssembly instance.
30
+ pub fn method_missing(&self, method_name: &str, arguments: Array) -> AnyObject {
31
+ let function = self
32
+ .instance
33
+ .dyn_func(method_name)
34
+ .map_err(|_| {
35
+ VM::raise_ex(AnyException::new(
36
+ "RuntimeError",
37
+ Some(&format!("Function `{}` does not exist.", method_name)),
38
+ ))
39
+ })
40
+ .unwrap();
41
+ let signature = function.signature();
42
+ let parameters = signature.params();
43
+ let number_of_parameters = parameters.len() as isize;
44
+ let number_of_arguments = arguments.length() as isize;
45
+ let diff: isize = number_of_parameters - number_of_arguments;
46
+
47
+ if diff > 0 {
48
+ VM::raise_ex(AnyException::new(
49
+ "ArgumentError",
50
+ Some(&format!(
51
+ "Missing {} argument(s) when calling `{}`: Expect {} argument(s), given {}.",
52
+ diff, method_name, number_of_parameters, number_of_arguments
53
+ )),
54
+ ));
55
+ unreachable!();
56
+ } else if diff < 0 {
57
+ VM::raise_ex(AnyException::new(
58
+ "ArgumentError",
59
+ Some(&format!(
60
+ "Given {} extra argument(s) when calling `{}`: Expect {} argument(s), given {}.",
61
+ diff.abs(), method_name, number_of_parameters, number_of_arguments
62
+ )),
63
+ ));
64
+ unreachable!();
65
+ }
66
+
67
+ let mut function_arguments =
68
+ Vec::<runtime::Value>::with_capacity(number_of_parameters as usize);
69
+
70
+ for (nth, (parameter, argument)) in parameters.iter().zip(arguments.into_iter()).enumerate()
71
+ {
72
+ let value = match (parameter, argument.ty()) {
73
+ (Type::I32, ValueType::Fixnum) => runtime::Value::I32(
74
+ argument
75
+ .try_convert_to::<Fixnum>()
76
+ .map_err(|_| {
77
+ VM::raise_ex(AnyException::new(
78
+ "TypeError",
79
+ Some(&format!(
80
+ "Cannot convert argument #{} to a WebAssembly i32 value.",
81
+ nth + 1
82
+ )),
83
+ ))
84
+ })
85
+ .unwrap()
86
+ .to_i32(),
87
+ ),
88
+ (Type::I64, ValueType::Fixnum) => runtime::Value::I64(
89
+ argument
90
+ .try_convert_to::<Fixnum>()
91
+ .map_err(|_| {
92
+ VM::raise_ex(AnyException::new(
93
+ "TypeError",
94
+ Some(&format!(
95
+ "Cannot convert argument #{} to a WebAssembly i64 value.",
96
+ nth + 1
97
+ )),
98
+ ))
99
+ })
100
+ .unwrap()
101
+ .to_i64(),
102
+ ),
103
+ (Type::F32, ValueType::Float) => runtime::Value::F32(
104
+ argument
105
+ .try_convert_to::<Float>()
106
+ .map_err(|_| {
107
+ VM::raise_ex(AnyException::new(
108
+ "TypeError",
109
+ Some(&format!(
110
+ "Cannot convert argument #{} to a WebAssembly f32 value.",
111
+ nth + 1
112
+ )),
113
+ ))
114
+ })
115
+ .unwrap()
116
+ .to_f64() as f32,
117
+ ),
118
+ (Type::F64, ValueType::Float) => runtime::Value::F64(
119
+ argument
120
+ .try_convert_to::<Float>()
121
+ .map_err(|_| {
122
+ VM::raise_ex(AnyException::new(
123
+ "TypeError",
124
+ Some(&format!(
125
+ "Cannot convert argument #{} to a WebAssembly f64 value.",
126
+ nth + 1
127
+ )),
128
+ ))
129
+ })
130
+ .unwrap()
131
+ .to_f64(),
132
+ ),
133
+ (_, ty) => {
134
+ VM::raise_ex(AnyException::new(
135
+ "ArgumentError",
136
+ Some(&format!(
137
+ "Cannot convert argument #{} to a WebAssembly value. Only integers and floats are supported. Given `{:?}`.",
138
+ nth + 1,
139
+ ty
140
+ ))));
141
+ unreachable!()
142
+ }
143
+ };
144
+
145
+ function_arguments.push(value);
146
+ }
147
+
148
+ let results = function
149
+ .call(function_arguments.as_slice())
150
+ .map_err(|e| VM::raise_ex(AnyException::new("RuntimeError", Some(&format!("{}", e)))))
151
+ .unwrap();
152
+
153
+ match results[0] {
154
+ runtime::Value::I32(result) => Fixnum::new(result as i64).into(),
155
+ runtime::Value::I64(result) => Fixnum::new(result).into(),
156
+ runtime::Value::F32(result) => Float::new(result as f64).into(),
157
+ runtime::Value::F64(result) => Float::new(result).into(),
158
+ }
159
+ }
160
+ }
161
+
162
+ wrappable_struct!(
163
+ ExportedFunctions,
164
+ ExportedFunctionsWrapper,
165
+ EXPORTED_FUNCTIONS_WRAPPER
166
+ );
167
+
168
+ class!(RubyExportedFunctions);
169
+
170
+ /// Glue code to call the `ExportedFunctions.method_missing` method.
171
+ pub extern "C" fn ruby_exported_functions_method_missing(
172
+ argc: Argc,
173
+ argv: *const AnyObject,
174
+ itself: RubyExportedFunctions,
175
+ ) -> AnyObject {
176
+ let arguments = Value::from(0);
177
+
178
+ unsafe {
179
+ let argv_pointer: *const Value = mem::transmute(argv);
180
+
181
+ class::rb_scan_args(argc, argv_pointer, str_to_cstring("*").as_ptr(), &arguments)
182
+ };
183
+
184
+ let mut arguments = Array::from(arguments);
185
+ let method_name = unsafe { arguments.shift().to::<Symbol>() };
186
+ let method_name = method_name.to_str();
187
+
188
+ itself
189
+ .get_data(&*EXPORTED_FUNCTIONS_WRAPPER)
190
+ .method_missing(method_name, arguments)
191
+ }
192
+
193
+ /// The `Instance` Ruby class.
194
+ pub struct Instance {
195
+ /// The WebAssembly instance.
196
+ instance: Rc<runtime::Instance>,
197
+ }
198
+
199
+ impl Instance {
200
+ /// Create a new instance of the `Instance` Ruby class.
201
+ /// The constructor receives bytes from a string.
202
+ pub fn new(bytes: &[u8]) -> Self {
203
+ let import_object = imports! {};
204
+ let instance = Rc::new(
205
+ runtime::instantiate(bytes, &import_object)
206
+ .map_err(|e| {
207
+ VM::raise_ex(AnyException::new(
208
+ "RuntimeError",
209
+ Some(&format!("Failed to instantiate the module:\n {}", e)),
210
+ ))
211
+ })
212
+ .unwrap(),
213
+ );
214
+
215
+ Self { instance }
216
+ }
217
+ }
218
+
219
+ wrappable_struct!(Instance, InstanceWrapper, INSTANCE_WRAPPER);
220
+
221
+ class!(RubyInstance);
222
+
223
+ #[rustfmt::skip]
224
+ methods!(
225
+ RubyInstance,
226
+ _itself,
227
+ // Glue code to call the `Instance.new` method.
228
+ fn ruby_instance_new(bytes: RString) -> AnyObject {
229
+ let instance = Instance::new(
230
+ bytes
231
+ .map_err(|_| {
232
+ VM::raise_ex(AnyException::new(
233
+ "ArgumentError",
234
+ Some("WebAssembly module must be represented by Ruby bytes only."),
235
+ ))
236
+ })
237
+ .unwrap()
238
+ .to_bytes_unchecked(),
239
+ );
240
+ let exported_functions = ExportedFunctions::new(instance.instance.clone());
241
+
242
+ let memory = instance
243
+ .instance
244
+ .exports()
245
+ .find_map(|(_, export)| match export {
246
+ Export::Memory(memory) => Some(Memory::new(Rc::new(memory))),
247
+ _ => None,
248
+ })
249
+ .ok_or_else(|| VM::raise_ex(AnyException::new("RuntimeError", Some("The WebAssembly module has no exported memory."))))
250
+ .unwrap();
251
+
252
+ let mut ruby_instance: AnyObject =
253
+ Class::from_existing("Instance").wrap_data(instance, &*INSTANCE_WRAPPER);
254
+
255
+ let ruby_exported_functions: RubyExportedFunctions =
256
+ Class::from_existing("ExportedFunctions")
257
+ .wrap_data(exported_functions, &*EXPORTED_FUNCTIONS_WRAPPER);
258
+
259
+ ruby_instance.instance_variable_set("@exports", ruby_exported_functions);
260
+
261
+ let ruby_memory: RubyMemory =
262
+ Class::from_existing("Memory").wrap_data(memory, &*MEMORY_WRAPPER);
263
+
264
+ ruby_instance.instance_variable_set("@memory", ruby_memory);
265
+
266
+ ruby_instance
267
+ }
268
+
269
+ // Glue code to call the `Instance.exports` getter method.
270
+ fn ruby_instance_exported_functions() -> RubyExportedFunctions {
271
+ unsafe {
272
+ _itself
273
+ .instance_variable_get("@exports")
274
+ .to::<RubyExportedFunctions>()
275
+ }
276
+ }
277
+
278
+ // Glue code to call the `Instance.memory` getter method.
279
+ fn ruby_instance_memory() -> RubyMemory {
280
+ unsafe { _itself.instance_variable_get("@memory").to::<RubyMemory>() }
281
+ }
282
+ );