Skip to content

Instantly share code, notes, and snippets.

@jwoertink
Last active February 7, 2022 01:56
Show Gist options
  • Save jwoertink/b8fb8430ed819dd4041a56ff6ed3ef64 to your computer and use it in GitHub Desktop.
Save jwoertink/b8fb8430ed819dd4041a56ff6ed3ef64 to your computer and use it in GitHub Desktop.

Crystal on WASM

Running

  • crystal wasm.cr
  • php -S localhost:3000
  • open your browser
  • open dev tool js console
  • crystal.add(1, 2)

Explanation

This writes the WASM bytes with the built-in add_function named add to a file called crystal.wasm. Then a fetch call loads the result in to WebAssembly, and puts that function in to a JS object called crystal.

You're calling an add function from WASM that was generated using crystal... It's kinda neat, but pretty useless.

struct Int32
# this is a patch because Crystal (or ruby for that matter)
# don't have this `>>>` zero-fill right shift bitwise operator
# https://github.com/rjanse/demo-s3-swf-upload/blob/7acf16be7d2f75c42935aa1aa9af10b73d4376f7/vendor/plugins/s3-swf-upload-plugin/lib/patch/integer.rb#L8
def zfrs(n : Int32)
self >> n & (2**(32-n)-1)
end
end
module Encoding
# https://www.geeksforgeeks.org/ieee-standard-754-floating-point-numbers/
def ieee754(n : UInt8)
String.build do |io|
io.write_byte n
end
end
def encode_string(str : String) : Array(Int32)
[str.size] + str.codepoints
end
# https://docs.rs/leb128/0.2.4/leb128/
def signed_LEB128(n : Int) : Array(Int32)
buffer = [] of Int32
more = true
while more
byte = n & 0x7f
n = n.zfrs(7)
if (n == 0 && (byte & 0x40) == 0) || (n == -1 && (byte & 0x40) != 0)
more = false
else
byte |= 0x80
end
buffer << byte
end
buffer
end
def unsigned_LEB128(n : Int)
buffer = [] of Int32
loop do
byte = n & 0x7f
n = n.zfrs(7)
if n != 0
byte |= 0x80
end
buffer << byte
break if n == 0
end
buffer
end
end
window.onload = ()=> {
window.crystal = {}
fetch("crystal.wasm")
.then(res => res.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes))
.then(obj => crystal = obj.instance.exports)
}
<html>
<body>
<script src="glue.js"></script>
</body>
</html>
require "./encoding"
include Encoding
# https://webassembly.github.io/spec/core/binary/modules.html#sections
enum Section : UInt8
Custom
Type
Import
Func
Table
Memory
Global
Export
Start
Element
Code
Data
end
# https://webassembly.github.io/spec/core/binary/types.html
module Valtype
I32 = 0x7f
I64 = 0x7e
F32 = 0x7d
end
# https://webassembly.github.io/spec/core/binary/instructions.html
module Opcodes
End = 0x0b
GetLocal = 0x20
F32Add = 0x92
end
# http://webassembly.github.io/spec/core/binary/modules.html#export-section
module ExportType
Func = 0x00
Table = 0x01
Mem = 0x02
Global = 0x03
end
# http://webassembly.github.io/spec/core/binary/types.html#function-types
FunctionType = 0x60
EmptyArray = 0x0
# https://webassembly.github.io/spec/core/binary/conventions.html#binary-vec
def encode_vector(data) : Array(UInt8)
size = [data.size.to_u8] of UInt8
size + data.flatten.map(&.to_u8)
end
# https://webassembly.github.io/spec/core/binary/modules.html#sections
def create_section(type : Section, data) : Array(UInt8)
type = [type.value] of UInt8
type + encode_vector(data)
end
magic_module_header = [0x00, 0x61, 0x73, 0x6d] of UInt8
module_version = [0x01, 0x00, 0x00, 0x00] of UInt8
header = magic_module_header + module_version
add_function_type = [FunctionType] + encode_vector([Valtype::F32, Valtype::F32]) + encode_vector([Valtype::F32])
type_section = create_section(Section::Type, encode_vector([add_function_type]))
func_section = create_section(Section::Func, encode_vector([0x00]))
export_section = create_section(Section::Export, encode_vector([encode_string("add") + [ExportType::Func, 0x00]]))
code = [
Opcodes::GetLocal,
unsigned_LEB128(0),
Opcodes::GetLocal,
unsigned_LEB128(1),
Opcodes::F32Add
].flatten
function_body = encode_vector([
EmptyArray,
code,
Opcodes::End
].flatten)
code_section = create_section(Section::Code, encode_vector([function_body]))
buffer = (header + type_section + func_section + export_section + code_section)
File.open("crystal.wasm", "wb") do |f|
ptr = (buffer.to_unsafe.as(UInt8*))
f.write(ptr.to_slice(buffer.size * sizeof(UInt8)))
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment