embedding a javascript runtime in rust, part 2: mixing languages
previously we created some rust bindings for ((o) duktape, a javascript runtime that turned out to be pretty easy to embed.
we want to be able to pass structs from rust to javascript, so that scripts can act on the same information as the rest of the program.
say we have a struct that represents the author of a blog post.
src/main.rs > fn main()
fn main()struct Author {
name: String,
link_url: String,
avatar_url: String,
email_address: String,
phone_number: String,
pronouns: String,
bio: String,
}
let author = Author {
name: "shuppy".to_owned(),
link_url: "https://shuppy.org".to_owned(),
avatar_url: "https://shuppy.org/avatar.png".to_owned(),
email_address: "test@shuppy.org".to_owned(),
phone_number: "+61 491 570 006".to_owned(),
pronouns: "she/her".to_owned(),
bio: "
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
".repeat(100),
};
unsafe {
todo!();
}
4. calling js functions in rust
we want to let the user write a linkify_author() function to customise the markdown we generate for links to the author. first we create a heap and context, then we run the user’s custom code, then we call the user’s function.
src/main.rs > fn main() > todo!()
fn main() > todo!()let ctx = duk_create_heap(None, None, None, null_mut(), None);
duk_push_c_function(ctx, Some(print), DUK_VARARGS);
duk_put_global_string(ctx, c"print".as_ptr());
// run the user’s custom code
duk_eval_string(ctx, cr#"
function linkify_author(author) {
return "[" + author.name + "](" + author.link_url + ")";
}
"#.as_ptr());
duk_pop(ctx);
// TODO: call the user’s function
// `result = pop()`
println!("{:?}", CStr::from_ptr(duk_get_string(ctx, -1)));
duk_pop(ctx);
duk_destroy_heap(ctx);
duktape’s interpreter is a stack machine. we interact with that machine by feeding it instructions that push values onto, pop values off, read from, or reorder the stack.
to call linkify_author(), we push the function, then the arguments (the author), then call that function with one argument, leaving the return value on the stack. but the author is a rust struct, so how do we push that?
src/main.rs > fn main() > todo!() > // TODO: call the user’s function
fn main() > todo!() > // TODO: call the user’s function// `push(linkify_author(rust author))`
duk_get_global_string(ctx, c"linkify_author".as_ptr());
// TODO: duk_push_something(rust author);
duk_call(ctx, 1);
5. exporting rust structs to js
we need to build an object in javascript representing the Author value we have in rust.
let’s reorder our instructions a bit, to untangle building the object from calling the function. this is more readable, but probably less efficient.
src/main.rs > fn main() > todo!() > // TODO: call the user’s function
fn main() > todo!() > // TODO: call the user’s function// TODO: duk_push_something(rust author);
// `push(linkify_author(rust author))`
duk_get_global_string(ctx, c"linkify_author".as_ptr());
duk_pull(ctx, -2);
duk_call(ctx, 1);
the obvious approach would be to build a javascript object that has all of the same fields as the rust Author, converted to javascript types.
src/main.rs > fn main() > todo!() > // TODO: duk_push_something(rust author);
fn main() > todo!() > // TODO: duk_push_something(rust author);// `author = {};`
// `for (key in author) author[key] = convert rust author[key];`
// `push(author)`
duk_push_object(ctx);
duk_push_lstring(ctx, author.name.as_ptr() as *const _, author.name.len());
duk_put_prop_string(ctx, -2, c"name".as_ptr());
duk_push_lstring(ctx, author.link_url.as_ptr() as *const _, author.link_url.len());
duk_put_prop_string(ctx, -2, c"link_url".as_ptr());
duk_push_lstring(ctx, author.avatar_url.as_ptr() as *const _, author.avatar_url.len());
duk_put_prop_string(ctx, -2, c"avatar_url".as_ptr());
duk_push_lstring(ctx, author.email_address.as_ptr() as *const _, author.email_address.len());
duk_put_prop_string(ctx, -2, c"email_address".as_ptr());
duk_push_lstring(ctx, author.phone_number.as_ptr() as *const _, author.phone_number.len());
duk_put_prop_string(ctx, -2, c"phone_number".as_ptr());
duk_push_lstring(ctx, author.pronouns.as_ptr() as *const _, author.pronouns.len());
duk_put_prop_string(ctx, -2, c"pronouns".as_ptr());
duk_push_lstring(ctx, author.bio.as_ptr() as *const _, author.bio.len());
duk_put_prop_string(ctx, -2, c"bio".as_ptr());
this works!
$ cargo run
"[shuppy](https://shuppy.org)"
but if the javascript code doesn’t end up reading all of the fields, then we’ve wasted time copying a bunch of strings for nothing.
what if we instead build an object that wraps an “inner” rust pointer, with getters for each field that only copy strings into javascript when needed?
src/main.rs > fn main() > todo!() > // TODO: duk_push_something(rust author);
fn main() > todo!() > // TODO: duk_push_something(rust author);// `author = {inner: rust author};`
// `for (key in author) Object.defineProperty(author, key, {get});`
// `push(author)`
duk_push_object(ctx);
duk_push_pointer(ctx, &author as *const _ /* 🗞️ bad */ as *mut _);
duk_put_prop_string(ctx, -2, c"inner".as_ptr());
macro_rules! define_author_getter {
($ctx:ident, $key:literal, |author| author.$field:ident) => {{
duk_push_string($ctx, $key.as_ptr());
duk_push_c_function($ctx, Some(get), 1);
duk_def_prop($ctx, -3, DUK_DEFPROP_HAVE_GETTER);
extern "C" fn get(ctx: *mut duk_context) -> duk_ret_t {
unsafe {
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, c"inner".as_ptr());
let author = duk_get_pointer(ctx, -1) as *const Author;
duk_pop_2(ctx);
let result = &(*author).$field;
duk_push_lstring(ctx, result.as_ptr() as *const _, result.len());
}
1
}
}};
}
define_author_getter!(ctx, c"name", |author| author.name);
define_author_getter!(ctx, c"link_url", |author| author.link_url);
define_author_getter!(ctx, c"avatar_url", |author| author.avatar_url);
define_author_getter!(ctx, c"email_address", |author| author.email_address);
define_author_getter!(ctx, c"phone_number", |author| author.phone_number);
define_author_getter!(ctx, c"pronouns", |author| author.pronouns);
define_author_getter!(ctx, c"bio", |author| author.bio);
we’re defining getters for each field on the instance, and if we have more than one instance, that’s quite a few instructions per instance. let’s define them on a prototype, which many instances can cheaply reuse.
src/main.rs > fn main() > todo!() > // TODO: duk_push_something(rust author);
fn main() > todo!() > // TODO: duk_push_something(rust author);duk_eval_string(ctx, cr#"
function Author(inner) {
this.inner = inner;
}
"#.as_ptr());
duk_pop(ctx);
// `for (key in author) Object.defineProperty(Author.prototype, key, {get})`
duk_eval_string(ctx, c"Author.prototype".as_ptr());
macro_rules! define_author_getter {
($ctx:ident, $key:literal, |author| author.$field:ident) => {{
duk_push_string($ctx, $key.as_ptr());
duk_push_c_function($ctx, Some(get), 1);
duk_def_prop($ctx, -3, DUK_DEFPROP_HAVE_GETTER);
extern "C" fn get(ctx: *mut duk_context) -> duk_ret_t {
unsafe {
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, c"inner".as_ptr());
let author = duk_get_pointer(ctx, -1) as *const Author;
duk_pop_2(ctx);
let result = &(*author).$field;
duk_push_lstring(ctx, result.as_ptr() as *const _, result.len());
}
1
}
}};
}
define_author_getter!(ctx, c"name", |author| author.name);
define_author_getter!(ctx, c"link_url", |author| author.link_url);
define_author_getter!(ctx, c"avatar_url", |author| author.avatar_url);
define_author_getter!(ctx, c"email_address", |author| author.email_address);
define_author_getter!(ctx, c"phone_number", |author| author.phone_number);
define_author_getter!(ctx, c"pronouns", |author| author.pronouns);
define_author_getter!(ctx, c"bio", |author| author.bio);
duk_pop(ctx);
// `push(new Author(rust author))`
duk_get_global_string(ctx, c"Author".as_ptr());
duk_push_pointer(ctx, &author as *const _ /* 🗞️ bad */ as *mut _);
duk_new(ctx, 1);
indeed this is the fastest approach! if we subtract the ~360ns of work that is common to all three approaches, they take ~1167ns, ~595ns, and ~251ns respectively.
$ cargo +nightly bench --features bench
test empty_object ... bench: 360.78 ns/iter (+/- 24.98)
test obvious_approach ... bench: 1,527.54 ns/iter (+/- 11.06)
test with_getters ... bench: 955.30 ns/iter (+/- 10.53)
test with_prototype ... bench: 611.16 ns/iter (+/- 6.16)
stay tuned!
next time, we’ll look at building some abstractions over these bindings, plus some tools to help us manage the interpreter’s value stack correctly.
full src/main.rs
mod sys;
use std::{ffi::CStr, io::{Write, stdout}, ptr::null_mut};
use crate::sys::*;
fn main() {
struct Author {
name: String,
link_url: String,
avatar_url: String,
email_address: String,
phone_number: String,
pronouns: String,
bio: String,
}
let author = Author {
name: "shuppy".to_owned(),
link_url: "https://shuppy.org".to_owned(),
avatar_url: "https://shuppy.org/avatar.png".to_owned(),
email_address: "test@shuppy.org".to_owned(),
phone_number: "+61 491 570 006".to_owned(),
pronouns: "she/her".to_owned(),
bio: "
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
".repeat(100),
};
unsafe {
let ctx = duk_create_heap(None, None, None, null_mut(), None);
duk_push_c_function(ctx, Some(print), DUK_VARARGS);
duk_put_global_string(ctx, c"print".as_ptr());
// run the user’s custom code
duk_eval_string(ctx, cr#"
function linkify_author(author) {
return "[" + author.name + "](" + author.link_url + ")";
}
"#.as_ptr());
duk_pop(ctx);
duk_eval_string(ctx, cr#"
function Author(inner) {
this.inner = inner;
}
"#.as_ptr());
duk_pop(ctx);
// `for (key in author) Object.defineProperty(Author.prototype, key, {get})`
duk_eval_string(ctx, c"Author.prototype".as_ptr());
macro_rules! define_author_getter {
($ctx:ident, $key:literal, |author| author.$field:ident) => {{
duk_push_string($ctx, $key.as_ptr());
duk_push_c_function($ctx, Some(get), 1);
duk_def_prop($ctx, -3, DUK_DEFPROP_HAVE_GETTER);
extern "C" fn get(ctx: *mut duk_context) -> duk_ret_t {
unsafe {
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, c"inner".as_ptr());
let author = duk_get_pointer(ctx, -1) as *const Author;
duk_pop_2(ctx);
let result = &(*author).$field;
duk_push_lstring(ctx, result.as_ptr() as *const _, result.len());
}
1
}
}};
}
define_author_getter!(ctx, c"name", |author| author.name);
define_author_getter!(ctx, c"link_url", |author| author.link_url);
define_author_getter!(ctx, c"avatar_url", |author| author.avatar_url);
define_author_getter!(ctx, c"email_address", |author| author.email_address);
define_author_getter!(ctx, c"phone_number", |author| author.phone_number);
define_author_getter!(ctx, c"pronouns", |author| author.pronouns);
define_author_getter!(ctx, c"bio", |author| author.bio);
duk_pop(ctx);
// `push(new Author(rust author))`
duk_get_global_string(ctx, c"Author".as_ptr());
duk_push_pointer(ctx, &author as *const _ /* 🗞️ bad */ as *mut _);
duk_new(ctx, 1);
// `push(linkify_author(rust author))`
duk_get_global_string(ctx, c"linkify_author".as_ptr());
duk_pull(ctx, -2);
duk_call(ctx, 1);
// `result = pop()`
println!("{:?}", CStr::from_ptr(duk_get_string(ctx, -1)));
duk_pop(ctx);
duk_destroy_heap(ctx);
}
}
extern "C" fn print(ctx: *mut duk_context) -> duk_ret_t {
unsafe {
duk_push_string(ctx, c" ".as_ptr());
duk_insert(ctx, 0);
duk_join(ctx, duk_get_top(ctx) - 1);
let text = duk_safe_to_string(ctx, -1);
let text = CStr::from_ptr(text);
let _ = stdout().write_all(text.to_bytes());
}
0
}
extra code for cargo bench
#![cfg_attr(feature = "bench", feature(test))]
#[cfg(feature = "bench")]
extern crate test;
#[cfg(feature = "bench")]
#[bench]
fn empty_object(bencher: &mut test::bench::Bencher) {
struct Author {
name: String,
link_url: String,
avatar_url: String,
email_address: String,
phone_number: String,
pronouns: String,
bio: String,
}
let author = Author {
name: "shuppy".to_owned(),
link_url: "https://shuppy.org".to_owned(),
avatar_url: "https://shuppy.org/avatar.png".to_owned(),
email_address: "test@shuppy.org".to_owned(),
phone_number: "+61 491 570 006".to_owned(),
pronouns: "she/her".to_owned(),
bio: "
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
".repeat(100),
};
unsafe {
let ctx = duk_create_heap(None, None, None, null_mut(), None);
duk_push_c_function(ctx, Some(print), DUK_VARARGS);
duk_put_global_string(ctx, c"print".as_ptr());
// run the user’s custom code
duk_eval_string(ctx, cr#"
function linkify_author(author) {
return "[" + author.name + "](" + author.link_url + ")";
}
"#.as_ptr());
duk_pop(ctx);
bencher.iter(|| {
// `push({})`
duk_push_object(ctx);
// `push(linkify_author(rust author))`
duk_get_global_string(ctx, c"linkify_author".as_ptr());
duk_pull(ctx, -2);
duk_call(ctx, 1);
// `result = pop()`
let result = CStr::from_ptr(duk_get_string(ctx, -1));
duk_pop(ctx);
result
});
duk_destroy_heap(ctx);
}
}
#[cfg(feature = "bench")]
#[bench]
fn obvious_approach(bencher: &mut test::bench::Bencher) {
struct Author {
name: String,
link_url: String,
avatar_url: String,
email_address: String,
phone_number: String,
pronouns: String,
bio: String,
}
let author = Author {
name: "shuppy".to_owned(),
link_url: "https://shuppy.org".to_owned(),
avatar_url: "https://shuppy.org/avatar.png".to_owned(),
email_address: "test@shuppy.org".to_owned(),
phone_number: "+61 491 570 006".to_owned(),
pronouns: "she/her".to_owned(),
bio: "
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
".repeat(100),
};
unsafe {
let ctx = duk_create_heap(None, None, None, null_mut(), None);
duk_push_c_function(ctx, Some(print), DUK_VARARGS);
duk_put_global_string(ctx, c"print".as_ptr());
// run the user’s custom code
duk_eval_string(ctx, cr#"
function linkify_author(author) {
return "[" + author.name + "](" + author.link_url + ")";
}
"#.as_ptr());
duk_pop(ctx);
bencher.iter(|| {
// `author = {};`
// `for (key in author) author[key] = convert rust author[key];`
// `push(author)`
duk_push_object(ctx);
duk_push_lstring(ctx, author.name.as_ptr() as *const _, author.name.len());
duk_put_prop_string(ctx, -2, c"name".as_ptr());
duk_push_lstring(ctx, author.link_url.as_ptr() as *const _, author.link_url.len());
duk_put_prop_string(ctx, -2, c"link_url".as_ptr());
duk_push_lstring(ctx, author.avatar_url.as_ptr() as *const _, author.avatar_url.len());
duk_put_prop_string(ctx, -2, c"avatar_url".as_ptr());
duk_push_lstring(ctx, author.email_address.as_ptr() as *const _, author.email_address.len());
duk_put_prop_string(ctx, -2, c"email_address".as_ptr());
duk_push_lstring(ctx, author.phone_number.as_ptr() as *const _, author.phone_number.len());
duk_put_prop_string(ctx, -2, c"phone_number".as_ptr());
duk_push_lstring(ctx, author.pronouns.as_ptr() as *const _, author.pronouns.len());
duk_put_prop_string(ctx, -2, c"pronouns".as_ptr());
duk_push_lstring(ctx, author.bio.as_ptr() as *const _, author.bio.len());
duk_put_prop_string(ctx, -2, c"bio".as_ptr());
// `push(linkify_author(rust author))`
duk_get_global_string(ctx, c"linkify_author".as_ptr());
duk_pull(ctx, -2);
duk_call(ctx, 1);
// `result = pop()`
let result = CStr::from_ptr(duk_get_string(ctx, -1));
duk_pop(ctx);
result
});
duk_destroy_heap(ctx);
}
}
#[cfg(feature = "bench")]
#[bench]
fn with_getters(bencher: &mut test::bench::Bencher) {
struct Author {
name: String,
link_url: String,
avatar_url: String,
email_address: String,
phone_number: String,
pronouns: String,
bio: String,
}
let author = Author {
name: "shuppy".to_owned(),
link_url: "https://shuppy.org".to_owned(),
avatar_url: "https://shuppy.org/avatar.png".to_owned(),
email_address: "test@shuppy.org".to_owned(),
phone_number: "+61 491 570 006".to_owned(),
pronouns: "she/her".to_owned(),
bio: "
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
".repeat(100),
};
unsafe {
let ctx = duk_create_heap(None, None, None, null_mut(), None);
duk_push_c_function(ctx, Some(print), DUK_VARARGS);
duk_put_global_string(ctx, c"print".as_ptr());
// run the user’s custom code
duk_eval_string(ctx, cr#"
function linkify_author(author) {
return "[" + author.name + "](" + author.link_url + ")";
}
"#.as_ptr());
duk_pop(ctx);
bencher.iter(|| {
// `author = {inner: rust author};`
// `for (key in author) Object.defineProperty(author, key, {get});`
// `push(author)`
duk_push_object(ctx);
duk_push_pointer(ctx, &author as *const _ /* 🗞️ bad */ as *mut _);
duk_put_prop_string(ctx, -2, c"inner".as_ptr());
macro_rules! define_author_getter {
($ctx:ident, $key:literal, |author| author.$field:ident) => {{
duk_push_string($ctx, $key.as_ptr());
duk_push_c_function($ctx, Some(get), 1);
duk_def_prop($ctx, -3, DUK_DEFPROP_HAVE_GETTER);
extern "C" fn get(ctx: *mut duk_context) -> duk_ret_t {
unsafe {
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, c"inner".as_ptr());
let author = duk_get_pointer(ctx, -1) as *const Author;
duk_pop_2(ctx);
let result = &(*author).$field;
duk_push_lstring(ctx, result.as_ptr() as *const _, result.len());
}
1
}
}};
}
define_author_getter!(ctx, c"name", |author| author.name);
define_author_getter!(ctx, c"link_url", |author| author.link_url);
define_author_getter!(ctx, c"avatar_url", |author| author.avatar_url);
define_author_getter!(ctx, c"email_address", |author| author.email_address);
define_author_getter!(ctx, c"phone_number", |author| author.phone_number);
define_author_getter!(ctx, c"pronouns", |author| author.pronouns);
define_author_getter!(ctx, c"bio", |author| author.bio);
// `push(linkify_author(rust author))`
duk_get_global_string(ctx, c"linkify_author".as_ptr());
duk_pull(ctx, -2);
duk_call(ctx, 1);
// `result = pop()`
let result = CStr::from_ptr(duk_get_string(ctx, -1));
duk_pop(ctx);
result
});
duk_destroy_heap(ctx);
}
}
#[cfg(feature = "bench")]
#[bench]
fn with_prototype(bencher: &mut test::bench::Bencher) {
struct Author {
name: String,
link_url: String,
avatar_url: String,
email_address: String,
phone_number: String,
pronouns: String,
bio: String,
}
let author = Author {
name: "shuppy".to_owned(),
link_url: "https://shuppy.org".to_owned(),
avatar_url: "https://shuppy.org/avatar.png".to_owned(),
email_address: "test@shuppy.org".to_owned(),
phone_number: "+61 491 570 006".to_owned(),
pronouns: "she/her".to_owned(),
bio: "
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
".repeat(100),
};
unsafe {
let ctx = duk_create_heap(None, None, None, null_mut(), None);
duk_push_c_function(ctx, Some(print), DUK_VARARGS);
duk_put_global_string(ctx, c"print".as_ptr());
// run the user’s custom code
duk_eval_string(ctx, cr#"
function linkify_author(author) {
return "[" + author.name + "](" + author.link_url + ")";
}
"#.as_ptr());
duk_pop(ctx);
duk_eval_string(ctx, cr#"
function Author(inner) {
this.inner = inner;
}
"#.as_ptr());
duk_pop(ctx);
// `for (key in author) Object.defineProperty(Author.prototype, key, {get})`
duk_eval_string(ctx, c"Author.prototype".as_ptr());
macro_rules! define_author_getter {
($ctx:ident, $key:literal, |author| author.$field:ident) => {{
duk_push_string($ctx, $key.as_ptr());
duk_push_c_function($ctx, Some(get), 1);
duk_def_prop($ctx, -3, DUK_DEFPROP_HAVE_GETTER);
extern "C" fn get(ctx: *mut duk_context) -> duk_ret_t {
unsafe {
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, c"inner".as_ptr());
let author = duk_get_pointer(ctx, -1) as *const Author;
duk_pop_2(ctx);
let result = &(*author).$field;
duk_push_lstring(ctx, result.as_ptr() as *const _, result.len());
}
1
}
}};
}
define_author_getter!(ctx, c"name", |author| author.name);
define_author_getter!(ctx, c"link_url", |author| author.link_url);
define_author_getter!(ctx, c"avatar_url", |author| author.avatar_url);
define_author_getter!(ctx, c"email_address", |author| author.email_address);
define_author_getter!(ctx, c"bio", |author| author.bio);
duk_pop(ctx);
bencher.iter(|| {
// `push(new Author(rust author))`
duk_get_global_string(ctx, c"Author".as_ptr());
duk_push_pointer(ctx, &author as *const _ /* 🗞️ bad */ as *mut _);
duk_new(ctx, 1);
// `push(linkify_author(rust author))`
duk_get_global_string(ctx, c"linkify_author".as_ptr());
duk_pull(ctx, -2);
duk_call(ctx, 1);
// `result = pop()`
let result = CStr::from_ptr(duk_get_string(ctx, -1));
duk_pop(ctx);
result
});
duk_destroy_heap(ctx);
}
}