Skip to content

Commit

Permalink
Migrate all of the APIs to use rquickjs
Browse files Browse the repository at this point in the history
This commit migrates the `console`, `stream_io`, `random` and `text_encoding`  APIs to use rquickjs. 

One notable change in this commit is the introduction of the `Args` struct to tie the lifetime of `Ctx<'js>` and `Rest<Value<'js>>` arguments, given that explicit lifetime binding is not possible in Rust closures at the moment.
  • Loading branch information
saulecabrera committed Apr 5, 2024
1 parent 300c3ca commit 878ddd5
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 227 deletions.
121 changes: 68 additions & 53 deletions crates/apis/src/console/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@ use std::io::Write;

use anyhow::{Error, Result};
use javy::{
quickjs::{
prelude::{MutFn, Rest},
Context, Ctx, Function, Object, Value,
},
quickjs::{prelude::MutFn, Context, Function, Object, Value},
Runtime,
};

use crate::{print, APIConfig, JSApiSet};
use crate::{print, APIConfig, Args, JSApiSet};

pub(super) use config::ConsoleConfig;
pub use config::LogStream;

mod config;

// TODO: #[derive(Default)]
pub(super) struct Console {}

impl Console {
Expand All @@ -34,41 +32,49 @@ impl JSApiSet for Console {
}
}

