-
Notifications
You must be signed in to change notification settings - Fork 210
Web optimizations #559
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
Web optimizations #559
Changes from 8 commits
1dbda17
d804258
b2ab9a1
02507fe
dcd8f4a
e024029
6feb9b7
65ba727
055a7c2
ec6f289
cf72d6c
e5d68a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,34 +7,54 @@ pub use crate::util::{inner_u32, inner_u64}; | |
#[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))] | ||
compile_error!("`wasm_js` backend can be enabled only for OS-less WASM targets!"); | ||
|
||
use js_sys::{global, Uint8Array}; | ||
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; | ||
#[cfg(target_feature = "atomics")] | ||
use js_sys::Uint8Array; | ||
newpavlov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; | ||
|
||
// Size of our temporary Uint8Array buffer used with WebCrypto methods | ||
// Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues | ||
const CRYPTO_BUFFER_SIZE: u16 = 256; | ||
// Maximum buffer size allowed in `Crypto.getRandomValuesSize` is 65536 bytes. | ||
// See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues | ||
const MAX_BUFFER_SIZE: u32 = 65536; | ||
daxpedda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> { | ||
let global: Global = global().unchecked_into(); | ||
let crypto = global.crypto(); | ||
CRYPTO.with(|crypto| { | ||
let crypto = crypto.as_ref().ok_or(Error::WEB_CRYPTO)?; | ||
inner(crypto, dest) | ||
}) | ||
} | ||
|
||
if !crypto.is_object() { | ||
return Err(Error::WEB_CRYPTO); | ||
#[cfg(not(target_feature = "atomics"))] | ||
fn inner(crypto: &Crypto, dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> { | ||
for chunk in dest.chunks_mut(MAX_BUFFER_SIZE as usize) { | ||
if crypto.get_random_values(chunk).is_err() { | ||
return Err(Error::WEB_GET_RANDOM_VALUES); | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(target_feature = "atomics")] | ||
fn inner(crypto: &Crypto, dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> { | ||
// getRandomValues does not work with all types of WASM memory, | ||
// so we initially write to browser memory to avoid exceptions. | ||
let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into()); | ||
for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) { | ||
let buf_len = u32::min( | ||
dest.len().try_into().unwrap_or(MAX_BUFFER_SIZE), | ||
MAX_BUFFER_SIZE, | ||
); | ||
let buf = Uint8Array::new_with_length(buf_len); | ||
for chunk in dest.chunks_mut(buf_len as usize) { | ||
let chunk_len: u32 = chunk | ||
.len() | ||
.try_into() | ||
.expect("chunk length is bounded by CRYPTO_BUFFER_SIZE"); | ||
.expect("chunk length is bounded by MAX_BUFFER_SIZE"); | ||
// The chunk can be smaller than buf's length, so we call to | ||
// JS to create a smaller view of buf without allocation. | ||
let sub_buf = buf.subarray(0, chunk_len); | ||
let sub_buf = if chunk_len == buf_len { | ||
&buf | ||
} else { | ||
&buf.subarray(0, chunk_len) | ||
}; | ||
|
||
if crypto.get_random_values(&sub_buf).is_err() { | ||
if crypto.get_random_values(sub_buf).is_err() { | ||
return Err(Error::WEB_GET_RANDOM_VALUES); | ||
} | ||
|
||
|
@@ -46,14 +66,16 @@ pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> { | |
|
||
#[wasm_bindgen] | ||
extern "C" { | ||
// Return type of js_sys::global() | ||
type Global; | ||
// Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/) | ||
type Crypto; | ||
// Getters for the Crypto API | ||
#[wasm_bindgen(method, getter)] | ||
fn crypto(this: &Global) -> Crypto; | ||
// Holds the global `Crypto` object. | ||
#[wasm_bindgen(thread_local_v2, js_name = crypto)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially avoided using For context: the current setup just accesses the object directly from the global scope. According to the spec this is perfectly fine. However, this is usually discouraged because global variables can simply be overwritten. While I'm not sure if there is more history behind this, fact is that read-only properties like But to stick with "how it should be done" I will add EDIT: Support should also not be a concern after #554, but you can make your own evaluations via "Can I Use". |
||
static CRYPTO: Option<Crypto>; | ||
// Crypto.getRandomValues() | ||
#[cfg(not(target_feature = "atomics"))] | ||
#[wasm_bindgen(method, js_name = getRandomValues, catch)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using methods, // Web Crypto API bindings (https://www.w3.org/TR/WebCryptoAPI/)
#[wasm_bindgen(js_namespace = crypto)]
extern "C" {
#[cfg(not(target_feature = "atomics"))]
#[wasm_bindgen(js_name = getRandomValues, catch)]
fn get_random_values(buf: &mut [MaybeUninit<u8>]) -> Result<(), JsValue>;
#[cfg(target_feature = "atomics")]
#[wasm_bindgen(js_name = getRandomValues, catch)]
fn get_random_values(buf: &Uint8Array) -> Result<(), JsValue>;
} (I'm not sure if Then the code can just be: // If atomics are not enabled, we assume that Rust's memory is not backed by a
// SharedArrayBuffer, so we can directly call getRandomValues() on the buffer.
#[cfg(not(target_feature = "atomics"))]
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
// The byteLength of the input must not exceed 65536 bytes, see:
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const MAX_LENGTH: usize = 65536;
for chunk in dest.chunks_mut(MAX_LENGTH) {
if crypto.get_random_values(chunk).is_err() {
return Err(Error::WEB_GET_RANDOM_VALUES);
}
}
Ok(())
}
// If atomics are enabled, Rust's memory can be backed by a SharedArrayBuffer,
// so we use a temporary Uint8Array buffer when calling getRandomValues().
#[cfg(target_feature = "atomics")]
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
// We are rarely called with long slices, so we allocate a small buffer.
const BUF_LEN: u16 = 256; // Less than MAX_LENGTH
let tmp = js_sys::Uint8Array::new_with_length(BUF_LEN.into());
for chunk in dest.chunks_mut(BUF_LEN.into()) {
// The chunk can be smaller than tmp's length, so we call to
// JS to create a smaller view of buf without allocation.
let buf = if chunk.len() == BUF_LEN.into() {
tmp
} else {
let len: u32 = chunk.len().try_into().expect("chunk.len() <= BUF_LEN <= u32::MAX");
tmp.subarray(0, len)
}
if crypto.get_random_values(&buf).is_err() {
return Err(Error::WEB_GET_RANDOM_VALUES);
}
// SAFETY: buf's length is equal the chunk's length.
unsafe { buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::<u8>()) };
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are forfeiting the check if if CRYPTO.with(Option::is_none) {
return Err(Error::WEB_CRYPTO)
}
// use `get_random_values()` without nesting:
get_random_values(&buf); Let me know how you want to proceed. My recommendation is to keep the check in place, there's all sort of strange JS environments out there, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking if the global So if I'm understanding things correctly, having the check for the existence of the object just lets us return I think returning good error messages is valuable, so I think keeping some sort of check is OK, provided that we aren't making an additional FFI call every time (which I don't think we do because the result of I personally find the un-nested version easier to read, so we might try a version that avoids too much nesting (potentially by using a helper function). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, you are relying on the fact that WBG just catches any exception when tagging it with
Sure! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, let me know if you like this better! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see! From my understanding the issue was getting rid of the nesting, but that has been achieved, right? Is there some other motivation to get rid of On that note: your proposal will store the check globally. But different threads may have different outcomes. This isn't hypothetical either, some worklets don't have access to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not familiar with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, we have one FFI function less present in the Wasm file. To put it into perspective: the function that actually ends up in the Wasm file after post-processing is done is just a single FFI signature, one byte cast and an if condition. Not counting the storing and loading ofc.
If you don't care about the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, that fast: according to the specification, Let me know what you think! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 8cf6832. |
||
fn get_random_values(this: &Crypto, buf: &mut [MaybeUninit<u8>]) -> Result<(), JsValue>; | ||
#[cfg(target_feature = "atomics")] | ||
#[wasm_bindgen(method, js_name = getRandomValues, catch)] | ||
fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.