Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiling and running scryer as a WebAssembly binary? #615

Open
rla opened this issue Jun 27, 2020 · 58 comments
Open

Compiling and running scryer as a WebAssembly binary? #615

rla opened this issue Jun 27, 2020 · 58 comments

Comments

@rla
Copy link

rla commented Jun 27, 2020

Hello @mthom! Would it be possible to compile and run scryer as a WebAssembly binary? I worked some time ago on SWI-Prolog WebAssembly port but it turned out to be rather difficult due to:

  • Clib/POSIX platform expectations (like getting stuck with the Emscripten's broken readdir and other functions);
  • Fighting with the build system (it requires intermediate binary to build itselt);
  • Awful difficulties with cross-compiling C as usual.

Also, it was quite inconvenient to provide API for JavaScript. It takes hundreds of lines of handwritten code to bind JS functions to low-level FLI, a SWI-Prolog's FFI.

More info: SWI-Prolog/roadmap#43

It seems like Rust has some support for WebAssembly (https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm).

What do you think:

  • Would it be possible to build scryer as a WebAssembly binary?
  • Could scryer be decoupled from filesystem/POSIX/threading/signals requirements and run without filesystem, clib or any syscalls at all?
  • Would it be possible to use wasm-bindgen to generate useful API? (It could be something quick-and-dirty like terms serialized as JSON strings at first)
@cduret
Copy link

cduret commented Jun 27, 2020

I have compiled scryer without signal (nix package) under windows OS

@tniessen
Copy link
Contributor

tniessen commented Jun 28, 2020

Would it be possible to build scryer as a WebAssembly binary?

I did that one or two months ago, based on c342d18. It seems possible, but it certainly isn't trivial.

Compilation problems

  1. The rug dependency doesn't work with WebAssembly. I exchanged a few emails with @tspiteri, and it seemed challenging to cross-compile GMP. Workaround: Remove the rug dependency and change the default features to ["num"], which uses num-rug-adapter.

  2. I couldn't get the crossterm crate to work, which is not really a surprise given the lack of a terminal in WebAssembly, but there might be a solution that I missed. Workaround: Remove crossterm and adapt get_single_char in src/prolog/machine/system_calls.rs to use an imported function raw_mode_single_char instead (which needs to be implemented in JavaScript):

    extern "C" {
      pub fn raw_mode_single_char() -> u32;
    }
    
    pub fn get_single_char() -> char {
      unsafe {
        return char::from_u32(raw_mode_single_char()).unwrap();
      }
    }
  3. ProcessTime won't work in WebAssembly. Workaround: Replace or remove ProcessTime::now() in src/prolog/machine/system_calls.rs.

  4. Signal handling doesn't work in WebAssembly. Workaround: Remove signal handling logic in src/main.rs. (Probably not necessary if you are building the project as a library instead of an executable binary.)

Cross-compiling

I don't remember the exact command I used to compile it, but I used rustup's nightly toolchain for the wasm32-wasi target.

Syscalls in the browser

I implemented necessary syscalls to an extent that allowed to load a prolog source file from a virtual in-browser file system, and to see output written to the stdout file descriptor. Apart from that, I left most of the wasi syscalls unimplemented, but someone with more time could probably get most of them to work.

Result

I got this tiny "Hello world" to work eventually:

:- initialization(hello_world).

hello_world :-
    write('Hello, World!'), nl.

Partial screenshot of the web page (with some debugging info):

prolog-webassembly

I didn't try much more after that, this had already taken me many hours and I mostly did it to gain some experience with Rust → WebAssembly.

@rla
Copy link
Author

rla commented Jun 29, 2020

@tniessen, thank you! That's awesome. Despite these issues, it seems like porting scryer would be easier.

  • SWI-Prolog wasm version also does not support GMP;
  • Unless using threads, terminal input cannot work anyway since the blocking read call would also block the browser event loop including input events to read user input. SWI-Prolog wasm version has exactly the same issue.
  • I see that all IO is collected to system_calls.rs module, this also simplifies things.

@tniessen, do you happen to still have your changes somewhere? It would be a nice starting point.

@tniessen
Copy link
Contributor

Unless using threads, terminal input cannot work anyway since the blocking read call would also block the browser event loop including input events to read user input. SWI-Prolog wasm version has exactly the same issue.

@rla It might be possible to avoid this in some browsers with only minimal modifications in scryer-prolog. I wrote synchronous-channel a while back to allow WebAssembly in worker threads to retrieve information (such as terminal input) from the main thread. I did not test the library in browsers, but it should work. So the idea would be for the main thread to write terminal input to a SynchronousChannel, and a worker thread running the WebAssembly code could read it from the SynchronousChannel. The library supports both blocking and non-blocking read and write operations.

do you happen to still have your changes somewhere? It would be a nice starting point.

I'll try to find and commit them, but it might take a few days.

@ghost ghost mentioned this issue Jun 30, 2020
@jacobfriedman
Copy link

Very interested in a wasm module with TCP/UDP bindings. I'm trying to build the SWI-Prolog WASM just to see where I can find those calls... any idea if I'll be able to sniff those calls out with a nodeJS WASI wrapper?

@jacobfriedman
Copy link

@tniessen when you're ready to dedicate some time to get a WASM going, I'm right here with @rla . I'd like to get an environment up... 'one to bind them all' with prolog - scryer's the most lean implementation I've seen. Please let me know if you're willing to work together :)

