opal-webassembly 0.1.0

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rspec-opal +3 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +25 -0
  7. data/LICENSE.adoc +31 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.adoc +45 -0
  10. data/Rakefile +13 -0
  11. data/bin/console +14 -0
  12. data/bin/opal-repl-wasm +4 -0
  13. data/bin/setup +8 -0
  14. data/examples/experiment/Makefile +2 -0
  15. data/examples/experiment/experiment-wasm.c +74 -0
  16. data/examples/experiment/experiment-wasm.wasm +0 -0
  17. data/examples/experiment/experiment.rb +25 -0
  18. data/examples/experiment/experiment.sh +2 -0
  19. data/examples/onigmo/onigmo-wasm.wasm +0 -0
  20. data/examples/onigmo/onigmo.rb +4 -0
  21. data/examples/onigmo/onigmo.sh +2 -0
  22. data/examples/simple_ffi/Rakefile +14 -0
  23. data/examples/simple_ffi/simple-wasm.wasm +0 -0
  24. data/examples/simple_ffi/simple-wasm.wat +8 -0
  25. data/examples/simple_ffi/simple.rb +14 -0
  26. data/exe/opal-repl-wasm +9 -0
  27. data/lib/opal/webassembly.rb +9 -0
  28. data/lib/opal/webassembly/processor.rb +49 -0
  29. data/lib/opal/webassembly/version.rb +5 -0
  30. data/opal-webassembly.gemspec +31 -0
  31. data/opal/ffi.rb +22 -0
  32. data/opal/ffi/library.rb +67 -0
  33. data/opal/ffi/pointer.rb +289 -0
  34. data/opal/ffi/struct.rb +125 -0
  35. data/opal/ffi/types.rb +226 -0
  36. data/opal/webassembly.rb +68 -0
  37. data/opal/webassembly/global.rb +15 -0
  38. data/opal/webassembly/instance.rb +44 -0
  39. data/opal/webassembly/memory.rb +18 -0
  40. data/opal/webassembly/module.rb +10 -0
  41. data/opal/webassembly/table.rb +35 -0
  42. data/spec-opal/ffi_spec.rb +139 -0
  43. data/spec-opal/spec_helper.rb +18 -0
  44. data/spec-opal/webassembly_spec.rb +12 -0
  45. metadata +104 -0
@@ -0,0 +1,5 @@
1
+ module Opal
2
+ module WebAssembly
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'lib/opal/webassembly/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "opal-webassembly"
5
+ spec.version = Opal::WebAssembly::VERSION
6
+ spec.authors = ["Ribose Inc."]
7
+ spec.email = ["open.source@ribose.com"]
8
+
9
+ spec.summary = %q{Opal with WebAssembly support}
10
+ #spec.description = %q{TODO: Write a longer description or delete this line.}
11
+ spec.homepage = "https://github.com/interscript/opal-webassembly"
12
+ spec.license = "BSD-2-Clause"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ #spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/interscript/opal-webassembly"
19
+ spec.metadata["changelog_uri"] = "https://github.com/interscript/opal-webassembly/commits/master"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "opal"
31
+ end
@@ -0,0 +1,22 @@
1
+ require 'webassembly'
2
+
3
+ # An API trying to be compatible with Ruby-FFI.
4
+ module FFI
5
+ def self.contexts
6
+ (@contexts ||= [])
7
+ end
8
+
9
+ def self.context
10
+ (@contexts ||= []).last.tap do |lib|
11
+ unless lib
12
+ raise RuntimeError, "This call needs to be done in a FFI::Library#context "+
13
+ "block (Opal-WebAssembly limitation)."
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require 'ffi/types'
20
+ require 'ffi/pointer'
21
+ require 'ffi/struct'
22
+ require 'ffi/library'
@@ -0,0 +1,67 @@
1
+ module FFI
2
+ module Library
3
+ def self.extended(mod)
4
+ raise RuntimeError.new("must only be extended by module") unless mod.kind_of?(Module)
5
+ end
6
+
7
+ def ffi_lib(*names)
8
+ # Expect the library (first one) to be already loaded in "Opal.WebAssembly.modules"
9
+ @ffi_lib = WebAssembly.libs[names.first]
10
+ raise LoadError, "Library #{names.first} not loaded" if !@ffi_lib
11
+
12
+ begin
13
+ attach_function :malloc, [:long], :pointer
14
+ attach_function :free, [:pointer], :void
15
+ attach_function :realloc, [:pointer, :long], :pointer
16
+ rescue LoadError
17
+ # It's ok, a library doesn't really need to provide memory functions.
18
+ # But we will need those.
19
+ end
20
+ end
21
+
22
+ def library
23
+ @ffi_lib
24
+ end
25
+
26
+ def attach_function(name, func, args = nil, returns = nil, **options)
27
+ if func.is_a? Array # Don't rename a function
28
+ func, args, returns = name, func, args
29
+ end
30
+
31
+ raise LoadError, "No library responds to #{func}" unless @ffi_lib.exports.has_key?(func)
32
+ fun = @ffi_lib.exports[func]
33
+
34
+ self.define_singleton_method name do |*as|
35
+ if as.count != args.count # :varargs?
36
+ raise ArgumentError, "Provided #{as.count} arguments, expected #{args.count}"
37
+ end
38
+
39
+ as = as.each_with_index.map do |a,ind|
40
+ type = Type[args[ind]]
41
+ if type.respond_to? :to_native_mem
42
+ type.to_native_mem(a, @ffi_lib.memory)
43
+ else
44
+ type.to_native(a)
45
+ end
46
+ end
47
+
48
+ ret = fun.call(*as)
49
+ type = Type[returns]
50
+ if type.respond_to? :from_native_mem
51
+ type.from_native_mem(ret, @ffi_lib.memory)
52
+ else
53
+ type.from_native(ret)
54
+ end
55
+ end
56
+ end
57
+
58
+ # Launch a block in a current context. Needed for memory management.
59
+ # Think: segmented memory.
60
+ def context &block
61
+ FFI.contexts.push self
62
+ out = yield
63
+ FFI.contexts.pop
64
+ out
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,289 @@
1
+ # Large portions of this file are adapted from
2
+ # https://github.com/ffi/ffi/blob/master/lib/ffi/pointer.rb
3
+
4
+ module FFI
5
+ class Pointer
6
+ SIZE = 4
7
+
8
+ # Return the size of a pointer on the current platform, in bytes
9
+ # @return [Numeric]
10
+ def self.size
11
+ SIZE
12
+ end
13
+
14
+ # @param [nil,Numeric] len length of string to return
15
+ # @return [String]
16
+ # Read pointer's contents as a string, or the first +len+ bytes of the
17
+ # equivalent string if +len+ is not +nil+.
18
+ def read_string(len=nil)
19
+ if len
20
+ return ''.b if len == 0
21
+ get_bytes(0, len)
22
+ else
23
+ get_string(0)
24
+ end
25
+ end
26
+
27
+ # @param [Numeric] len length of string to return
28
+ # @return [String]
29
+ # Read the first +len+ bytes of pointer's contents as a string.
30
+ #
31
+ # Same as:
32
+ # ptr.read_string(len) # with len not nil
33
+ def read_string_length(len)
34
+ get_bytes(0, len)
35
+ end
36
+
37
+ # @return [String]
38
+ # Read pointer's contents as a string.
39
+ #
40
+ # Same as:
41
+ # ptr.read_string # with no len
42
+ def read_string_to_null
43
+ get_string(0)
44
+ end
45
+
46
+ # @param [String] str string to write
47
+ # @param [Numeric] len length of string to return
48
+ # @return [self]
49
+ # Write +len+ first bytes of +str+ in pointer's contents.
50
+ #
51
+ # Same as:
52
+ # ptr.write_string(str, len) # with len not nil
53
+ def write_string_length(str, len)
54
+ put_bytes(0, str, 0, len)
55
+ end
56
+
57
+ # @param [String] str string to write
58
+ # @param [Numeric] len length of string to return
59
+ # @return [self]
60
+ # Write +str+ in pointer's contents, or first +len+ bytes if
61
+ # +len+ is not +nil+.
62
+ def write_string(str, len=nil)
63
+ #len = str.bytesize unless len
64
+ # Write the string data without NUL termination
65
+ put_bytes(0, str, 0, len)
66
+ end
67
+
68
+ # @param [Type] type type of data to read from pointer's contents
69
+ # @param [Symbol] reader method to send to +self+ to read +type+
70
+ # @param [Numeric] length
71
+ # @return [Array]
72
+ # Read an array of +type+ of length +length+.
73
+ # @example
74
+ # ptr.read_array_of_type(TYPE_UINT8, :read_uint8, 4) # -> [1, 2, 3, 4]
75
+ def read_array_of_type(type, reader, length)
76
+ ary = []
77
+ size = FFI.type_size(type)
78
+ tmp = self
79
+ length.times { |j|
80
+ ary << tmp.send(reader)
81
+ tmp += size unless j == length-1 # avoid OOB
82
+ }
83
+ ary
84
+ end
85
+
86
+ # @param [Type] type type of data to write to pointer's contents
87
+ # @param [Symbol] writer method to send to +self+ to write +type+
88
+ # @param [Array] ary
89
+ # @return [self]
90
+ # Write +ary+ in pointer's contents as +type+.
91
+ # @example
92
+ # ptr.write_array_of_type(TYPE_UINT8, :put_uint8, [1, 2, 3 ,4])
93
+ def write_array_of_type(type, writer, ary)
94
+ size = FFI.type_size(type)
95
+ ary.each_with_index { |val, i|
96
+ break unless i < self.size
97
+ self.send(writer, i * size, val)
98
+ }
99
+ self
100
+ end
101
+
102
+ # @return [self]
103
+ def to_ptr
104
+ self
105
+ end
106
+
107
+ def pointer
108
+ self
109
+ end
110
+
111
+ # @param [Symbol,Type] type of data to read
112
+ # @return [Object]
113
+ # Read pointer's contents as +type+
114
+ #
115
+ # Same as:
116
+ # ptr.get(type, 0)
117
+ def read(type)
118
+ get(type, 0)
119
+ end
120
+
121
+ # @param [Symbol,Type] type of data to read
122
+ # @param [Object] value to write
123
+ # @return [nil]
124
+ # Write +value+ of type +type+ to pointer's content
125
+ #
126
+ # Same as:
127
+ # ptr.put(type, 0)
128
+ def write(type, value)
129
+ put(type, 0, value)
130
+ end
131
+
132
+ def get_bytes(pos, len)
133
+ ary = @memory.buffer.to_a
134
+
135
+ out = []
136
+ i = 0
137
+ while i < len
138
+ out << ary[@address + pos + i]
139
+ i += 1
140
+ end
141
+
142
+ # We expect a binary string as an output of this function.
143
+ out.pack("c*").b # TODO upstream: #bytes is wrong!
144
+ end
145
+
146
+ def put_bytes(pos, src, src_pos=0, len=nil)
147
+ if src.respond_to? :bytes
148
+ src = if src.encoding == Encoding::BINARY
149
+ src.chars.map(&:ord)
150
+ else
151
+ src.bytes
152
+ end
153
+ end
154
+ len ||= src.length - src_pos
155
+
156
+ ary = @memory.buffer.to_a
157
+ i = 0
158
+ while i < len
159
+ ary[@address + i + pos] = src[src_pos + i]
160
+ i += 1
161
+ end
162
+ end
163
+
164
+ def get(type, pos)
165
+ type = FFI::Type[type]
166
+ bytes = get_bytes(pos, type.size)
167
+ if type.respond_to? :from_native_mem
168
+ type.from_native_mem(type.unpack(bytes), @memory)
169
+ else
170
+ type.from_native(type.unpack(bytes))
171
+ end
172
+ end
173
+
174
+ def put(type, pos, value)
175
+ type = FFI::Type[type]
176
+ value = if type.respond_to? :to_native_mem
177
+ type.to_native_mem(value, @memory)
178
+ else
179
+ type.to_native(value)
180
+ end
181
+ value = type.pack(value)
182
+ put_bytes(pos, value)
183
+ end
184
+
185
+ def get_string(pos)
186
+ out = []
187
+ ary = @memory.buffer.to_a
188
+ pos += @address
189
+ loop do
190
+ byte = ary[pos]
191
+ break if byte == 0
192
+ out << byte
193
+ pos += 1
194
+ end
195
+ out.pack("c*").b
196
+ end
197
+
198
+ def put_string(pos, string)
199
+ put_bytes(pos, string)
200
+ end
201
+
202
+ def self.from_string src
203
+ bs = if src.encoding == Encoding::BINARY
204
+ src.chars.map(&:ord)
205
+ else
206
+ src.bytes
207
+ end
208
+ bs = bs + [0, 0]
209
+ out = new(:uint8, bs.count)
210
+ out.put_bytes(0, bs)
211
+ out
212
+ end
213
+
214
+ def self.alloc_out size, count, smh=false
215
+ new(:uint8, size*count)
216
+ end
217
+
218
+ def self.alloc_in size, count, smh=false
219
+ new(:uint8, size*count)
220
+ end
221
+
222
+ def method_missing method, *args
223
+ method = method.to_s
224
+ super
225
+ end
226
+
227
+ # Pointer in our case is more complex than a regular one: it's
228
+ # [WebAssembly::Memory instance, uint32 address] since WebAssembly
229
+ # "processes" run in separate address spaces.
230
+ #
231
+ # Alternatively, memory can be deduced from wrapping inside
232
+ # Library#context.
233
+ def initialize(address, type=nil, size=nil)
234
+ if address.respond_to? :address
235
+ if address.respond_to? :memory
236
+ @memory = address.memory || FFI.context.library.memory
237
+ else
238
+ @memory = FFI.context.library.memory
239
+ end
240
+ @address = address.address
241
+ elsif address.respond_to?(:to_sym) || address.is_a?(Type) || address.is_a?(Class) # Allocation call
242
+ type, count = address, type
243
+ @memory = FFI.context.library.memory
244
+ @address = FFI.context.malloc(FFI::Type[type].size * (count || 1)).address
245
+ elsif address.respond_to? :to_ary
246
+ @memory, @address = address.to_ary
247
+ @size = size
248
+ elsif address.respond_to? :to_int
249
+ @memory = FFI.context.library.memory
250
+ @address = address.to_int
251
+ @size = size
252
+ else
253
+ raise TypeError, "Address has an invalid type"
254
+ end
255
+ @type = type ? FFI::Type[type] : FFI::Type[:uint8]
256
+ end
257
+
258
+ attr_accessor :memory, :address, :type
259
+
260
+ def == other
261
+ (self.address == other.address) &&
262
+ (self.address == 0 || (self.memory == other.memory))
263
+ end
264
+
265
+ def + offset
266
+ self.dup.tap { |i| i.address += offset * type.size }
267
+ end
268
+
269
+ def [] offset
270
+ self.get(type, offset * type.size)
271
+ end
272
+
273
+ def []= offset, value
274
+ self.put(type, offset * type.size, value)
275
+ end
276
+
277
+ def resize(new_size)
278
+ self.address = FFI.context.realloc(self, new_size).address
279
+ end
280
+
281
+ def inspect
282
+ out = "#<#{self.class.name}:#{"0x%08x" % self.address} of #{self.type.inspect}>"
283
+ end
284
+
285
+ NULL = new([nil, 0])
286
+ end
287
+
288
+ AutoPointer = MemoryPointer = Buffer = Pointer
289
+ end
@@ -0,0 +1,125 @@
1
+ module FFI
2
+ class Struct
3
+ def self.size
4
+ @size || 4
5
+ end
6
+
7
+ def self.alignment
8
+ @alignment ||= @layout&.values&.map(&:type)&.map(&:alignment)&.max || 4
9
+ end
10
+
11
+ def self.by_value; self; end
12
+ def self.by_ptr; FFI::Type::WrappedStruct.new(self); end
13
+
14
+ def self.pack x; raise ArgumentError, "Packing Struct directly is forbidden"; end
15
+ def self.unpack x; raise ArgumentError, "Unpacking Struct directly is forbidden"; end
16
+
17
+ def from_native_mem(x, memory)
18
+ new(FFI::Pointer.new([memory, x], self))
19
+ end
20
+
21
+ def to_native(x)
22
+ x.pointer.address
23
+ end
24
+
25
+ def self.ptr; by_ptr; end
26
+
27
+ def self.layout *fs, **fields
28
+ if fs.length > 0
29
+ while (z = fs.shift(2)).length == 2
30
+ k, v = z
31
+ fields[k] = v
32
+ end
33
+ end
34
+
35
+ offset = 0
36
+ @layout = {}
37
+ fields.each do |name, type|
38
+ count = nil
39
+ if type.is_a? Array
40
+ type, count = type
41
+ end
42
+ type = FFI::Type[type, :struct]
43
+
44
+ offset += 1 until (offset % type.alignment) == 0
45
+ @layout[name] = Field.new(self, name, type, offset, count)
46
+
47
+ if count
48
+ offset += type.size * count
49
+ else
50
+ offset += type.size
51
+ end
52
+ end
53
+ @size = offset
54
+ end
55
+
56
+ def self.members; @layout; end
57
+ def members; self.class.members; end
58
+
59
+ def [] field
60
+ members[field].get(self)
61
+ end
62
+
63
+ def []= field, value
64
+ members[field].set(self, value)
65
+ end
66
+
67
+ def offset_of field
68
+ members[field].offset
69
+ end
70
+
71
+ attr_reader :pointer
72
+
73
+ def address
74
+ pointer.address
75
+ end
76
+
77
+ def initialize pointer=nil
78
+ if pointer
79
+ @pointer = pointer
80
+ else
81
+ @pointer = FFI.context.malloc(self.class.size)
82
+ @pointer.type = FFI::Type[self.class]
83
+ @pointer
84
+ end
85
+ end
86
+
87
+ def inspect
88
+ out = ["#<#{self.class.name}:#{"0x%08x" % self.address}: "]
89
+ out << self.members.keys.map do |key|
90
+ ":#{key} => #{self.[](key).inspect}"
91
+ end.join(", ")
92
+ out << ">"
93
+ out.join
94
+ end
95
+ end
96
+
97
+ class Field
98
+ attr_accessor :struct, :name, :type, :offset, :count
99
+ def initialize struct, name, type, offset, count
100
+ @struct, @name, @type, @offset, @count = struct, name, type, offset, count
101
+ end
102
+
103
+ def get from
104
+ if count
105
+ FFI::Pointer.new([from.pointer.memory, from.address + offset], type)
106
+ elsif type.is_a?(Class) && type <= FFI::Struct
107
+ type.new(FFI::Pointer.new([from.pointer.memory, from.address + offset], type))
108
+ else
109
+ from.pointer.get(type, offset)
110
+ end
111
+ end
112
+
113
+ def set from, value
114
+ if count
115
+ raise ArgumentError, "Can't set an array #{name} of #{struct}."
116
+ elsif type.is_a?(Class) && type <= FFI::Struct
117
+ raise ArgumentError, "Can't set a nested struct #{name}."
118
+ else
119
+ from.pointer.put(type, offset, value)
120
+ end
121
+ end
122
+ end
123
+
124
+ ManagedStruct = Struct
125
+ end