From b6ce6b7cdb59c57d9ad4c5edf7d11e13ff49b948 Mon Sep 17 00:00:00 2001 From: Rujia Liu Date: Fri, 8 Sep 2023 17:27:22 +0800 Subject: [PATCH] Basic WebAssembly support with minimal Javascript API #615. Currently 6/12 crypto functions supported. Tested in browser with all default features disabled. --- Cargo.toml | 20 +++++++++- README.md | 71 ++++++++++++++++++++++++++++++++++ build/instructions_template.rs | 48 +++++++++++++++-------- src/atom_table.rs | 1 + src/lib.rs | 14 +++++++ src/machine/dispatch.rs | 12 ++++++ src/machine/mock_wam.rs | 13 +++++++ src/machine/mod.rs | 4 +- src/machine/system_calls.rs | 17 ++++++-- 9 files changed, 176 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 16f51100..c1e8ae2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,14 +12,18 @@ categories = ["command-line-utilities"] build = "build/main.rs" rust-version = "1.70" +[lib] +crate-type = ["cdylib", "rlib"] + [features] -default = ["ffi", "repl", "hostname", "tls", "http"] +default = ["ffi", "repl", "hostname", "tls", "http", "crypto-full"] ffi = ["dep:libffi"] repl = ["dep:crossterm", "dep:ctrlc", "dep:rustyline"] hostname = ["dep:hostname"] tls = ["dep:native-tls"] http = ["dep:hyper", "dep:reqwest"] rust_beta_channel = [] +crypto-full = [] [build-dependencies] indexmap = "1.0.2" @@ -80,7 +84,19 @@ tokio = { version = "1.28.2", features = ["full"] } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.10", features = ["js"] } -tokio = { version = "1.28.2", features = ["sync", "macros", "io-util", "rt", "time"] } +tokio = { version = "1.28.2", features = ["sync", "macros", "io-util", "rt"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +console_error_panic_hook = "0.1" +console_log = "1.0" +wasm-bindgen = "0.2.87" +wasm-bindgen-futures = "0.4" +serde-wasm-bindgen = "0.5" +web-sys = { version = "0.3", features = [ + "Document", + "Window", + "Element", +]} [target.'cfg(target_os = "wasi")'.dependencies] ring-wasi = { version = "0.16.25" } diff --git a/README.md b/README.md index bd55c050..49934052 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,77 @@ It will generate a very basic MSI file which installs the main executable and a Scryer Prolog must be built with **Rust 1.63 and up**. +### Building WebAssembly + +Scryer Prolog has basic WebAssembly support. You can follow `wasm-pack`'s [official instructions](https://rustwasm.github.io/docs/wasm-pack/quickstart.html) to install `wasm-pack` and build it in any way you like. + +However, none of the [default features](https://doc.rust-lang.org/cargo/reference/features.html#the-default-feature) are currently supported. The preferred way of disabling them is passing [extra options](https://rustwasm.github.io/wasm-pack/book/commands/build.html#extra-options) to `wasm-pack`. + +For example, if you want a minimal working package without using any bundler like `webpack`, you can do this: +``` +wasm-pack build --target web -- --no-default-features +``` +Then a `pkg` directory will be created, containing everything you need for a webapp. You can test whether the package is successfully built by creating an html file, adapted from `wasm-bindgen`'s [official example](https://rustwasm.github.io/wasm-bindgen/examples/without-a-bundler.html) like this: + +```html + + + + + Scryer Prolog - Sudoku Solver Example + + + + +``` + +Then you can serve it with your favorite http server like `python -m http.server` or `npx serve`, and access the page with your browser. + ### Docker Install First, install [Docker](https://docs.docker.com/get-docker/) on Linux, diff --git a/build/instructions_template.rs b/build/instructions_template.rs index f626d781..4dc9c893 100644 --- a/build/instructions_template.rs +++ b/build/instructions_template.rs @@ -496,22 +496,28 @@ enum SystemClauseType { CryptoDataHKDF, #[strum_discriminants(strum(props(Arity = "4", Name = "$crypto_password_hash")))] CryptoPasswordHash, + #[strum_discriminants(strum(props(Arity = "4", Name = "$crypto_curve_scalar_mult")))] + CryptoCurveScalarMult, + #[strum_discriminants(strum(props(Arity = "3", Name = "$curve25519_scalar_mult")))] + Curve25519ScalarMult, + #[cfg(feature = "crypto-full")] #[strum_discriminants(strum(props(Arity = "7", Name = "$crypto_data_encrypt")))] CryptoDataEncrypt, + #[cfg(feature = "crypto-full")] #[strum_discriminants(strum(props(Arity = "6", Name = "$crypto_data_decrypt")))] CryptoDataDecrypt, - #[strum_discriminants(strum(props(Arity = "4", Name = "$crypto_curve_scalar_mult")))] - CryptoCurveScalarMult, + #[cfg(feature = "crypto-full")] #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_sign")))] Ed25519Sign, + #[cfg(feature = "crypto-full")] #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_verify")))] Ed25519Verify, + #[cfg(feature = "crypto-full")] #[strum_discriminants(strum(props(Arity = "1", Name = "$ed25519_new_keypair")))] Ed25519NewKeyPair, + #[cfg(feature = "crypto-full")] #[strum_discriminants(strum(props(Arity = "2", Name = "$ed25519_keypair_public_key")))] Ed25519KeyPairPublicKey, - #[strum_discriminants(strum(props(Arity = "3", Name = "$curve25519_scalar_mult")))] - Curve25519ScalarMult, #[strum_discriminants(strum(props(Arity = "2", Name = "$first_non_octet")))] FirstNonOctet, #[strum_discriminants(strum(props(Arity = "3", Name = "$load_html")))] @@ -1842,13 +1848,7 @@ fn generate_instruction_preface() -> TokenStream { &Instruction::CallCryptoDataHash | &Instruction::CallCryptoDataHKDF | &Instruction::CallCryptoPasswordHash | - &Instruction::CallCryptoDataEncrypt | - &Instruction::CallCryptoDataDecrypt | &Instruction::CallCryptoCurveScalarMult | - &Instruction::CallEd25519Sign | - &Instruction::CallEd25519Verify | - &Instruction::CallEd25519NewKeyPair | - &Instruction::CallEd25519KeyPairPublicKey | &Instruction::CallCurve25519ScalarMult | &Instruction::CallFirstNonOctet | &Instruction::CallLoadHTML | @@ -1905,6 +1905,17 @@ fn generate_instruction_preface() -> TokenStream { functor!(atom!("call"), [atom(name), fixnum(arity)]) } // + #[cfg(feature = "crypto-full")] + &Instruction::CallCryptoDataEncrypt | + &Instruction::CallCryptoDataDecrypt | + &Instruction::CallEd25519Sign | + &Instruction::CallEd25519Verify | + &Instruction::CallEd25519NewKeyPair | + &Instruction::CallEd25519KeyPairPublicKey => { + let (name, arity) = self.to_name_and_arity(); + functor!(atom!("call"), [atom(name), fixnum(arity)]) + } + // &Instruction::ExecuteAtomChars | &Instruction::ExecuteAtomCodes | &Instruction::ExecuteAtomLength | @@ -2070,13 +2081,7 @@ fn generate_instruction_preface() -> TokenStream { &Instruction::ExecuteCryptoDataHash | &Instruction::ExecuteCryptoDataHKDF | &Instruction::ExecuteCryptoPasswordHash | - &Instruction::ExecuteCryptoDataEncrypt | - &Instruction::ExecuteCryptoDataDecrypt | &Instruction::ExecuteCryptoCurveScalarMult | - &Instruction::ExecuteEd25519Sign | - &Instruction::ExecuteEd25519Verify | - &Instruction::ExecuteEd25519NewKeyPair | - &Instruction::ExecuteEd25519KeyPairPublicKey | &Instruction::ExecuteCurve25519ScalarMult | &Instruction::ExecuteFirstNonOctet | &Instruction::ExecuteLoadHTML | @@ -2133,6 +2138,17 @@ fn generate_instruction_preface() -> TokenStream { functor!(atom!("execute"), [atom(name), fixnum(arity)]) } // + #[cfg(feature = "crypto-full")] + &Instruction::ExecuteCryptoDataEncrypt | + &Instruction::ExecuteCryptoDataDecrypt | + &Instruction::ExecuteEd25519Sign | + &Instruction::ExecuteEd25519Verify | + &Instruction::ExecuteEd25519NewKeyPair | + &Instruction::ExecuteEd25519KeyPairPublicKey => { + let (name, arity) = self.to_name_and_arity(); + functor!(atom!("execute"), [atom(name), fixnum(arity)]) + } + // &Instruction::Deallocate => { functor!(atom!("deallocate")) } diff --git a/src/atom_table.rs b/src/atom_table.rs index ba5a80b4..80ba5411 100644 --- a/src/atom_table.rs +++ b/src/atom_table.rs @@ -159,6 +159,7 @@ impl std::ops::Deref for AtomString<'_> { } } +#[cfg(feature = "repl")] impl rustyline::completion::Candidate for AtomString<'_> { fn display(&self) -> &str { self.deref() diff --git a/src/lib.rs b/src/lib.rs index 63c818e9..8817fa5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,3 +40,17 @@ pub mod types; use instructions::instr; mod rcu; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub fn eval_code(s: &str) -> String { + use web_sys::console; + use machine::mock_wam::*; + + let mut wam = Machine::with_test_streams(); + let bytes = wam.test_load_string(s); + String::from_utf8_lossy(&bytes).to_string() +} diff --git a/src/machine/dispatch.rs b/src/machine/dispatch.rs index 35a73f58..89174091 100644 --- a/src/machine/dispatch.rs +++ b/src/machine/dispatch.rs @@ -4742,18 +4742,22 @@ impl Machine { self.crypto_password_hash(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } + #[cfg(feature = "crypto-full")] &Instruction::CallCryptoDataEncrypt => { self.crypto_data_encrypt(); step_or_fail!(self, self.machine_st.p += 1); } + #[cfg(feature = "crypto-full")] &Instruction::ExecuteCryptoDataEncrypt => { self.crypto_data_encrypt(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } + #[cfg(feature = "crypto-full")] &Instruction::CallCryptoDataDecrypt => { self.crypto_data_decrypt(); step_or_fail!(self, self.machine_st.p += 1); } + #[cfg(feature = "crypto-full")] &Instruction::ExecuteCryptoDataDecrypt => { self.crypto_data_decrypt(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); @@ -4766,34 +4770,42 @@ impl Machine { self.crypto_curve_scalar_mult(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } + #[cfg(feature = "crypto-full")] &Instruction::CallEd25519Sign => { self.ed25519_sign(); step_or_fail!(self, self.machine_st.p += 1); } + #[cfg(feature = "crypto-full")] &Instruction::ExecuteEd25519Sign => { self.ed25519_sign(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } + #[cfg(feature = "crypto-full")] &Instruction::CallEd25519Verify => { self.ed25519_verify(); step_or_fail!(self, self.machine_st.p += 1); } + #[cfg(feature = "crypto-full")] &Instruction::ExecuteEd25519Verify => { self.ed25519_verify(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } + #[cfg(feature = "crypto-full")] &Instruction::CallEd25519NewKeyPair => { self.ed25519_new_key_pair(); step_or_fail!(self, self.machine_st.p += 1); } + #[cfg(feature = "crypto-full")] &Instruction::ExecuteEd25519NewKeyPair => { self.ed25519_new_key_pair(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } + #[cfg(feature = "crypto-full")] &Instruction::CallEd25519KeyPairPublicKey => { self.ed25519_key_pair_public_key(); step_or_fail!(self, self.machine_st.p += 1); } + #[cfg(feature = "crypto-full")] &Instruction::ExecuteEd25519KeyPairPublicKey => { self.ed25519_key_pair_public_key(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); diff --git a/src/machine/mock_wam.rs b/src/machine/mock_wam.rs index 7be95fbd..cd8f7836 100644 --- a/src/machine/mock_wam.rs +++ b/src/machine/mock_wam.rs @@ -332,6 +332,19 @@ impl Machine { self.load_file(file.into(), stream); self.user_output.bytes().map(|b| b.unwrap()).collect() } + + pub fn test_load_string(&mut self, code: &str) -> Vec { + use std::io::Read; + + let stream = Stream::from_owned_string( + code.to_owned(), + &mut self.machine_st.arena, + ); + + self.load_file("".into(), stream); + self.user_output.bytes().map(|b| b.unwrap()).collect() + } + } #[cfg(test)] diff --git a/src/machine/mod.rs b/src/machine/mod.rs index a7d80e9f..7e456d4e 100644 --- a/src/machine/mod.rs +++ b/src/machine/mod.rs @@ -453,9 +453,9 @@ impl Machine { let user_output = Stream::stdout(&mut machine_st.arena); let user_error = Stream::stderr(&mut machine_st.arena); - #[cfg(not(target_os = "wasi"))] + #[cfg(not(target_arch = "wasm32"))] let runtime = tokio::runtime::Runtime::new().unwrap(); - #[cfg(target_os = "wasi")] + #[cfg(target_arch = "wasm32")] let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 7b017c17..3e6a7090 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -60,7 +60,7 @@ use std::process; use std::str::FromStr; use chrono::{offset::Local, DateTime}; -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_arch = "wasm32"))] use cpu_time::ProcessTime; use std::time::{Duration, SystemTime}; @@ -71,8 +71,11 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use blake2::{Blake2b, Blake2s}; use ring::rand::{SecureRandom, SystemRandom}; +use ring::{digest, hkdf, pbkdf2}; + +#[cfg(feature = "crypto-full")] use ring::{ - aead, digest, hkdf, pbkdf2, + aead, signature::{self, KeyPair}, }; use ripemd160::{Digest, Ripemd160}; @@ -4290,7 +4293,7 @@ impl Machine { self.machine_st.fail = result; } - #[cfg(not(target_os = "wasi"))] + #[cfg(not(target_arch = "wasm32"))] #[inline(always)] pub(crate) fn cpu_now(&mut self) { let secs = ProcessTime::now().as_duration().as_secs_f64(); @@ -4300,7 +4303,7 @@ impl Machine { .unify_f64(secs, self.machine_st.registers[1]); } - #[cfg(target_os = "wasi")] + #[cfg(target_arch = "wasm32")] #[inline(always)] pub(crate) fn cpu_now(&mut self) { // TODO @@ -7382,6 +7385,7 @@ impl Machine { unify!(self.machine_st, self.machine_st.registers[4], ints_list); } + #[cfg(feature = "crypto-full")] #[inline(always)] pub(crate) fn crypto_data_encrypt(&mut self) { let encoding = cell_as_atom!(self.deref_register(3)); @@ -7430,6 +7434,7 @@ impl Machine { ); } + #[cfg(feature = "crypto-full")] #[inline(always)] pub(crate) fn crypto_data_decrypt(&mut self) { let data = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); @@ -7508,6 +7513,7 @@ impl Machine { unify!(self.machine_st, self.machine_st.registers[4], uncompressed); } + #[cfg(feature = "crypto-full")] #[inline(always)] pub(crate) fn ed25519_new_key_pair(&mut self) { let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(rng()).unwrap(); @@ -7520,6 +7526,7 @@ impl Machine { ) } + #[cfg(feature = "crypto-full")] #[inline(always)] pub(crate) fn ed25519_key_pair_public_key(&mut self) { let bytes = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); @@ -7541,6 +7548,7 @@ impl Machine { ); } + #[cfg(feature = "crypto-full")] #[inline(always)] pub(crate) fn ed25519_sign(&mut self) { let key = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); @@ -7567,6 +7575,7 @@ impl Machine { unify!(self.machine_st, self.machine_st.registers[4], sig_list); } + #[cfg(feature = "crypto-full")] #[inline(always)] pub(crate) fn ed25519_verify(&mut self) { let key = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); -- 2.54.0