Thank you, with 0.0.1 it works!
Saw this news in twitter.
@lbguilherme phenomenal work on WASM support for Crystal, can’t thank you enough
I’ve got the basic example working from the Crystal 1.4.0 is released! - The Crystal Programming Language announcement.
Having an issue trying to get an exported sum
function runnable.
Crystal code sum.cr:
# Exported functions need to use `fun`. They must be fully typed.
fun sum(a : Int32, b : Int32) : Int32
return a + b
end
Crystal version:
Crystal 1.5.1 [2d2b7799c] (2022-09-08)
LLVM: 10.0.0
Default target: x86_64-unknown-linux-gnu
Notes: also attempted using nightly build 1.6.0-dev
, but that gives an unrelated error I believe due to new WASM functionality coming down the pipe for that version.
LLD version: LLD 12.0.0
Compiling / linking via:
crystal build sum.cr --cross-compile --target wasm32-wasi
wasm-ld-12 sum.wasm -o sum-final --export=sum -lc -L"$PWD/wasm32-wasi-libs" -lpcre
Notes: I’ve tried with and without --export
and --export sum
Receiving this error with wasmer cli:
wasmer sum-final -i sum 1 2
error: failed to run `sum-final`
╰─▶ 1: Error while importing "wasi_snapshot_preview1"."args_get": unknown import. Expected Function(FunctionType { params: [I32, I32], results: [I32] })
Any direction and help is greatly appreciated, I’m probably overlooked something simple!
Hi @noaheverett!
Currently Crystal targets wasm32-wasi
: a WebAssembly environment with WASI enabled. It does not target wasm32-unknown
: a pure WebAssembly environment without WASI. Using WASI allows for most of the stdlib to work unchanged.
This is only an issue because wasmer
disables WASI when using the invoke flag (-i
). This behavior was reported here Using WASI from wasmer cli · Issue #2978 · wasmerio/wasmer · GitHub and here Support no_main wasi binaries · Issue #1716 · wasmerio/wasmer · GitHub.
Finally, this issue was resolved by this PR: Fix "run --invoke [function]" to behave the same as "run" by fschutt · Pull Request #2997 · wasmerio/wasmer · GitHub and was released as wasmer 3.0.0-alpha. The final 3.0.0 version isn’t released yet.
For now you can try with wasmtime
, it will work!
wasmtime run --invoke sum sum-final.wasm 1 2
Also, starting with Crystal 1.6.0 you won’t need to add --export sum
anymore.
Tip: you can build in a single command without a cross-compile step:
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs crystal build sum.cr -o sum.wasm --target wasm32-wasi --link-flags="--export sum"
Oh wow that’s great thank you very much for the help, it works with wasmtime! That new build command is way easier as well, cheers!
This makes me think a pure Crystal based WASM runtime would be sweet, like Go’s Wazero
Looking forward to Crystal 1.6 and thank you for all your work on it.
Happy that worked! Out of curiosity, what is your use case?
About a runtime, there is GitHub - naqvis/wasmer-crystal: WebAssembly runtime for Crystal for running Wasmer inside Crystal.
Maintaining an entirely new wasm runtime would be a lot of work for very little benefit, IMHO.
That’s the shard I’m looking to use and I’ve been testing the Ruby and Crystal version of the wasmer runtime, but I’m getting a similar error as above, but I’m hoping that will be resolved with wasmer v3.0 as you listed above
Trying to run the same sum.wasm file that works with wasmtime
gives below error for wasmer-crystal
:
Unhandled exception: Missing hash key: "wasi_snapshot_preview1" (KeyError)
from /usr/share/crystal/src/hash.cr:1077:11 in '[]'
from lib/wasmer/src/wasmer/import.cr:25:86 in 'into_inner'
from lib/wasmer/src/wasmer/instance.cr:23:17 in 'initialize'
from lib/wasmer/src/wasmer/instance.cr:22:5 in 'new'
from lib/wasmer/src/wasmer/instance.cr:32:7 in 'new'
from src/test.cr:23:12 in '__crystal_main'
from /usr/share/crystal/src/crystal/main.cr:115:5 in 'main_user_code'
from /usr/share/crystal/src/crystal/main.cr:101:7 in 'main'
from /usr/share/crystal/src/crystal/main.cr:127:3 in 'main'
from /lib/x86_64-linux-gnu/libc.so.6 in '__libc_start_main'
from /home/vagrant/.cache/crystal/crystal-run-test.tmp in '_start'
from ???
Code:
require "wasmer"
# Let's define the engine, that holds the compiler.
engine = Wasmer::Engine.new
# Let's define the store, that holds the engine, that holds the compiler.
store = Wasmer::Store.new(engine)
# Let's compile the module to be able to execute it!
module_ = Wasmer::Module.new store, File.read("/vagrant/wasm/sum.wasm")
# Now the module is compiled, we can instantiate it.
instance = Wasmer::Instance.new module_
# get the exported `sum` function
# function methods returns nil if it can't find the requested function. we know its there, so let's add `not_nil!`
sum = instance.function("sum").not_nil!
# Call the exported `sum` function with Crystal standard values. The WebAssembly
# types are inferred and values are casted automatically.
result = sum.call(5, 37)
puts result # => 42
It seems like you have to expose WASI manually. See this example: wasmer-crystal/wasi.cr at main · naqvis/wasmer-crystal · GitHub
Oh boy my ignorance is showing You’re right again, I appreciate it @lbguilherme!
In case anyone needs it, below is the full code I used to run a Crystal wasm module with wasi and call an exported function via the wasmer-crystal
shard:
Note: most of the code was pulled from examples/wasi.cr
from wasmer-crystal
require "wasmer"
# Let's get the `wasi.wasm` bytes
file = File.read("/vagrant/wasm/sum.wasm")
# Create a store
store = Wasmer.default_engine.new_store
# Let's compile the Wasm module, as usual
module_ = Wasmer.module(store, file)
# Here we go.
#
# First, let's extract the WASI version from the module. Why? Because
# WASI already exists in multiple versions, and it doesn't work the
# same way. So, to ensure compatibility, we need to know the version.
wasi_version = Wasmer::Wasi.version(module_)
# Second, create a `Wasmer::Wasi::Environment`. It contains everything related
# to WASI. To build such an environment, we must use the
# `Wasmer::Wasi::StateBuilder`.
#
# In this case, we specify the program name is `wasi_test_program`. We
# also specify the program is invoked with the `--test` argument, in
# addition to two environment variable: `COLOR` and
# `APP_SHOULD_LOG`. Finally, we map the `the_host_current_dir` to the
# current directory. There it is:
# NOTE: CHANGE THESE TO SUIT YOUR USE CASE
env = Wasmer::Wasi::StateBuilder.builder("wasi_test_program") {
with_arg("--test")
with_env("COLOR", "true")
with_env("APP_SHOULD_LOG", "false")
with_map_dir("the_host_current_dir", ".")
with_capture_stdout
}
imp_obj = env.generate_import_object(store, module_)
# Now can instantiate the module
instance = Wasmer.new_instance(module_, imp_obj)
#instance.wasi_start_func.call
sum = instance.function("sum").not_nil!
# Call the exported `sum` function with Crystal standard values. The WebAssembly
# types are inferred and values are casted automatically.
result = sum.call(11, 22)
puts result
Compile command:
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs crystal build sum.cr -o sum.wasm --target wasm32-wasi --link-flags="--export sum"
sum.cr:
# Exported functions need to use `fun`. They must be fully typed.
fun sum(a : Int32, b : Int32) : Int32
return a + b
end
If it’s possible to support String
? I got this error when use String
as parameter of fun
:
# Exported functions need to use `fun`. They must be fully typed.
fun sum(a : String) : String
return a
end
then build to wasm and got error:
Showing last frame. Use --error-trace for full trace.
In tmp.cr:3:13
3 | fun sum(a : String) : String
^-----
Error: only primitive types, pointers, structs, unions, enums and tuples are allowed in lib declarations, not String
when doing C Bindings or declaring fun
you need to follow C semantics. C doesn’t have concept of String
, so you should declare that as LibC::Char*
or simply UInt8*
.
HIH
If it’s possible to export module
or class
or def
or mose beside fun
when crystal build to webassembly?
WebAssembly only has numeric types, there is no such thing as String or classes. Thus, you can only export simple fun
functions with primitive numeric arguments. WASI by itself does not define the concept of “string”. But as the raw memory is exported it is possible to write low level functions that will take pointers and manipulate the memory to transfer arbitrary data.
If you are running WebAssembly on the Web or with a JS runtime such as Node.js or Deno, you can use this shard: GitHub - lbguilherme/crystal-js: Bindings to use Crystal compiled to WebAssembly in a JavaScript environment, such as Node.js, Deno or the Web.
It allows you to call JS methods and export def
methods with more complex types. For example:
require "js"
JS.export def split_spaces(data : String) : Array(String)
data.split(" ").map &.strip
end
And then it will generate a .js module that exports this function and handles the data conversion internally.
It is still a proof of concept and doesn’t yet support many types.
My one use case is:
1. use Python code to do most of one task, and I want to try write Crystal code(ex: core.cr
) for core part of the task which cost time.
2.Then crystal build core.cr
to got core.wasm
then invoke fun
of core.wasm
in the Python code by wasm runtime(wasmer
or wasmtime
).
3.The argument of fun
in core.wasm
usually is number
or String
or other.
How to pass a String argument from Python into Crystal:
Crystal side:
fun get_string_type_id : Int32
String::TYPE_ID
end
fun print_string(str : Void*)
str = str.as(String)
p str
end
Build command (for Crystal 1.5):
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs crystal build --target wasm32-wasi test.cr -o test.wasm --link-flags="--export print_string --export __crystal_malloc_atomic --export get_string_type_id"
Python side, using wasmer-python:
from wasmer import wasi, Store, Module, Instance
import codecs
import os
__dir__ = os.path.dirname(os.path.realpath(__file__))
wasm_bytes = open(__dir__ + '/test.wasm', 'rb').read()
store = Store()
module = Module(store, wasm_bytes)
wasi_version = wasi.get_version(module, strict=True)
wasi_env = wasi.StateBuilder('test').finalize()
import_object = wasi_env.generate_import_object(store, wasi_version)
instance = Instance(module, import_object)
instance.exports._start()
crystal_string_type_id = instance.exports.get_string_type_id()
def make_crystal_string(str):
str_bytes = codecs.encode(str, 'utf-8')
pointer = instance.exports.__crystal_malloc_atomic(13 + len(str_bytes))
header = instance.exports.memory.uint32_view(offset = pointer // 4)
header[0] = crystal_string_type_id # typeid
header[1] = len(str_bytes) # bytesize
header[2] = len(str) # size
body = instance.exports.memory.uint8_view(offset = pointer + 12)
for i in range(len(str_bytes)):
body[i] = str_bytes[i]
body[len(str_bytes)] = 0 # null byte at the end
return pointer
instance.exports.print_string(make_crystal_string("Hello!"))
That solution uses a lot of Crystal’s runtime internals. That’s not a very pleasant API and could technically break when there are changes to the internal representation.
Instead, I’d recommend to either use C-style strings (char array). It can be passed to String.new
in Crystal and you can pass a pointer to such a string around through the WASM interface.
fun make_string(str : Void*)
String.new(str.as(LibC::Char*))
end
def make_crystal_string(str):
bytes = codecs.encode(str, 'utf-8')
crystal_string = instance.exports.make_string(ctypes.c_char_p(bytes))
return crystal_string
@straight-shoota it is still necessary to copy the data into the wasm instance memory before a pointer can be passed in. So this would be a solution using less internals:
fun alloc_buffer(size : Int32) : Void*
Pointer(Void).malloc(size)
end
fun make_string(data : LibC::Char*, bytesize : Int32) : Void*
String.new(data, bytesize).as(Void*)
end
fun print_string(str : Void*)
str = str.as(String)
p str
end
def make_crystal_string(str):
str_bytes = codecs.encode(str, 'utf-8')
pointer = instance.exports.alloc_buffer(len(str_bytes) + 1)
buffer = instance.exports.memory.uint8_view(offset = pointer)
for i in range(len(str_bytes)):
buffer[i] = str_bytes[i]
buffer[len(str_bytes)] = 0
return instance.exports.make_string(pointer, len(str_bytes))
The downside is that the data is copied twice. But now it uses only public API.
I didn’t know you can’t pass an externally allocated pointer into WASM. But I guess it makes sense.
To avoid double allocation you would usually use the yielding String
constructor. But I suppose that won’t work easily with the interface.
Maybe we could have something like Array.unsafe_build
for String
to use with such APIs.
Trying to require "http"
to manually build an HTTP::Request via passed in strings (not trying to do any HTTP requests since WASM by itself doesn’t support this), but getting some compile errors.
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs crystal build sum.cr -o sum.wasm --target wasm32-wasi
wasm-ld: error: unable to find library -lz (this usually means you need to install the development package for libz)
wasm-ld: error: unable to find library -lssl (this usually means you need to install the development package for libssl)
wasm-ld: error: unable to find library -lcrypto (this usually means you need to install the development package for libcrypto)
Error: execution of command failed with code: 1: `wasm-ld "${@}" -o /vagrant/glue/sum.wasm -lc -L/vagrant/glue/wasm32-wasi-libs -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'``
I’ve tried a few variations of passing /usr/lib/crystal/lib/x86_64-linux-gnu
path via the CRYSTAL_LIBRARY_PATH or --link-flags
but get this error:
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs:/usr/lib/crystal/lib/x86_64-linux-gnu/ crystal build sum.cr -o sum.wasm --target wasm32-wasi
wasm-ld: error: unknown file type: crc32.o
Error: execution of command failed with code: 1: `wasm-ld "${@}" -o /vagrant/glue/sum.wasm -lc -L/vagrant/glue/wasm32-wasi-libs -L/usr/lib/crystal/lib/x86_64-linux-gnu/ -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'``
-or-
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs crystal build sum.cr -o sum.wasm --target wasm32-wasi --link-flags="-L/usr/lib/x86_64-linux-gnu/"
wasm-ld: error: unknown file type: errno.o
Error: execution of command failed with code: 1: `wasm-ld "${@}" -o /vagrant/glue/sum.wasm -L/usr/lib/x86_64-linux-gnu/ -lc -L/vagrant/glue/wasm32-wasi-libs -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'``
My first thought was possibly the full Crystal stdlib isn’t compiled to WASM yet?
You can try building with -Dwithout_openssl
and -Dwithout_zlib
.
I don’t have a version of those libraries compiled for wasm32-wasi. (yet?)