@tniessen
Copy link
Contributor

Sorry @jacobfriedman and @rla, I am super busy with university, OSS, and everything else right now, but it's still on my list. I'd love to collaborate on this, and I'll try to find time soon-ish, I hope.

@tniessen
Copy link
Contributor

tniessen commented Apr 7, 2021

@jacobfriedman @rla I am trying to get it to work again but recent changes have only made it more difficult.

  • crossterm doesn't work, this affects keyboard events, workaround in Compiling and running scryer as a WebAssembly binary? #615 (comment)
  • hostname doesn't work (not important)
  • rustyline doesn't work, this affects essentially everything related to user input
  • ring doesn't work (but might with some hacks)
  • openssl doesn't work (but might with some hacks)
  • native-tls doesn't work
  • sodiumoxide doesn't work (but might with some hacks)
  • slice-dequeue doesn't work

I am not sure why there are so many different crypto libraries in the list of dependencies.

TCP (the sockets library) won't work in WebAssembly, assuming it's supposed to run in a browser.

@jacobfriedman
Copy link

Well, a red flag for me:
"I am not sure why there are so many different crypto libraries in the list of dependencies."
@mthom is there a workaround?

What I've been seeing is that many libraries just wrap C instead of implementing in the native language. I wonder if these dependencies can be replaced with native (rust) drop-ins.

Are you using emscripten to compile, or compiling straight to WASM?

@triska
Copy link
Contributor

triska commented Apr 7, 2021

Thank you a lot for resuming work on this project!

The cryptographic crates are used to support various cryptographic predicates in library(crypto), and also TLS connections in library(sockets).

The big bouquet of crates is needed because none of the existing crates covers all the functionality that library(crypto) exposes. Some of these crates are more convenient to use than others, and all of them have unique functionality that only they provide, at least in this simplicity.

If you do not need this functionality, you can, in your WASM branch, simply remove all cryptographic predicates from system_calls.rs, starting from

