char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: capture and restore extension layouts in one artifactd71572b

main
22.5 KiB656 linesraw
1use std::cell::RefCell;
2use std::future::Future;
3use std::pin::Pin;
4use std::rc::Rc;
5use std::task::{Context, Poll};
6
7use futures::FutureExt;
8use futures::future::LocalBoxFuture;
9use futures::stream::{FuturesUnordered, StreamExt};
10use serde::{Serialize, de::DeserializeOwned};
11use v8::MapFnTo;
12
13trait OpTask: Future<Output = ()> {
14    fn complete(self: Pin<&mut Self>, scope: &mut v8::PinScope);
15}
16
17// Keep the concrete result in the future's allocation until V8 can serialize it.
18pin_project_lite::pin_project! {
19    struct OpFuture<F: Future> {
20        #[pin]
21        future: futures::future::MaybeDone<F>,
22        resolver: v8::Global<v8::PromiseResolver>,
23    }
24}
25
26impl<F: Future> Future for OpFuture<F> {
27    type Output = ();
28
29    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
30        self.project().future.poll(cx)
31    }
32}
33
34impl<F, R, E> OpTask for OpFuture<F>
35where
36    F: Future<Output = Result<R, E>>,
37    R: Serialize,
38    E: std::error::Error,
39{
40    fn complete(self: Pin<&mut Self>, scope: &mut v8::PinScope) {
41        let this = self.project();
42        let result = this
43            .future
44            .take_output()
45            .expect("op completed before its future was ready")
46            .map_err(|error| OpError::Error(error.to_string()))
47            .and_then(|value| {
48                serde_v8::to_v8(scope, value).map_err(|error| OpError::Error(error.to_string()))
49            });
50        let resolver = v8::Local::new(scope, &*this.resolver);
51        match result {
52            Ok(value) => {
53                resolver.resolve(scope, value);
54            }
55            Err(error) => {
56                let error = error.exception(scope);
57                resolver.reject(scope, error);
58            }
59        }
60    }
61}
62
63struct OpCompletion {
64    future: Pin<Box<dyn OpTask>>,
65    referenced: bool,
66}
67
68enum Completion {
69    Op(OpCompletion),
70    Import(crate::modules::ImportPreparation),
71}
72
73enum Pending {
74    Op(Option<OpCompletion>),
75    Import(LocalBoxFuture<'static, crate::modules::ImportPreparation>),
76}
77
78impl Future for Pending {
79    type Output = Completion;
80
81    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Completion> {
82        match self.get_mut() {
83            Self::Op(completion) => {
84                std::task::ready!(completion.as_mut().unwrap().future.as_mut().poll(cx));
85                Poll::Ready(Completion::Op(completion.take().unwrap()))
86            }
87            Self::Import(future) => future.as_mut().poll(cx).map(Completion::Import),
88        }
89    }
90}
91
92struct PendingOps {
93    futures: FuturesUnordered<Pending>,
94    referenced: usize,
95}
96enum RejectionPromise {
97    Pending(v8::Global<v8::Promise>),
98    Reported(v8::Weak<v8::Promise>),
99}
100struct Rejection {
101    promise: RejectionPromise,
102    reason: Option<v8::Global<v8::Value>>,
103}
104type Rejections = Rc<RefCell<Vec<Rejection>>>;
105type HandledRejections = Rc<RefCell<Vec<(v8::Global<v8::Promise>, v8::Global<v8::Value>)>>>;
106
107extern "C" fn promise_rejected(message: v8::PromiseRejectMessage) {
108    // SAFETY: V8 invokes this callback with a promise belonging to the active isolate.
109    v8::callback_scope!(unsafe scope, &message);
110    let rejections = scope.get_slot::<Rejections>().unwrap().clone();
111    match message.get_event() {
112        v8::PromiseRejectEvent::PromiseRejectWithNoHandler => {
113            let promise = message.get_promise();
114            let mut rejections = rejections.borrow_mut();
115            if !rejections.iter().any(|rejection| match &rejection.promise {
116                RejectionPromise::Pending(promise_global) => {
117                    v8::Local::new(scope, promise_global) == promise
118                }
119                RejectionPromise::Reported(weak) => {
120                    weak.to_local(scope).is_some_and(|value| value == promise)
121                }
122            }) {
123                rejections.push(Rejection {
124                    promise: RejectionPromise::Pending(v8::Global::new(scope, promise)),
125                    reason: Some(v8::Global::new(scope, message.get_value().unwrap())),
126                });
127            }
128        }
129        v8::PromiseRejectEvent::PromiseHandlerAddedAfterReject => {
130            let promise = message.get_promise();
131            let handled = {
132                let mut rejections = rejections.borrow_mut();
133                rejections
134                    .iter()
135                    .position(|rejection| match &rejection.promise {
136                        RejectionPromise::Pending(promise_global) => {
137                            v8::Local::new(scope, promise_global) == promise
138                        }
139                        RejectionPromise::Reported(weak) => {
140                            weak.to_local(scope).is_some_and(|value| value == promise)
141                        }
142                    })
143                    .map(|index| rejections.remove(index))
144            };
145            if let Some(rejection) = handled
146                && matches!(rejection.promise, RejectionPromise::Reported(_))
147            {
148                let reason = promise.result(scope);
149                let handled = scope.get_slot::<HandledRejections>().unwrap().clone();
150                handled.borrow_mut().push((
151                    v8::Global::new(scope, promise),
152                    v8::Global::new(scope, reason),
153                ));
154            }
155        }
156        _ => {}
157    }
158}
159
160pub(crate) fn mark_promise_handled<'s>(
161    scope: &mut v8::PinScope<'s, '_>,
162    args: v8::FunctionCallbackArguments<'s>,
163    _result: v8::ReturnValue,
164) {
165    let Ok(promise) = v8::Local::<v8::Promise>::try_from(args.get(0)) else {
166        let message = v8::String::new(scope, "Expected a promise").unwrap();
167        let error = v8::Exception::type_error(scope, message);
168        scope.throw_exception(error);
169        return;
170    };
171    // MarkAsHandled also notifies our rejection tracker for already-rejected promises.
172    promise.mark_as_handled();
173}
174
175/// Reads a promise's state for inspection: undefined for non-promises,
176/// otherwise `[state, result]`, where state 0 is pending, 1 fulfilled and
177/// 2 rejected. JavaScript cannot observe this synchronously.
178pub(crate) fn promise_state<'s>(
179    scope: &mut v8::PinScope<'s, '_>,
180    args: v8::FunctionCallbackArguments<'s>,
181    mut result: v8::ReturnValue,
182) {
183    let Ok(promise) = v8::Local::<v8::Promise>::try_from(args.get(0)) else {
184        return;
185    };
186    let (state, value) = match promise.state() {
187        v8::PromiseState::Pending => (0, v8::undefined(scope).into()),
188        v8::PromiseState::Fulfilled => (1, promise.result(scope)),
189        v8::PromiseState::Rejected => (2, promise.result(scope)),
190    };
191    let state = v8::Integer::new(scope, state).into();
192    result.set(v8::Array::new_with_elements(scope, &[state, value]).into());
193}
194
195pub(crate) fn perform_promise_then<'s>(
196    scope: &mut v8::PinScope<'s, '_>,
197    args: v8::FunctionCallbackArguments<'s>,
198    mut result: v8::ReturnValue,
199) {
200    let (Ok(promise), Ok(fulfilled), Ok(rejected)) = (
201        v8::Local::<v8::Promise>::try_from(args.get(0)),
202        v8::Local::<v8::Function>::try_from(args.get(1)),
203        v8::Local::<v8::Function>::try_from(args.get(2)),
204    ) else {
205        let message = v8::String::new(scope, "Expected a promise and two handlers").unwrap();
206        let error = v8::Exception::type_error(scope, message);
207        scope.throw_exception(error);
208        return;
209    };
210    // V8's API uses PerformPromiseThen, not the species-sensitive JS builtin.
211    if let Some(promise) = promise.then2(scope, fulfilled, rejected) {
212        result.set(promise.into());
213    }
214}
215
216type Callback = dyn for<'s, 'i> Fn(
217    &mut v8::PinScope<'s, 'i>,
218    v8::FunctionCallbackArguments<'s>,
219) -> Option<v8::Local<'s, v8::Value>>;
220
221pub struct Op {
222    pub name: &'static str,
223    callback: Box<Callback>,
224}
225
226#[derive(Clone)]
227pub(crate) struct RegisteredOps {
228    ops: Rc<[Op]>,
229}
230
231impl RegisteredOps {
232    pub(crate) fn new(mut ops: Vec<Op>) -> Result<Self, crate::Error> {
233        // Snapshot indices depend on names, not extension order or native addresses.
234        // Restoring requires the same op names; adding or removing an op invalidates the snapshot.
235        ops.sort_unstable_by_key(|op| op.name);
236        for pair in ops.windows(2) {
237            if pair[0].name == pair[1].name {
238                return Err(crate::Error::DuplicateOp(pair[0].name));
239            }
240        }
241        Ok(Self { ops: ops.into() })
242    }
243
244    pub(crate) fn external_references() -> Vec<v8::ExternalReference> {
245        vec![v8::ExternalReference { function: dispatch.map_fn_to() }]
246    }
247
248    pub(crate) fn object<'s>(
249        &self,
250        scope: &mut v8::PinScope<'s, '_>,
251    ) -> Option<v8::Local<'s, v8::Object>> {
252        let registry = v8::Object::new(scope);
253        registry.set_prototype(scope, v8::null(scope).into())?;
254        for (index, op) in self.ops.iter().enumerate() {
255            let key = v8::String::new(scope, op.name)?;
256            let data =
257                v8::Integer::new_from_unsigned(scope, index.try_into().expect("Too many ops"));
258            let function = v8::Function::builder(dispatch)
259                .constructor_behavior(v8::ConstructorBehavior::Throw)
260                .data(data.into())
261                .build(scope)?;
262            registry.set(scope, key.into(), function.into())?;
263        }
264        registry.set_integrity_level(scope, v8::IntegrityLevel::Frozen)?;
265        Some(registry)
266    }
267}
268
269enum OpError {
270    Type(String),
271    Error(String),
272}
273
274impl OpError {
275    fn exception<'s>(&self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
276        let (is_type_error, message) = match self {
277            Self::Type(message) => (true, message),
278            Self::Error(message) => (false, message),
279        };
280        let message = v8::String::new(scope, message).unwrap();
281        if is_type_error {
282            v8::Exception::type_error(scope, message)
283        } else {
284            v8::Exception::error(scope, message)
285        }
286    }
287}
288
289impl Op {
290    /// Receives all positional JavaScript arguments without deserialization.
291    pub fn raw<F>(name: &'static str, function: F) -> Self
292    where
293        F: for<'s, 'i> Fn(
294                &mut v8::PinScope<'s, 'i>,
295                v8::FunctionCallbackArguments<'s>,
296            ) -> Option<v8::Local<'s, v8::Value>>
297            + 'static,
298    {
299        Self {
300            name,
301            callback: Box::new(function),
302        }
303    }
304
305    /// A positional op that returns V8 values directly and throws host errors.
306    pub fn try_raw<E>(
307        name: &'static str,
308        function: impl for<'s, 'i> Fn(
309            &mut v8::PinScope<'s, 'i>,
310            v8::FunctionCallbackArguments<'s>,
311        ) -> Result<v8::Local<'s, v8::Value>, E>
312        + 'static,
313    ) -> Self
314    where
315        E: std::error::Error,
316    {
317        Self::with_callback(name, move |scope, args| {
318            function(scope, args).map_err(|error| OpError::Error(error.to_string()))
319        })
320    }
321
322    fn with_callback(
323        name: &'static str,
324        function: impl for<'s, 'i> Fn(
325            &mut v8::PinScope<'s, 'i>,
326            v8::FunctionCallbackArguments<'s>,
327        ) -> Result<v8::Local<'s, v8::Value>, OpError>
328        + 'static,
329    ) -> Self {
330        Self::raw(name, move |scope, args| match function(scope, args) {
331            Ok(value) => Some(value),
332            Err(error) => {
333                let exception = error.exception(scope);
334                scope.throw_exception(exception);
335                None
336            }
337        })
338    }
339
340    pub fn new<A, R, E>(name: &'static str, function: impl Fn(A) -> Result<R, E> + 'static) -> Self
341    where
342        A: DeserializeOwned,
343        R: Serialize,
344        E: std::error::Error,
345    {
346        Self::with_scope(name, move |_, args| function(args))
347    }
348
349    /// A typed op with access to the caller's V8 scope. Pending JavaScript exceptions
350    /// propagate to the caller; returning `Err` replaces them with a host error.
351    pub fn with_scope<A, R, E>(
352        name: &'static str,
353        function: impl for<'s, 'i> Fn(&mut v8::PinScope<'s, 'i>, A) -> Result<R, E> + 'static,
354    ) -> Self
355    where
356        A: DeserializeOwned,
357        R: Serialize,
358        E: std::error::Error,
359    {
360        Self::with_callback(name, move |scope, args| {
361            let args = serde_v8::from_v8(scope, args.get(0))
362                .map_err(|error| OpError::Type(error.to_string()))?;
363            let result =
364                function(scope, args).map_err(|error| OpError::Error(error.to_string()))?;
365            serde_v8::to_v8(scope, result).map_err(|error| OpError::Error(error.to_string()))
366        })
367    }
368
369    pub fn asynchronous<A, R, E, F>(name: &'static str, function: impl Fn(A) -> F + 'static) -> Self
370    where
371        A: DeserializeOwned,
372        R: Serialize + 'static,
373        E: std::error::Error + 'static,
374        F: Future<Output = Result<R, E>> + 'static,
375    {
376        Self::asynchronous_with_liveness(name, function, true)
377    }
378
379    pub fn asynchronous_unreferenced<A, R, E, F>(
380        name: &'static str,
381        function: impl Fn(A) -> F + 'static,
382    ) -> Self
383    where
384        A: DeserializeOwned,
385        R: Serialize + 'static,
386        E: std::error::Error + 'static,
387        F: Future<Output = Result<R, E>> + 'static,
388    {
389        Self::asynchronous_with_liveness(name, function, false)
390    }
391
392    fn asynchronous_with_liveness<A, R, E, F>(
393        name: &'static str,
394        function: impl Fn(A) -> F + 'static,
395        referenced: bool,
396    ) -> Self
397    where
398        A: DeserializeOwned,
399        R: Serialize + 'static,
400        E: std::error::Error + 'static,
401        F: Future<Output = Result<R, E>> + 'static,
402    {
403        Self::with_callback(name, move |scope, args| {
404            let args = serde_v8::from_v8(scope, args.get(0))
405                .map_err(|error| OpError::Type(error.to_string()))?;
406            let future = function(args);
407            let resolver = v8::PromiseResolver::new(scope).unwrap();
408            let promise = resolver.get_promise(scope);
409            let resolver = v8::Global::new(scope, resolver);
410            let pending = scope.get_slot::<Rc<RefCell<PendingOps>>>().unwrap().clone();
411            let mut pending = pending.borrow_mut();
412            if referenced {
413                pending.referenced += 1;
414            }
415            pending.futures.push(Pending::Op(Some(OpCompletion {
416                future: Box::pin(OpFuture {
417                    future: futures::future::maybe_done(future),
418                    resolver,
419                }),
420                referenced,
421            })));
422            drop(pending);
423            if let Some(waker) = scope.get_slot::<std::task::Waker>() {
424                waker.wake_by_ref();
425            }
426            Ok(promise.into())
427        })
428    }
429}
430
431pub(crate) fn clear(isolate: &mut v8::Isolate) {
432    isolate.remove_slot::<Rc<RefCell<PendingOps>>>();
433    isolate.remove_slot::<Rejections>();
434    isolate.remove_slot::<HandledRejections>();
435}
436
437pub(crate) fn init(isolate: &mut v8::Isolate) {
438    isolate.set_slot::<Rejections>(Rc::new(RefCell::new(Vec::new())));
439    isolate.set_slot::<HandledRejections>(Rc::new(RefCell::new(Vec::new())));
440    isolate.set_promise_reject_callback(promise_rejected);
441    isolate.set_slot::<Rc<RefCell<PendingOps>>>(Rc::new(RefCell::new(PendingOps {
442        futures: FuturesUnordered::new(),
443        referenced: 0,
444    })));
445}
446
447pub(crate) fn enqueue_import(
448    scope: &mut v8::PinScope,
449    future: LocalBoxFuture<'static, crate::modules::ImportPreparation>,
450) {
451    let pending = scope.get_slot::<Rc<RefCell<PendingOps>>>().unwrap().clone();
452    let mut pending = pending.borrow_mut();
453    pending.referenced += 1;
454    pending.futures.push(Pending::Import(future));
455    drop(pending);
456    if let Some(waker) = scope.get_slot::<std::task::Waker>() {
457        waker.wake_by_ref();
458    }
459}
460
461pub(crate) fn process_rejections(scope: &mut v8::PinScope) -> Option<v8::Global<v8::Value>> {
462    let handled = scope.get_slot::<HandledRejections>().unwrap().clone();
463    let reports = handled.borrow_mut().drain(..).collect::<Vec<_>>();
464    for (promise, reason) in reports {
465        crate::reporting::report_rejection_handled(
466            scope,
467            v8::Local::new(scope, &promise),
468            v8::Local::new(scope, &reason),
469        );
470    }
471    let rejections = scope.get_slot::<Rejections>().unwrap().clone();
472    rejections
473        .borrow_mut()
474        .retain(|rejection| match &rejection.promise {
475            RejectionPromise::Pending(_) => true,
476            RejectionPromise::Reported(weak) => !weak.is_empty(),
477        });
478    let mut fatal = None;
479    loop {
480        let (promise, reason) = {
481            let rejections = rejections.borrow();
482            let Some(rejection) = rejections
483                .iter()
484                .find(|rejection| matches!(rejection.promise, RejectionPromise::Pending(_)))
485            else {
486                break;
487            };
488            let RejectionPromise::Pending(promise) = &rejection.promise else {
489                unreachable!();
490            };
491            (promise.clone(), rejection.reason.as_ref().unwrap().clone())
492        };
493        let promise_local = v8::Local::new(scope, &promise);
494        let reason_local = v8::Local::new(scope, &reason);
495        let canceled =
496            crate::reporting::report_unhandled_rejection(scope, promise_local, reason_local);
497        let weak = v8::Weak::new(scope, promise_local);
498        let mut rejections = rejections.borrow_mut();
499        let Some(rejection) = rejections.iter_mut().find(|rejection| {
500            matches!(&rejection.promise, RejectionPromise::Pending(promise) if v8::Local::new(scope, promise) == promise_local)
501        }) else {
502            continue;
503        };
504        rejection.promise = RejectionPromise::Reported(weak);
505        rejection.reason.take();
506        drop(rejections);
507        if !canceled && fatal.is_none() {
508            fatal = Some(reason);
509        }
510    }
511    if let Some(error) = &fatal {
512        crate::reporting::suppress_exception(scope, v8::Local::new(scope, error));
513    }
514    fatal
515}
516
517pub(crate) fn checkpoint(scope: &mut v8::PinScope) {
518    loop {
519        scope.perform_microtask_checkpoint();
520        if !v8::Platform::pump_message_loop(&v8::V8::get_current_platform(), scope, false) {
521            break;
522        }
523    }
524}
525
526pub(crate) fn has_pending_work(isolate: &v8::Isolate) -> bool {
527    !isolate
528        .get_slot::<Rc<RefCell<PendingOps>>>()
529        .unwrap()
530        .borrow()
531        .futures
532        .is_empty()
533}
534
535pub(crate) fn poll_event_loop(scope: &mut v8::PinScope, context: &mut Context<'_>) -> Option<bool> {
536    if crate::tasks::run_next(scope)? {
537        return Some(true);
538    }
539    let pending = scope.get_slot::<Rc<RefCell<PendingOps>>>().unwrap().clone();
540    let completion = pending.borrow_mut().futures.poll_next_unpin(context);
541    let Poll::Ready(Some(completion)) = completion else {
542        return Some(false);
543    };
544
545    match completion {
546        Completion::Import(preparation) => {
547            pending.borrow_mut().referenced -= 1;
548            crate::modules::import_prepared(scope, preparation);
549        }
550        Completion::Op(mut completion) => {
551            if completion.referenced {
552                pending.borrow_mut().referenced -= 1;
553            }
554            completion.future.as_mut().complete(scope);
555        }
556    }
557    Some(true)
558}
559
560pub(crate) fn run_event_loop(
561    scope: &mut v8::PinScope,
562    promise: Option<v8::Local<v8::Promise>>,
563    wait_for_idle: bool,
564) -> Option<()> {
565    let pending = scope.get_slot::<Rc<RefCell<PendingOps>>>().unwrap().clone();
566    loop {
567        checkpoint(scope);
568        if scope.is_execution_terminating() {
569            return None;
570        }
571        if let Some(error) = crate::microtasks::process_exceptions(scope) {
572            let error = v8::Local::new(scope, error);
573            crate::reporting::mark_processed_exception(scope, error);
574            scope.throw_exception(error);
575            return None;
576        }
577        if let Some(promise) = promise
578            && promise.state() == v8::PromiseState::Rejected
579        {
580            let error = promise.result(scope);
581            crate::reporting::report_exception_for_value(scope, error);
582            crate::reporting::mark_processed_exception(scope, error);
583            scope.throw_exception(error);
584            return None;
585        }
586        if let Some(error) = process_rejections(scope) {
587            let error = v8::Local::new(scope, error);
588            scope.throw_exception(error);
589            return None;
590        }
591        if !wait_for_idle
592            && promise.is_some_and(|promise| promise.state() == v8::PromiseState::Fulfilled)
593        {
594            return Some(());
595        }
596        if crate::tasks::run_next(scope)? {
597            continue;
598        }
599        let completion = if pending.borrow().referenced == 0 {
600            let mut pending = pending.borrow_mut();
601            let Some(completion) = pending.futures.next().now_or_never().flatten() else {
602                return match promise.map(|promise| promise.state()) {
603                    None | Some(v8::PromiseState::Fulfilled) => Some(()),
604                    _ => {
605                        let message = v8::String::new(
606                            scope,
607                            "Module evaluation is still pending with no runnable host work",
608                        )
609                        .unwrap();
610                        let error = v8::Exception::error(scope, message);
611                        scope.throw_exception(error);
612                        None
613                    }
614                };
615            };
616            completion
617        } else {
618            futures::executor::block_on(pending.borrow_mut().futures.next()).unwrap()
619        };
620        let mut completion = match completion {
621            Completion::Op(completion) => completion,
622            Completion::Import(preparation) => {
623                pending.borrow_mut().referenced -= 1;
624                crate::modules::import_prepared(scope, preparation);
625                continue;
626            }
627        };
628        if completion.referenced {
629            pending.borrow_mut().referenced -= 1;
630        }
631        completion.future.as_mut().complete(scope);
632    }
633}
634
635fn dispatch<'s>(
636    scope: &mut v8::PinScope<'s, '_>,
637    args: v8::FunctionCallbackArguments<'s>,
638    mut result: v8::ReturnValue,
639) {
640    // Retain the closures across guest reentrancy without borrowing the isolate slot.
641    let ops = scope.get_slot::<RegisteredOps>().cloned();
642    let op = v8::Local::<v8::Uint32>::try_from(args.data())
643        .ok()
644        .and_then(|index| ops.as_ref()?.ops.get(index.value() as usize));
645    let Some(op) = op else {
646        let error = OpError::Error("Op is not registered in this isolate".into()).exception(scope);
647        scope.throw_exception(error);
648        return;
649    };
650    if let Some(value) = (op.callback)(scope, args) {
651        result.set(value);
652    }
653}
654
655#[cfg(test)]
656mod tests;