Skip to content

Commit cddeb38

Browse files
authored
Add a safe wrapper around ForOfIterator (#736)
`ForOfIterator` implements `for .. of` iteration in spidermonkey. It holds stack roots and is a pain to use directly due to some weird shenanigans where bindgen adds extra padding to the struct for some LLVM versions. Its also hard to use in a crash-proof manner, because `ForOfIterator` must not be moved. We'd have to `Pin<Box<>>` it to be safe, which adds overhead. This change instead adds `mozjs::rust::for_of`, a function that takes a callback and uses `ForOfIterator` correctly so users don't have to worry about it. Testing: The iterator is used during conversion of IDL sequences, and its covered by WPT. I'm also using it for web animations locally. Part of servo/servo#36950 Servo PR: servo/servo#44356 --------- Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
1 parent d083133 commit cddeb38

6 files changed

Lines changed: 142 additions & 82 deletions

File tree

mozjs-sys/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "mozjs_sys"
33
description = "System crate for the Mozilla SpiderMonkey JavaScript engine."
44
repository.workspace = true
5-
version = "0.140.10-2"
5+
version = "0.140.10-3"
66
authors = ["Mozilla", "The Servo Project Developers"]
77
links = "mozjs"
88
license.workspace = true

mozjs-sys/src/jsapi.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ bool JS_ForOfIteratorNext(JS::ForOfIterator* iterator,
172172
return iterator->next(val, done);
173173
}
174174

175+
bool JS_ForOfIteratorValueIsIterable(const JS::ForOfIterator* iterator) {
176+
return iterator->valueIsIterable();
177+
}
178+
175179
// These functions are only intended for use in testing,
176180
// to make sure that the Rust implementation of JS::Value
177181
// agrees with the C++ implementation.

mozjs-sys/src/jsimpls.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
use crate::jsapi::glue::JS_ForOfIteratorInit;
6-
use crate::jsapi::glue::JS_ForOfIteratorNext;
5+
use crate::jsapi::glue::{
6+
JS_ForOfIteratorInit, JS_ForOfIteratorNext, JS_ForOfIteratorValueIsIterable,
7+
};
78
use crate::jsapi::jsid;
89
use crate::jsapi::mozilla;
910
use crate::jsapi::JSAutoRealm;
@@ -577,6 +578,10 @@ impl JS::ForOfIterator {
577578
pub unsafe fn next(&mut self, val: JS::MutableHandleValue, done: *mut bool) -> bool {
578579
JS_ForOfIteratorNext(self, val, done)
579580
}
581+
582+
pub fn is_iterable(&self) -> bool {
583+
unsafe { JS_ForOfIteratorValueIsIterable(self) }
584+
}
580585
}
581586

582587
impl<T> mozilla::Range<T> {

mozjs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ encoding_rs = "0.8.35"
2727
libc.workspace = true
2828
log = "0.4"
2929
# When doing non-version changes also update ../mozjs-sys/etc/sm-security-bump.py
30-
mozjs_sys = { version = "=0.140.10-2", path = "../mozjs-sys" }
30+
mozjs_sys = { version = "=0.140.10-3", path = "../mozjs-sys" }
3131
num-traits = "0.2"
3232

3333
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]

mozjs/src/conversions.rs

Lines changed: 24 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,25 @@
3131
use crate::error::throw_type_error;
3232
use crate::jsapi::AssertSameCompartment;
3333
use crate::jsapi::JS;
34-
use crate::jsapi::{ForOfIterator, ForOfIterator_NonIterableBehavior};
3534
use crate::jsapi::{Heap, JS_DefineElement, JS_GetLatin1StringCharsAndLength};
36-
use crate::jsapi::{JSContext, JSObject, JSString, RootedObject, RootedValue};
35+
use crate::jsapi::{JSContext, JSObject, JSString};
3736
use crate::jsapi::{JS_DeprecatedStringHasLatin1Chars, JS_NewStringCopyUTF8N, JSPROP_ENUMERATE};
3837
use crate::jsapi::{JS_GetTwoByteStringCharsAndLength, NewArrayObject1};
3938
use crate::jsval::{BooleanValue, DoubleValue, Int32Value, NullValue, UInt32Value, UndefinedValue};
4039
use crate::jsval::{JSVal, ObjectOrNullValue, ObjectValue, StringValue, SymbolValue};
4140
use crate::rooted;
41+
use crate::rust::for_of;
4242
use crate::rust::maybe_wrap_value;
43+
use crate::rust::ForOfIterationFailure;
4344
use crate::rust::{maybe_wrap_object_or_null_value, maybe_wrap_object_value, ToString};
4445
use crate::rust::{HandleValue, MutableHandleValue};
4546
use crate::rust::{ToBoolean, ToInt32, ToInt64, ToNumber, ToUint16, ToUint32, ToUint64};
4647
use libc;
4748
use log::debug;
48-
use mozjs_sys::jsgc::Rooted;
4949
use num_traits::PrimInt;
5050
use std::borrow::Cow;
5151
use std::ffi::CStr;
52-
use std::mem;
52+
use std::ops::ControlFlow;
5353
use std::ptr::NonNull;
5454
use std::rc::Rc;
5555
use std::{ptr, slice};
@@ -757,31 +757,6 @@ impl<T: ToJSValConvertible> ToJSValConvertible for Vec<T> {
757757
}
758758
}
759759