&SystemClauseType::CryptoRandomByte => {
up to and including
&SystemClauseType::Curve25519ScalarMult => {

and then simply remove the inclusion of these crates. This could be useful to start with this project. If at all possible, it would be great to preserve the cryptographic functionality as far as practical also in the WASM port.

Please let me know if you have any questions about this.

@jacobfriedman
Copy link

Thank you for the help! @tniessen will you take the wheel on this, as I'm relocating this week? If so please fork/branch & post the URL here :)

@triska
Copy link
Contributor

triska commented Apr 7, 2021

One additional remark: If you plan to get one of the cryptographic crates working, I suggest to focus on ring. This is the most useful and well designed of the cryptographic crates, and porting it would enable a lot of the cryptographic functionality, most notably secure hashes, encryption and authentication.

The other crates are used for more rarely used hashes, and low-level reasoning over elliptic curves. I plan to get rid of specifically the openssl dependency as soon as a native Rust crate provides the needed functionality for elliptic curves.

@infogulch
Copy link
Contributor

simply remove all cryptographic predicates from system_calls.rs ...

and then simply remove the inclusion of these crates

I wonder if this would be a good use-case for a cfg flag (crypto perhaps?), enabled by default, that if disabled omits the crypto-related system calls and their dependencies. This way, if the project is being built for WASM the cfg flag could simply be disabled.

@tniessen
Copy link
Contributor

tniessen commented Apr 8, 2021

@tniessen will you take the wheel on this, as I'm relocating this week?

Sure, I'll try to get a minimal version to work.

@jacobfriedman
Copy link

jacobfriedman commented Apr 8, 2021 via email

@tniessen
Copy link
Contributor

tniessen commented Apr 8, 2021

@jacobfriedman No, I am trying to get it to work with cargo wasi. Emscripten might come in handy when we need to provide the WASI environment in the browser, but I'd personally prefer a "clean" build without Emscripten first.

@tniessen
Copy link
Contributor

tniessen commented Apr 8, 2021

Oh, and best of luck on your move :)

@jacobfriedman
Copy link

jacobfriedman commented Apr 8, 2021 via email

@tniessen
Copy link
Contributor

tniessen commented Apr 9, 2021

I made the crypto and terminal dependencies optional, but getting rid of slice-deque seems like a bigger challenge here. That will probably become a large diff.

@jacobfriedman
Copy link

jacobfriedman commented Apr 9, 2021 via email

@tniessen
Copy link
Contributor

tniessen commented Apr 9, 2021

There must be a better way than a huge diff.

If I figure out how to redefine the sdeq! macro and if this crate doesn't actually use any properties of SliceDeque that aren't provided by VecDeque (it does), it might be quite small. (I'm very new to Rust, so we'll see.)

Edit: the APIs of SliceDeque and VecDeque have subtle differences and are not interchangeable. make_contiguous can be used to access a VecDeque as a slice. I suppose the only way to solve this without causing an even bigger mess is to define another Deque implementation that either uses SliceDeque or VecDeque internally, but I'm struggling with the implementation.

@triska
Copy link
Contributor

triska commented Apr 10, 2021

Is it worth trying to ask the maintainers of the affected crates for the implementation of the needed functionality to simplify this change? You can link to this issue to explain the motivation for the needed features.

@jacobfriedman
Copy link

jacobfriedman commented Apr 10, 2021

Ah, the tangled web of dependency maintenance. Who's going to maintain the maintainers?

@triska, I remember in one of your PoP videos you spoke of depending on systems for not months, not years, but decades. I left the node/npm ecosystem because of careless package dependency.

How is it possible to lessen the dependencies so the installation is unaffected by external forces?

@tniessen
Copy link
Contributor

tniessen commented Apr 11, 2021

Is it worth trying to ask the maintainers of the affected crates for the implementation of the needed functionality to simplify this change?

According to the maintainers, SliceDeque cannot work in WebAssembly because of the assumptions SliceDeque makes about memory management within the host system.

We can replace SliceDeque with VecDeque for cross-compilation to WebAssembly, but the APIs are not compatible. For example, the return type of ::remove is different, and, because VecDeque isn't backed by a slice, it doesn't deref into a slice, so the only way to access it as a slice is to call make_contiguous first.

It would be great if the APIs were compatible, but that seems impossible at this point, and would mean breaking changes for the maintainers of those crates.

So the best option might still be our own Deque implementation that only provides the features we need, and that either uses SliceDeque or VecDeque internally. That's the approach I went with so far, but I am struggling with a proper implementation. (Also, it might end up being super slow in WebAssembly.)

@mpabst
Copy link

mpabst commented Aug 23, 2021

@tniessen et al: Would you like some help finishing this? I'm interested in a good WASM Prolog too and have free time to contribute.

@triska
Copy link
Contributor

triska commented Sep 20, 2021

@mpabst: I only want to add that I would greatly appreciate you looking into targeting WASM, and please let me know if I can help in any way by ensuring that the cryptographic libraries can be ported! It seems that simply outlining the steps and issues you encountered would already be a great contribution! Thank you a lot!

@triska
Copy link
Contributor

triska commented Feb 12, 2022

@tniessen: Please have a look at the latest developments in the rebis-dev branch:

https://github.com/mthom/scryer-prolog/tree/rebis-dev

Notably, db20cda changes many occurrences of SliceDeque to VecDeque! Does that help for the WASM port?

@rujialiu
Copy link

Ok! I've made a PR. @triska It looks like there are still a lot of things left to do to make it truly usable, but I'll try :)