fn register_console<'js, T, U>(context: &Context, log_stream: T, error_stream: U) -> Result<()>
fn register_console<'js, T, U>(
context: &Context,
mut log_stream: T,
mut error_stream: U,
) -> Result<()>
where
T: Write,
U: Write,
T: Write + 'static,
U: Write + 'static,
{
context.with(|cx| {
let globals = cx.globals();
let console = Object::new(cx)?;
// TODO: Revisit the callback signatures, there's a possibility that we can
// actually convert from anyhow::Error to quickjs::Error.
context.with(|this| {
let globals = this.globals();
let console = Object::new(this.clone())?;

console.set(
"log",
Function::new(
cx,
MutFn::new(move |cx: Ctx<'js>, args: Rest<Value<'js>>| {
log(cx, &args, &mut log_stream).unwrap()
this.clone(),
MutFn::new(move |cx, args| {
log(Args::hold(cx, args), &mut log_stream).expect("console.log to succeed")
}),
)?,
)?;

console.set(
"error",
Function::new(
cx,
MutFn::new(move |cx: Ctx<'js>, args: Rest<Value<'js>>| {
log(cx, &args, &mut error_stream).unwrap()
this,
MutFn::new(move |cx, args| {
log(Args::hold(cx, args), &mut error_stream).expect("console.error to succeed")
}),
)?,
)?;

globals.set("console", console)?;
Ok::<_, Error>(())
});
})?;
Ok(())
}

fn log<'js, T: Write>(ctx: Ctx<'js>, args: &[Value<'js>], mut stream: T) -> Result<Value<'js>> {
fn log<'js, T: Write>(args: Args<'js>, stream: &mut T) -> Result<Value<'js>> {
let (ctx, args) = args.release();
let mut buf = String::new();
for (i, arg) in args.iter().enumerate() {
if i != 0 {
Expand All @@ -79,13 +85,16 @@ fn log<'js, T: Write>(ctx: Ctx<'js>, args: &[Value<'js>], mut stream: T) -> Resu

writeln!(stream, "{buf}")?;

Ok(Value::new_undefined(ctx))
Ok(Value::new_undefined(ctx.clone()))
}

#[cfg(test)]
mod tests {
use anyhow::Result;
use javy::Runtime;
use anyhow::{Error, Result};
use javy::{
quickjs::{Object, Value},
Runtime,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::{cmp, io};
Expand All @@ -99,9 +108,13 @@ mod tests {
fn test_register() -> Result<()> {
let runtime = Runtime::default();
Console::new().register(&runtime, &APIConfig::default())?;
let console = runtime.context().global_object()?.get_property("console")?;
assert!(console.get_property("log").is_ok());
assert!(console.get_property("error").is_ok());
runtime.context().with(|cx| {
let console: Object<'_> = cx.globals().get("console")?;
assert!(console.get::<&str, Value<'_>>("log").is_ok());
assert!(console.get::<&str, Value<'_>>("error").is_ok());

Ok::<_, Error>(())
})?;
Ok(())
}

Expand All @@ -113,24 +126,25 @@ mod tests {
let ctx = runtime.context();
register_console(ctx, stream.clone(), stream.clone())?;

ctx.eval_global("main", "console.log(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());
ctx.with(|this| {
this.eval("console.log(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());
stream.clear();

stream.clear();
this.eval("console.log(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());

ctx.eval_global("main", "console.log(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());
stream.clear();

stream.clear();
this.eval("console.log(2.3, true, { foo: 'bar' }, null, undefined)")?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);

Ok::<_, Error>(())
})?;

ctx.eval_global(
"main",
"console.log(2.3, true, { foo: 'bar' }, null, undefined)",
)?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);
Ok(())
}

Expand All @@ -142,24 +156,25 @@ mod tests {
let ctx = runtime.context();
register_console(ctx, stream.clone(), stream.clone())?;

ctx.eval_global("main", "console.error(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());
ctx.with(|this| {
this.eval("console.error(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());

stream.clear();
stream.clear();

ctx.eval_global("main", "console.error(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());
this.eval("console.error(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());

stream.clear();
stream.clear();

this.eval("console.error(2.3, true, { foo: 'bar' }, null, undefined)")?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);
Ok::<_, Error>(())
})?;

ctx.eval_global(
"main",
"console.error(2.3, true, { foo: 'bar' }, null, undefined)",
)?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);
Ok(())
}

Expand Down
22 changes: 21 additions & 1 deletion crates/apis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
use anyhow::Result;
use javy::Runtime;

use javy::quickjs::{Type, Value};
use javy::quickjs::{prelude::Rest, Ctx, Type, Value};
use std::fmt::Write;

pub use api_config::APIConfig;
Expand Down Expand Up @@ -126,3 +126,23 @@ pub(crate) fn print<'js>(val: &Value<'js>, sink: &mut String) -> Result<()> {
_ => unimplemented!(),
}
}

/// A struct to hold the current [Ctx] and [Value]s passed as arguments to Rust
/// functions.
/// A struct here is used to explicitly tie these values with a particular
/// lifetime.
//
// See: https://github.com/rust-lang/rfcs/pull/3216
pub(crate) struct Args<'js>(Ctx<'js>, Rest<Value<'js>>);

impl<'js> Args<'js> {
/// Tie the [Ctx] and [Rest<Value>].
fn hold(cx: Ctx<'js>, args: Rest<Value<'js>>) -> Self {
Self(cx, args)
}

/// Get the [Ctx] and [Rest<Value>].
fn release(self) -> (Ctx<'js>, Rest<Value<'js>>) {
(self.0, self.1)
}
}
21 changes: 14 additions & 7 deletions crates/apis/src/random/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,25 @@ impl JSApiSet for Random {
#[cfg(test)]
mod tests {
use crate::{random::Random, APIConfig, JSApiSet};
use anyhow::Result;
use javy::Runtime;
use anyhow::{Error, Result};
use javy::{quickjs::Value, Runtime};

#[test]
fn test_random() -> Result<()> {
let runtime = Runtime::default();
Random.register(&runtime, &APIConfig::default())?;
let ctx = runtime.context();
ctx.eval_global("test.js", "result = Math.random()")?;
let result = ctx.global_object()?.get_property("result")?.as_f64()?;
assert!(result >= 0.0);
assert!(result < 1.0);
runtime.context().with(|this| {
this.eval("result = Math.random()")?;
let result: f64 = this
.globals()
.get::<&str, Value<'_>>("result")?
.as_float()
.unwrap();
assert!(result >= 0.0);
assert!(result < 1.0);
Ok::<_, Error>(())
});

Ok(())
}
}
Loading

0 comments on commit 878ddd5

Please sign in to comment.