760-
/// Rooting guard for the iterator field of ForOfIterator.
761-
/// Behaves like RootedGuard (roots on creation, unroots on drop),
762-
/// but borrows and allows access to the whole ForOfIterator, so
763-
/// that methods on ForOfIterator can still be used through it.
764-
struct ForOfIteratorGuard<'a> {
765-
root: &'a mut ForOfIterator,
766-
}
767-
768-
impl<'a> ForOfIteratorGuard<'a> {
769-
fn new(cx: *mut JSContext, root: &'a mut ForOfIterator) -> Self {
770-
unsafe {
771-
Rooted::add_to_root_stack(&raw mut root.iterator, cx);
772-
}
773-
ForOfIteratorGuard { root }
774-
}
775-
}
776-
777-
impl<'a> Drop for ForOfIteratorGuard<'a> {
778-
fn drop(&mut self) {
779-
unsafe {
780-
self.root.iterator.remove_from_root_stack();
781-
}
782-
}
783-
}
784-
785760
impl<C: Clone, T: FromJSValConvertible<Config = C>> FromJSValConvertible for Vec<T> {
786761
type Config = C;
787762

@@ -794,57 +769,31 @@ impl<C: Clone, T: FromJSValConvertible<Config = C>> FromJSValConvertible for Vec
794769
return Ok(ConversionResult::Failure(c"Value is not an object".into()));
795770
}
796771

797-
// Depending on the version of LLVM in use, bindgen can end up including
798-
// a padding field in the ForOfIterator. To support multiple versions of
799-
// LLVM that may not have the same fields as a result, we create an empty
800-
// iterator instance and initialize a non-empty instance using the empty
801-
// instance as a base value.
802-
#[allow(unused_variables)]
803-
let zero = mem::zeroed();
804-
let mut iterator = ForOfIterator {
805-
cx_: cx,
806-
iterator: RootedObject::new_unrooted(ptr::null_mut()),
807-
nextMethod: RootedValue::new_unrooted(JSVal { asBits_: 0 }),
808-
index: ::std::u32::MAX, // NOT_ARRAY
809-
..zero
810-
};
811-
let iterator = ForOfIteratorGuard::new(cx, &mut iterator);
812-
let iterator: &mut ForOfIterator = &mut *iterator.root;
813-
814-
if !iterator.init(
815-
value.into(),
816-
ForOfIterator_NonIterableBehavior::AllowNonIterable,
817-
) {
818-
return Err(());
819-
}
820-
821-
if iterator.iterator.data.is_null() {
822-
return Ok(ConversionResult::Failure(c"Value is not iterable".into()));
823-
}
772+
let mut return_value = vec![];
773+
let result = for_of(cx, value, |iterator_element| {
774+
let conversion_result = T::from_jsval(cx, iterator_element, option.clone())
775+
.map_err(|_| ForOfIterationFailure::JSFailed)?;
776+
return_value.push(match conversion_result {
777+
ConversionResult::Success(value) => value,
778+
ConversionResult::Failure(error) => {
779+
return Err(ForOfIterationFailure::Other(error));
780+
}
781+
});
824782

825-
let mut ret = vec![];
783+
Ok(ControlFlow::Continue(()))
784+
});
826785