@rujialiu
Copy link

I have changed all the commented codes to use guards like cfg(feature = "ffi") and cfg(feature = "http"). I guess it's better to make a separate PR from these changes?
BTW: I found there is a ring-wasi crate which works for wasi and the original ring already works with wasm32 web so I'm leaving ring a mandatory dependency, otherwise I'll write a lot more cfg guards :)

I'll hopefully push the changes tomorrow in the existing PR first.

@triska
Copy link
Contributor

triska commented Aug 19, 2023

Thank you a lot, this sounds awesome! It is great to hear that ring and thus all the cryptographic functionality of Scryer Prolog will be available in the WASM build!

While you are making so fast progress, you can also simply force-push to the branch so that always the latest changes are easily visible for everyone interested in this topic! And yes, for self-contained changes that are immediately applicable and which do not cause any regressions (on any platform), please do file separate PRs if possible, so that they can be easily merged as soon as possible. I look forward to all improvements in these areas!

rujialiu pushed a commit to rujialiu/scryer-prolog that referenced this issue Aug 20, 2023
@rujialiu
Copy link

I'm made another PR for the optional features, most of my changes are there. After that is merged, the more "hardcore" 32-bit related changes will be much easier to review. There is one thing though: exactly one crypto function is implemented using sodiumoxide, which has to be marked as an optional feature. It will be great if that can be implemented with ring instead, so we can remove that dependency. Is it possible? @triska

rujialiu pushed a commit to rujialiu/scryer-prolog that referenced this issue Aug 20, 2023
@triska
Copy link
Contributor

triska commented Aug 20, 2023

@rujialiu: Thank you so much for working on this!

Yes, it seems possible that we can use the excellent crrl crate by @pornin to get rid of the sodiumoxide dependency. crrl is currently already used by Scryer for secp256k1. I will look into this, please do leave this functionality as optional for now so that the compilation works and this work can continue. Thank you a lot!

triska added a commit to triska/scryer-prolog that referenced this issue Aug 20, 2023
This is to facilitate WASM compilation as currently worked on
by @rujialiu in mthom#615. Many thanks, and many thanks to @pornin
for crrl which makes this possible!
@triska
Copy link
Contributor

triska commented Aug 20, 2023

I have filed #1970 to eliminate the sodiumoxide dependency by using crrl!

rujialiu pushed a commit to rujialiu/scryer-prolog that referenced this issue Aug 21, 2023
@rujialiu
Copy link

Anyone interested in supporting wasix? It's relatively new, wasmer-only (currently), but the featureset is too attractive. Here is a list from https://wasmer.io/posts/announcing-wasix

  • full support for efficient multithreading
  • full support for sockets (socket, bind, connect)
  • changing the current directory (chdir)
  • setjmp / longjmp support (used extensively in libc ) via asyncify wizardy
  • pthreads support
  • process forking (fork and vfork )
  • subprocess spawning and waiting (exec , wait )
  • TTY support
  • asynchronous polling of sockets and files
  • pipe and event support (pipe, event )
  • DNS resolution support (resolve )

So for example hyper should work out-of-box. But personally I don't need these in near future :)

@rujialiu
Copy link

I decided to tackle i686 32-bit platform before wasi because it's easier to debug :) Here is current output of cargo test:

running 24 tests

