Skip to content

Commit 867f1ae

Browse files
committed
feat: finish new API sections
1 parent 01c3ef0 commit 867f1ae

File tree

1 file changed

+248
-10
lines changed

1 file changed

+248
-10
lines changed

blog/boa-release-21/index.md

Lines changed: 248 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,16 @@ biggest pain points of our API. This was mostly caused by how we defined
6666
`FutureJob`:
6767

6868
```rust
69+
pub struct NativeJob {
70+
f: Box<dyn FnOnce(&mut Context) -> JsResult<JsValue>>,
71+
realm: Option<Realm>,
72+
}
73+
6974
pub type FutureJob = Pin<Box<dyn Future<Output = NativeJob> + 'static>>;
7075
```
7176

7277
With this definition, it was pretty much impossible to capture the `Context`
73-
inside the future, and functions that needed to interweave engine operations
78+
inside the `Future`, and functions that needed to interweave engine operations
7479
with awaiting `Future`s would have to be split into multiple parts:
7580

7681
```rust
@@ -99,8 +104,7 @@ let fetch = async move {
99104
// `JobQueue`.
100105
context
101106
.job_queue()
102-
.enqueue_future_job(Box::pin(fetch), context)
103-
}
107+
.enqueue_future_job(Box::pin(fetch), context);
104108
```
105109