827-
loop {
828-
let mut done = false;
829-
rooted!(in(cx) let mut val = UndefinedValue());
830-
if !iterator.next(val.handle_mut().into(), &mut done) {
831-
return Err(());
786+
match result {
787+
Ok(_) => Ok(ConversionResult::Success(return_value)),
788+
Err(ForOfIterationFailure::ValueIsNotIterable) => {
789+
Ok(ConversionResult::Failure(c"Value is not iterable".into()))
832790
}
833-
834-
if done {
835-
break;
791+
Err(ForOfIterationFailure::JSFailed) => Err(()),
792+
Err(ForOfIterationFailure::Other(error)) => {
793+
throw_type_error(cx, error.as_ref());
794+
Err(())
836795
}
837-
838-
ret.push(match T::from_jsval(cx, val.handle(), option.clone())? {
839-
ConversionResult::Success(v) => v,
840-
ConversionResult::Failure(e) => {
841-
throw_type_error(cx, e.as_ref());
842-
return Err(());
843-
}
844-
});
845796
}
846-
847-
Ok(ret).map(ConversionResult::Success)
848797
}
849798
}
850799

mozjs/src/rust.rs

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ use std::char;
99
use std::default::Default;
1010
use std::ffi::{c_char, c_void, CStr, CString};
1111
use std::marker::PhantomData;
12+
use std::mem;
1213
use std::mem::MaybeUninit;
13-
use std::ops::{Deref, DerefMut};
14+
use std::ops::{ControlFlow, Deref, DerefMut};
1415
use std::ptr::{self, NonNull};
1516
use std::slice;
1617
use std::str;
@@ -63,10 +64,12 @@ use crate::jsapi::{JS_RequestInterruptCallback, JS_RequestInterruptCallbackCanWa
6364
use crate::jsapi::{JS_SetGCParameter, JS_SetNativeStackQuota, JS_WrapObject, JS_WrapValue};
6465
use crate::jsapi::{JS_StackCapture_AllFrames, JS_StackCapture_MaxFrames};
6566
use crate::jsapi::{PersistentRootedObjectVector, ReadOnlyCompileOptions, RootingContext};
67+
use crate::jsapi::{
68+
RootedObject, RootedValue, ToUint32Slow, ToUint64Slow, ToWindowProxyIfWindowSlow,
69+
};
6670
use crate::jsapi::{SetWarningReporter, SourceText, ToBooleanSlow};
6771
use crate::jsapi::{ToInt32Slow, ToInt64Slow, ToNumberSlow, ToStringSlow, ToUint16Slow};
68-
use crate::jsapi::{ToUint32Slow, ToUint64Slow, ToWindowProxyIfWindowSlow};
69-
use crate::jsval::{JSVal, ObjectValue};
72+
use crate::jsval::{JSVal, ObjectValue, UndefinedValue};
7073
use crate::panic::maybe_resume_unwind;
7174
use crate::realm::AutoRealm;
7275
use log::{debug, warn};
@@ -1267,6 +1270,105 @@ impl<'a> Handle<'a, StackGCVector<*mut JSString, js::TempAllocPolicy>> {
12671270
}
12681271
}
12691272

1273+
#[derive(Clone, Copy, Debug)]
1274+
pub enum ForOfIterationFailure<OtherError> {
1275+
ValueIsNotIterable,
1276+
/// There is a pending exception
1277+
JSFailed,
1278+
Other(OtherError),
1279+
}
1280+
1281+
impl<OtherError> From<OtherError> for ForOfIterationFailure<OtherError> {
1282+
fn from(value: OtherError) -> Self {
1283+
Self::Other(value)
1284+
}
1285+
}
1286+
1287+
/// Helper for running `for .. of` iteration from rust.
1288+
///
1289+
/// If `Ok()` is returned then the iteration completed without unexpected failures.
1290+
///
1291+
/// The callback returns `Err()` to indicate a pending exception or `Ok()` containing a boolean
1292+
/// value that is `true` if the iterator should continue iterating.
1293+
pub fn for_of<Callback, OtherError>(
1294+
cx: *mut JSContext,
1295+
iterable: HandleValue<'_>,
1296+
mut callback: Callback,
1297+
) -> Result<(), ForOfIterationFailure<OtherError>>
1298+
where
1299+
Callback: FnMut(HandleValue<'_>) -> Result<ControlFlow<()>, ForOfIterationFailure<OtherError>>,
1300+
{
1301+
// Depending on the version of LLVM in use, bindgen can end up including
1302+
// a padding field in the ForOfIterator. To support multiple versions of
1303+
// LLVM that may not have the same fields as a result, we create an empty
1304+
// iterator instance and initialize a non-empty instance using the empty
1305+
// instance as a base value.
1306+
#[allow(unused_variables)]
1307+
let zero = unsafe { mem::zeroed() };
1308+
let mut iterator = jsapi::ForOfIterator {
1309+
cx_: cx,
1310+
iterator: RootedObject::new_unrooted(ptr::null_mut()),
1311+
nextMethod: RootedValue::new_unrooted(JSVal { asBits_: 0 }),
1312+
index: ::std::u32::MAX, // NOT_ARRAY
1313+
..zero
1314+
};
1315+
1316+
// This code would benefit from https://github.com/rust-lang/rust/issues/144426
1317+
struct IteratorRootGuard<'a> {
1318+
inner: &'a mut jsapi::ForOfIterator,
1319+
}
1320+
1321+
impl<'a> Drop for IteratorRootGuard<'a> {
1322+
fn drop(&mut self) {
1323+
// SAFETY: These values won't be used anymore
1324+
unsafe {
1325+
self.inner.iterator.remove_from_root_stack();
1326+
self.inner.nextMethod.remove_from_root_stack();
1327+
}
1328+
}
1329+
}
1330+
let guard = IteratorRootGuard {
1331+
inner: &mut iterator,
1332+
};
1333+
let iterator = &mut *guard.inner;
1334+
1335+
unsafe {
1336+
RootedObject::add_to_root_stack(&raw mut iterator.iterator, cx);
1337+
RootedValue::add_to_root_stack(&raw mut iterator.nextMethod, cx);
1338+
}
1339+
1340+
let success = unsafe {
1341+
iterator.init(
1342+
iterable.into_handle(),
1343+
jsapi::ForOfIterator_NonIterableBehavior::AllowNonIterable,
1344+
)
1345+
};
1346+
if !success {
1347+
return Err(ForOfIterationFailure::JSFailed);
1348+
}
1349+
if !iterator.is_iterable() {
1350+
return Err(ForOfIterationFailure::ValueIsNotIterable);
1351+
}
1352+
1353+
let mut done = false;
1354+
rooted!(in(cx) let mut value = UndefinedValue());
1355+
loop {
1356+
if !unsafe { iterator.next(value.handle_mut().into(), &mut done) } {
1357+
return Err(ForOfIterationFailure::JSFailed);
1358+
}
1359+
1360+
if done {
1361+
break;
1362+
}
1363+
1364+
if callback(value.handle())?.is_break() {
1365+
break;
1366+
}
1367+
}
1368+
1369+
Ok(())
1370+
}
1371+
12701372
/// Wrappers for JSAPI methods that accept lifetimed Handle and MutableHandle arguments
12711373
pub mod wrappers {
12721374
macro_rules! wrap {

0 commit comments

Comments
 (0)