test parser::char_reader::tests::plain_string ... ok
test parser::char_reader::tests::greek_string ... ok
test parser::char_reader::tests::russian_string ... ok
test machine::stack::tests::stack_tests ... ok
test parser::char_reader::tests::armenian_lorem_ipsum ... ok
test arena::tests::float_ptr_cast ... ok
test parser::char_reader::tests::greek_lorem_ipsum ... ok
test machine::mock_wam::tests::is_cyclic_term_tests ... ok
test arena::tests::heap_cell_value_const_cast ... ok
test machine::mock_wam::tests::test_term_compare ... ok
test machine::copier::tests::copier_tests ... ok
test machine::mock_wam::tests::test_unify_with_occurs_check ... ok
test machine::partial_string::test::pstr_iter_tests ... ok
test machine::arithmetic_ops::tests::arith_eval_by_metacall_tests ... ok
test heap_iter::tests::heap_stackful_post_order_iter ... ok
test machine::mock_wam::tests::unify_tests ... ok
test heap_iter::tests::heap_stackful_iter_tests ... ok
test parser::char_reader::tests::russian_lorem_ipsum ... ok
test heap_print::tests::term_printing_tests ... ok
test atom_table::atomtable_is_not_concurrency_safe - should panic ... ok
test heap_iter::tests::heap_stackless_post_order_iter ... FAILED
test machine::gc::tests::heap_marking_tests ... FAILED
test arena::tests::heap_put_literal_tests ... FAILED
test heap_iter::tests::heap_stackless_iter_tests ... FAILED

failures:

---- arena::tests::heap_put_literal_tests stdout ----
thread 'arena::tests::heap_put_literal_tests' panicked at 'value contains invalid bit pattern for field ArenaHeader.tag: InvalidBitPattern { invalid_bytes: 0 }', src\arena.rs:167:5

---- machine::gc::tests::heap_marking_tests stdout ----
thread 'machine::gc::tests::heap_marking_tests' panicked at 'assertion failed: `(left == right)`
  left: `HeapCellValue { tag: Atom, name: "f", arity: 0, m: false, f: false }`,
 right: `HeapCellValue { tag: Atom, name: "f", arity: 2, m: false, f: false }`', src\machine\gc.rs:376:9

---- heap_iter::tests::heap_stackless_post_order_iter stdout ----
thread 'heap_iter::tests::heap_stackless_post_order_iter' panicked at 'assertion failed: `(left == right)`
  left: `Some(HeapCellValue { tag: Atom, name: "f", arity: 0, m: false, f: false })`,
 right: `None`', src\heap_iter.rs:2522:13

---- heap_iter::tests::heap_stackless_iter_tests stdout ----
thread 'heap_iter::tests::heap_stackless_iter_tests' panicked at 'assertion failed: `(left == right)`
  left: `Some(HeapCellValue { tag: Atom, name: "f", arity: 0, m: false, f: false })`,
 right: `None`', src\heap_iter.rs:540:13

failures:
    arena::tests::heap_put_literal_tests
    heap_iter::tests::heap_stackless_iter_tests
    heap_iter::tests::heap_stackless_post_order_iter
    machine::gc::tests::heap_marking_tests

test result: FAILED. 20 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Could @mthom or @triska make some guesses about potential causes? Since some tests already passed, but some are not, maybe you can guess from "what's different between those tests", which I'm totally clueless... Or maybe some more low-level tests that I can understand easier? Thanks a lot!

@rujialiu
Copy link

Getting closer! Only 3 tests failing.

It looks like UntypedArenaPtr points to an ArenaHeader (instead of a pointer to it) which is stored at the end of AllocSlab, so I changed this:

    fn header_offset_from_payload() -> usize {
        mem::size_of::<*const ArenaHeader>()
    }

to:

    fn header_offset_from_payload() -> usize {
        mem::size_of::<ArenaHeader>()
    }

However, for some reason, in target i686-pc-windows-msvc, ArenaHeader is NOT at the end of AllocSlab, so I added a #[repr(C)] to AllocSlab.

After fixing some other minor issue that I forgot, only three tests are failing: heap_stackless_iter_tests, heap_marking_tests and heap_stackless_post_order_iter.

And after skipping these test, all remaining 23 test can also pass:

running 23 tests
test src_tests::facts ... ok
test issues::handle_residual_goal ... ok
test issues::do_not_duplicate_path_components ... ok
test issues::display_constraints ... ok
test issues::occurs_check_flag ... ok
test issues::op3 ... ok
test issues::occurs_check_flag2 ... ok
test issues::compound_goal ... ok
test issues::multiple_goals ... ok
test issues::ignored_constraint ... ok
test issues::no_stutter ... ok
test src_tests::builtins ... ok
test src_tests::setup_call_cleanup_process ... ok
test src_tests::syntax_error ... ok
test src_tests::hello_world ... ok
test src_tests::clpz_load ... ok
test issues::call_0 ... ok
test src_tests::call_with_inference_limit ... ok
test src_tests::iso_conformity_tests ... ok
test src_tests::predicates ... ok
test issues::atomtable_is_not_concurrency_safe - should panic ... ok
test src_tests::setup_call_cleanup_load ... ok
test src_tests::rules ... ok