106110
We wanted to improve this API, and the solution we thought about was to make
@@ -173,16 +177,228 @@ pub trait JobExecutor: Any {
173177
}
174178
```
175179

176-
As you can probably tell, we made a lot of changes on the `JobExecutor`:
180+
As you can probably tell, we made a lot of changes on `JobExecutor`:
177181

178-
TODO
182+
- All methods now take `Rc<Self>` as their receiver, making it consistent with
183+
how the `Context` itself stores the `JobExecutor`.
184+
- [`enqueue_promise_job`] and [`enqueue_future_job`] now are unified in a single
185+
`enqueue_job`, where `Job` is an enum containing the type of job that needs to
186+
be scheduled. This makes it much simpler to extend the engine with newer job
187+
types in the future, such as the newly introduced `TimeoutJob` and `GenericJob`
188+
types.
189+
- `run_jobs_async` was converted to a proper async function, and excluded from
190+
`JobExecutor`'s VTable. Additionally, this method now takes a `&RefCell<&mut Context>`
191+
as its context, which is the missing piece that enables sharing the `Context` between
192+
multiple `Future`s at the same time. This, however, means that we cannot provide
193+
a convenient wrapper such as [`Context::run_jobs`] anymore, which is one of the
194+
reasons why we decided to exclude that method from `JobExecutor`'s VTable.
179195

180-
### Revamped `ModuleLoader`
196+
These changes not only made `JobExecutor` much simpler, but it also expanded
197+
the places where we could use its async capabilities to handle "special"
198+
features of ECMAScript that are more suited to an async way of doing things.
199+
`ModuleLoader` is one of those places.
181200

182-
TODO
201+
[`enqueue_promise_job`]: https://docs.rs/boa_engine/0.20.0/boa_engine/job/trait.JobQueue.html#tymethod.enqueue_promise_job
202+
[`enqueue_future_job`]: https://docs.rs/boa_engine/0.20.0/boa_engine/job/trait.JobQueue.html#tymethod.enqueue_future_job
203+
[`Context::run_jobs`]: https://docs.rs/boa_engine/0.20.0/boa_engine/context/struct.Context.html#method.run_jobs
204+
205+
### Asyncified `ModuleLoader`
206+
207+
Looking at the previous definition of `ModuleLoader`:
208+
209+
```rust
210+
pub trait ModuleLoader {
211+
// Required method
212+
fn load_imported_module(
213+
&self,
214+
referrer: Referrer,
215+
specifier: JsString,
216+
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>,
217+
context: &mut Context,
218+
);
219+
220+
// Provided methods
221+
fn register_module(&self, _specifier: JsString, _module: Module) { ... }
222+
fn get_module(&self, _specifier: JsString) -> Option<Module> { ... }
223+
fn init_import_meta(
224+
&self,
225+
_import_meta: &JsObject,
226+
_module: &Module,
227+
_context: &mut Context,
228+
) { ... }
229+
}
230+
```
231+
232+
... the weird `finish_load` on `load_imported_module` immediately pops up as an anomaly.
233+
In this case, `finish_load` is Boa's equivalent to
234+
[HostLoadImportedModule ( referrer, moduleRequest, hostDefined, payload )][hlim],
235+
which is an abstract operation that is primarily used to define how an application
236+
will load and resolve a "module request"; think of it as a function that takes
237+
the `"module-name"` from `import * as name from "module-name"`, then does
238+
"things" to load the module that corresponds to `"module_name"`.
239+
240+
[hlim]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
241+
242+
The peculiarity about this abstract operation is that it doesn't return anything!
243+
Instead, it just has a special requirement:
244+
245+
> The host environment must perform `FinishLoadingImportedModule(referrer, moduleRequest, payload, result)`,
246+
where result is either a normal completion containing the loaded `Module Record` or a throw completion,
247+
either synchronously or asynchronously.
248+
249+
Why expose the hook this way? Well, there is a clue in the previous requirement:
250+
251+
> ... either synchronously or asynchronously.
252+
253+
Aha! Directly returning from the hook makes it very hard to enable use cases
254+
where an application wants to load multiple modules asynchronously. Thus, the
255+
specification instead exposes a hook to pass the name of the module that needs to
256+
be loaded, and delegates the task of running the "post-load" phase to the host, which
257+
enables fetching modules synchronously or asynchronously, depending on the specific
258+
requirements of each application.
259+
260+
One downside of this definition, however, is that any data that is required
261+
by the engine to properly process the returned module would need to be transparently
262+
passed to the `FinishLoadingImportedModule` abstract operation, which is why
263+
the hook also has an additional requirement:
264+
265+
> The operation must treat `payload` as an opaque value to be passed through to
266+
`FinishLoadingImportedModule`.
267+
268+
`payload` is precisely that data, and it may change depending on how the module
269+
is imported in the code; `import "module"` and `import("module")` are two examples
270+
of this.
271+
272+
We could expose this as an opaque `*const ()` pointer argument and call it a day,
273+
but we're using Rust, dang it! and we like statically guaranteed safety!
274+
So, instead, we exposed `FinishLoadingImportedModule` as `finish_load`, which is a
275+
"closure" that captures `payload` on its stack, and can be called anywhere
276+
(like inside a `Future`) on the application with a proper `Module` and `Context`
277+
to further continue processing the module loaded by the `ModuleLoader`.
278+
279+
```rust
280+
...
281+
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>,
282+
...
283+
```
284+
285+
Unfortunately,
286+
this API has downsides: it is still possible to forget to call `finish_load`,
287+
which is still safe but prone to bugs. It is also really painful to work with,
288+
because you cannot capture the `Context` to further process the module after
289+
loading it ... Sounds familiar? **This is exactly [the code snippet we talked about before!](#async-apis-enhancements)**
290+
291+
Fast forward a couple of years and we're now changing big parts of `JobExecutor`:
292+
adding new job types, tinkering with `JobExecutor`, changing API signatures, etc.
293+
Then, while looking at the definition of `ModuleLoader`, we thought...
294+
295+
> Huh, can't we make `load_imported_module` async now?
296+
297+
And that's exactly what we did! Behold, the new `ModuleLoader`!
298+
299+
```rust
300+
pub trait ModuleLoader: Any {
301+
async fn load_imported_module(
302+
self: Rc<Self>,
303+
referrer: Referrer,
304+
specifier: JsString,
305+
context: &RefCell<&mut Context>,
306+
) -> JsResult<Module>;
307+
308+
fn init_import_meta(
309+
self: Rc<Self>,
310+
_import_meta: &JsObject,
311+
_module: &Module,
312+
_context: &mut Context,
313+
) {
314+
}
315+
}
316+
```
317+
318+
Then, the code snippet we mentioned before nicely simplifies to:
319+
320+
```rust
321+
async fn load_imported_module(
322+
self: Rc<Self>,
323+
_referrer: boa_engine::module::Referrer,
324+
specifier: JsString,
325+
context: &RefCell<&mut Context>,
326+
) -> JsResult<Module> {
327+
let url = specifier.to_std_string_escaped();
328+
329+
let response = async {
330+
let request = Request::get(&url)
331+
.redirect_policy(RedirectPolicy::Limit(5))
332+
.body(())?;
333+
let response = request.send_async().await?.text().await?;
334+
Ok(response)
335+
}
336+
.await
337+
.map_err(|err: isahc::Error| JsNativeError::typ().with_message(err.to_string()))?;
338+
339+
let source = Source::from_bytes(&response);
340+
341+
Module::parse(source, None, &mut context.borrow_mut())
342+
}
343+
```
344+
345+
> *What about synchronous applications?*
346+
347+
The advantage of having `JobExecutor` be the main entry point for any Rust
348+
`Future`s that are enqueued by the engine is that an application can decide how to
349+
handle all `Future`s received by the implementation of `JobExecutor`. Thus, an application
350+
that doesn't want to deal with async Rust executors can implement a completely synchronous
351+
`ModuleLoader` and poll on all futures received by `JobExecutor` using something like
352+
[`futures_lite::poll_once`][poll_once].
353+
354+
> *Why not just block on each `Future` one by one instead?*
355+
356+
Well, there is one new built-in that was introduced on this release which heavily
357+
depends on "properly" running `Future`s, and by "properly" we mean "not blocking
358+
the whole thread waiting on a future to finish". More on that in a bit.
183359

184360
### Built-ins updates
185361

362+
#### Atomics.waitAsync
363+
364+
This release adds support for the `Atomics.waitAsync` method introduced in
365+
ECMAScript's 2024 specification.
366+
This method allows doing thread synchronization just like `Atomics.wait`, but with
367+
the big difference that it will return a `Promise` that will resolve when the
368+
thread gets notified with the `Atomics.notify` method, instead of blocking until
369+
that happens.
370+
371+
```javascript
372+
// Given an `Int32Array` shared between two threads:
373+
374+
const sab = new SharedArrayBuffer(1024);
375+
const int32 = new Int32Array(sab);
376+
377+
// Thread 1 runs the following:
378+
// { async: true, value: Promise {<pending>} }
379+
const result = Atomics.waitAsync(int32, 0, 0, 1000);
380+
result.value.then(() => console.log("waited!"));
381+
382+
// And thread 2 runs the following after Thread 1:
383+
Atomics.notify(int32, 0);
384+
385+
// Then, in thread 1 we will (eventually) see "waited!" printed.
386+
```
387+
388+
Note that this built-in requires having a "proper" implementation of a `JobExecutor`; again, "proper"
389+
in the sense of "not blocking the whole thread waiting on a future to finish", which can be accomplished
390+
with [`FutureGroup`] and [`futures_lite::poll_once`][poll_once] if an async executor is not required
391+
(see [`SimpleJobExecutor`'s implementation][sje-impl]).
392+
This is because it heavily relies on `TimeoutJob` and `NativeAsyncJob` to timeout if a notification
393+
doesn't arrive and communicate with the notifier threads, respectively. This is the reason why
394+
we don't recommend just blocking on each received `Future`; that could cause
395+
`TimeoutJob`s to run much later than required, or even make it so that they don't
396+
run at all!
397+
398+
[poll_once]: https://docs.rs/futures-lite/latest/futures_lite/future/fn.poll_once.html
399+
[`FutureGroup`]: https://docs.rs/futures-concurrency/latest/futures_concurrency/future/future_group/struct.FutureGroup.html
400+
[sje-impl]: https://github.com/boa-dev/boa/blob/0468498b4bb9da31caa20123201e4d8ee132c608/core/engine/src/job.rs#L678
401+
186402
#### Set methods
187403

188404
This release adds support for the new set methods added in ECMAScript's 2025
@@ -234,12 +450,34 @@ let sum = Math.sumPrecise([1e20, 0.1, -1e20]);
234450
console.log(sum); // 0.1
235451
```
236452

237-
#### Atomics.waitAsync
453+
#### Array.fromAsync
238454

239-
TODO
455+
This release adds support for `Array.fromAsync`, which will be introduced in
456+
ECMAScript's 2026 specification.
240457

458+
`Array.fromAsync` allows to conveniently create a array from an async iterable by
459+
awaiting all of the items consecutively.
241460

242-
#### Array.fromAsync
461+
```javascript
462+
// Array.fromAsync is roughly equivalent to:
463+
async function toArray(asyncIterator){
464+
const arr=[];
465+
for await(const i of asyncIterator) arr.push(i);
466+
return arr;
467+
}
468+
469+
async function* asyncIterable() {
470+
for (let i = 0; i < 5; i++) {
471+
await new Promise((resolve) => setTimeout(resolve, 10 * i));
472+
yield i;
473+
}
474+
};
475+
476+
Array.fromAsync(asyncIterable()).then((array) => console.log(array));
477+
// [0, 1, 2, 3, 4]
478+
toArray(asyncIterable()).then((array) => console.log(array));
479+
// [0, 1, 2, 3, 4]
480+
```
243481

244482
## Boa Runtime
245483

0 commit comments

Comments
 (0)