Module: Cry::Wasm

Defined in:
lib/cry/wasm.rb,
lib/cry/wasm/version.rb

Overview

The Cry::Wasm module is for defining Ruby methods that will be compiled into WASM. use ‘cry` to define the method signature. use `cry_build` to compile the method to WASM.

Examples:

class Foo
  extend Cry::Wasm
  cry [Int32, Int32], Int32
  def add(a, b)
    a + b
  end
  cry_build
end
Foo.new.add(1, 2) #=> 3

Constant Summary collapse

VERSION =
'0.0.0'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extended(obj) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/cry/wasm.rb', line 186

def self.extended(obj)
  # Initialize class instance variables
  raise "class instance variable '@cry_wasm' is already defined" if obj.instance_variable_defined?(:@cry_wasm)

  # Only one class instance variable is used here.
  # to avoid bugs caused by overwriting.
  @cry_wasm_runtime ||= Wasmer
  obj.instance_variable_set(:@cry_wasm, {
                              runtime: @cry_wasm_runtime.new,
                              flag: false,
                              marked_methods: [],
                              fname: '',
                              line_number: 0,
                              compiler: Compiler.new,
                              codegen: Codegen.new
                            })
end

.runtimeObject



30
31
32
# File 'lib/cry/wasm.rb', line 30

def self.runtime
  @cry_wasm_runtime
end

.runtime=(runtime) ⇒ Object



26
27
28
# File 'lib/cry/wasm.rb', line 26

def self.runtime=(runtime)
  @cry_wasm_runtime = runtime
end

Instance Method Details

#cry(arg_types, ret_type) ⇒ nil

Defines the method signature. This method must be called before the target method is defined.

Parameters:

  • arg_types (Array<String, Symbol>)

    crystal argument types

  • ret_type (String, Symbol)

    crystal return type

Returns:

  • (nil)


89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/cry/wasm.rb', line 89

def cry(arg_types, ret_type)
  fname, l = caller[0].split(':')
  @cry_wasm[:codegen].source_path = fname

  # Searches for methods that appear on a line later than cry was called.
  @cry_wasm[:caller_line_number] = l.to_i
  @cry_wasm[:flag] = true
  @crystal_arg_types = arg_types
  @crystal_ret_type = ret_type

  nil
end

#cry_build(wasm_out = nil, **options) ⇒ Array<Symbol>

Compile the method to WASM. This method must be called after the target methods are defined.

Parameters:

  • wasm_out (String) (defaults to: nil)

    output path of WASM

  • options (Hash)

    options for Cry::Compiler#build_wasm

Returns:

  • (Array<Symbol>)

    method names that were compiled to WASM



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/cry/wasm.rb', line 108

def cry_build(wasm_out = nil, **options)
  crystal_code = @cry_wasm[:codegen].crystal_code
  wasm_bytes = @cry_wasm[:compiler].build_wasm(
    crystal_code,
    export: @cry_wasm[:codegen].function_names, # There are other methods other than marked_method that must be exported.
    output: wasm_out,
    **options
  )
  runtime = @cry_wasm[:runtime].load_wasm(wasm_bytes)
  @cry_wasm[:marked_methods].each do |name|
    func = runtime.function(name)
    itfc = @cry_wasm[:codegen].interface(name)
    define_method(name) do |*args, **kwargs, &block|
      # NOTE: This is a temporary implementation.
      #       In the future, keyword arguments may be used
      #       as a specification of local variable types.
      raise ArgumentError, 'keyword arguments are not supported' unless kwargs.empty?
      raise ArgumentError, 'block is not supported' if block

      new_args = []

      itfc.crystal_arg_types.zip(args).each do |t, arg|
        if t.is_array? || t.is_pointer?
          t2 = t.inner.downcase
          l = arg.length

          addr = runtime.invoke("__alloc_buffer_#{t2}", l)
          runtime.write_memory(addr, t2, arg)

          new_args << addr
          new_args << l if t.is_array?
        elsif t == 'String'
          raise ArgumentError, "expected String, got #{arg.class}" unless arg.is_a?(String)

          arg = arg.encode('UTF-8').bytes
          l = arg.size
          addr = runtime.invoke('__alloc_buffer_uint8', l)
          runtime.write_memory(addr, 'uint8', arg)

          new_args << addr
          new_args << l
        else
          new_args << arg
        end
      end

      r = itfc.crystal_ret_type
      addr2 = nil
      if r.is_array? or r == 'String'
        addr2 = runtime.invoke('__alloc_buffer_int32', 1)
        runtime.write_memory(addr2, 'int32', [0])
        new_args << addr2
      end

      result = func.call(*new_args)

      if r.is_pointer?
        view = runtime.get_view(result, r.inner.downcase)
      elsif r.is_array?
        l2 = runtime.read_memory(addr2, 'int32', 1)[0]
        runtime.read_memory(result, r.inner.downcase, l2)
      elsif r == 'String'
        l2 = runtime.read_memory(addr2, 'int32', 1)[0]
        runtime.read_memory(result, 'uint8', l2).pack('C*').force_encoding('UTF-8')
      else
        result
      end
      # FIXME: Release memory
    end
  end
end

#cry_eval(code, export: nil) ⇒ Object

Note:

naming. ‘cry_code` or `cry_soruce` is better?

Add crystal code before compiling to WASM. You can pass the crystal code as a string. Use load to load the crystal code from a file.

Parameters:

  • code (String)

    crystal code

  • export (String, Array<String>) (defaults to: nil)

    function names to be exported

Raises:

  • (ArgumentError)


70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/cry/wasm.rb', line 70

def cry_eval(code, export: nil)
  raise ArgumentError, 'code must be a String' unless code.is_a?(String)

  @cry_wasm[:codegen].crystal_code_blocks << code
  export = [export] if export.is_a?(String)
  return if export.nil?

  export.each do |e|
    e = e.to_s if e.is_a?(Symbol)
    @cry_wasm[:codegen].function_names << e
  end
end

#cry_load(file, basedir = __dir__) ⇒ Boolean

Loads the crystal code before compiling to WASM.

Parameters:

  • file (String)

    crystal file path

  • basedir (String) (defaults to: __dir__)

    base directory

Returns:

  • (Boolean)

    true



56
57
58
59
60
61
# File 'lib/cry/wasm.rb', line 56

def cry_load(file, basedir = __dir__)
  require 'pathname'
  file = Pathname.new(file).expand_path(basedir).to_s
  @cry_wasm[:codegen].load(file)
  true
end

#method_added(name) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/cry/wasm.rb', line 34

def method_added(name)
  return super(name) unless @cry_wasm[:flag]

  @cry_wasm[:codegen].add_crystal_function(
    name,
    @crystal_arg_types,
    @crystal_ret_type,
    @cry_wasm[:caller_line_number]
  )

  @cry_wasm[:marked_methods] << name
  @cry_wasm[:caller_line_number] = 0
  @cry_wasm[:flag] = false

  super(name)
end

#newObject



180
181
182
183
184
# File 'lib/cry/wasm.rb', line 180

def new(...)
  super(...).tap do
    @cry_wasm[:runtime].start if @cry_wasm[:runtime].instance
  end
end