test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 40.32s

So I guess we're pretty close to getting i686-pc-windows-msvc to work! Any hunch/suggestions are welcome! For example, are there any other fixed-layout structs that needs [repr(C)] as well? @mthom @triska

BTW: It's a bit surprising that failing these 3 tests did not prevent us from succeeding these "prolog program tests" :)

@rujialiu
Copy link

Ah, I forgot that dashu depends on his own num-order which in turn depends on published num-modular 0.5.1 which doesn't compile on i686-pc-windows-msvc. However, Scryer-prolog's forked num-modular (which is marked as 0.6.0 internally) actually fixed it. How can I make num-order (which isn't forked) to use the new fork? (Locally I just copied 0.6.0 codes to overwrite 0.5.1 :P)

@triska
Copy link
Contributor

triska commented Aug 21, 2023

Awesome, thank you a lot! Could you please push these changes somewhere (I recommend a PR) and explain how we can try them? I am very interested in 32-bit support!

@rujialiu
Copy link

Ok! Great to see the "optional-features" merged, will try to make a new PR soon.

rujialiu pushed a commit to rujialiu/scryer-prolog that referenced this issue Aug 22, 2023
mthom added a commit that referenced this issue Aug 25, 2023
@triska
Copy link
Contributor

triska commented Aug 26, 2023

@rla: The branch that @rujialiu provides in #1966 can now be compiled to WASM, could you please take a look and see how it works for you? I would greatly appreciate your input. Thank you a lot!

@rla
Copy link
Author

rla commented Aug 29, 2023

@rla: The branch that @rujialiu provides in #1966 can now be compiled to WASM, could you please take a look and see how it works for you? I would greatly appreciate your input. Thank you a lot!

It will likely take some time. I have zero ideas about Rust. Most of the original problems with SWI port got eventually solved and building the wasm version of SWI is now officially supported. It also has auto-yielding from VM which solved the blocked terminal problem. The hundreds of lines of handwritten JS binding code are now distributed with SWI. All of this is wrapped into the very high-quality swipl-wasm NPM package with Typescript bindings. Building everything is done automatically through GH Actions. Getting there took years of work from many people.

Currently the biggest issue with scryer-wasm I see is the lack of clear building instructions. I have zero ideas how to compile any Rust code. It should be a list of commands that you can copy-paste into a terminal and execute them. Even better would be a docker file that installs all required tools, pull code, applies patches, builds everything and finally gives you the runnable files. Some sort of online demo would be really nice.

Beyond there could be some work on the actual interfaces for the Prolog user: how to interact with the browser; use the DOM; run code from JS etc.

@triska
Copy link
Contributor

triska commented Aug 29, 2023

@rla: I have added build instructions at #1966 (comment), I hope this helps!

rujialiu pushed a commit to rujialiu/scryer-prolog that referenced this issue Sep 2, 2023
…ently 6/12 crypto functions supported. Tested in browser with all default features disabled.
rujialiu pushed a commit to rujialiu/scryer-prolog that referenced this issue Sep 8, 2023
…ently 6/12 crypto functions supported. Tested in browser with all default features disabled.
mthom added a commit that referenced this issue Sep 8, 2023
Basic WebAssembly support with minimal Javascript API #615
@triska
Copy link
Contributor

triska commented Sep 8, 2023

@rla: Scryer Prolog can now be compiled to WebAssembly, and the README explains the necessary steps, please take a look:

https://github.com/mthom/scryer-prolog#building-webassembly

@infogulch: Do you think it would be possible to build such a version automatically as one of the configured targets?

@triska
Copy link
Contributor

triska commented Sep 16, 2023

@rujialiu: Regarding wasmer, there is currently an interesting discussion in Zig that seems worth considering, for example I found this recent post by a Zig developer:

ziglang/zig#17115 (comment)

@rujialiu
Copy link

Yes, I read that one as soons as it was posted because I'm following zig's development 8-) @triska

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants