Ændring af quickjs til at importere rustfunktioner - en ny måde at tænke på at udvide Kraken på
Kodeopbevaring
Tilblivelsen
Kraken er en højtydende webrenderingsmotor baseret på Flutter, som bruger quickjs som scriptingmotor.
Jeg ønskede at skrive nogle udvidelser til Kraken ved hjælp af rust.
Kraken understøtter skrivning af udvidelser ved hjælp af dart .
Brug af flutter_rust_bridge rust og dart.
Hvis man kombinerer disse to punkter, er det ikke svært at skrive Kraken-udvidelser ved hjælp af rust.
Denne løsning har dog et stort overhead i forhold til ydelsen, da der er en ydelsesmæssig straf for dart, der kalder rust, og en anden for quickjs, der kalder dart.
På den anden side, mens rust har rquickjs sådanne kald til biblioteket quickjs på rust.
De kalder dog quickjs i stedet for at indlejre quickjsog kan ikke bruges til at trylle quickjs.
I denne kodebase har jeg implementeret en ny løsning: direkte ændring af kildekoden til quickjs for at understøtte udvidelsen rust.
Dette er en generisk løsning, som ikke kun kan bruges til at ændre Kraken, men også til alle rammer og biblioteker, der indlejrer quickjs.
Demonstration
Koden til test.js er som følger :
const try_run = (func, ...args) => {
try {
func(...args)
} catch (err) {
console.log('❌', err.message)
console.log(err.stack)
}
};
import * as rust from 'rust'
console.log("export from rust :", Object.keys(rust))
import {
fib,
sleep
} from 'rust'
(async () => {
console.log('begin sleep 2s')
await sleep(2000);
console.log('sleep done')
console.log('fib(3) =', fib(3));
console.log("try catch example :")
try_run(fib);
try_run(fib, '*');
})()
Kør ./quickjs/qjs test.js, output :
export from rust : fib,sleep
begin sleep 2s
sleep done
fib(3) = 6
try catch example :
❌ miss : args need 1 pass 0
at fib (native)
at try_run (test.js:8)
at <anonymous> (test.js:27)
❌ not number : args position 0
at fib (native)
at try_run (test.js:6)
at <anonymous> (test.js:28)
Implementering af fib i rust
Fib-funktionen, der er importeret i js, er fra rust/src/export/fib.rs, og koden er som følger :
use crate::js::{self, arg};
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::os::raw::c_int;
#[js]
pub fn fib(n: i64) -> i64 {
if n <= 1 {
return if n == 1 { 1 } else { 0 };
}
n + fib(n - 1)
}
#[no_mangle]
pub extern "C" fn js_fib(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, fib_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => js::val(ctx, fib(n)),
}
}
I øjeblikket tilføjer proceduremakroen #[js] blot en konstant fib_args_len, der angiver antallet af argumenter til funktionen.
I fremtiden kan proceduremakroen ./rust_macro skrives for at muliggøre fuldautomatisk funktionseksport.
Implementering af sleep-funktionen i rust
Sleep-funktionen, der er importeret i js, er fra rust/src/export/sleep.rs, og koden er som følger :
use crate::js::{self, arg};
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::os::raw::c_int;
#[js]
pub fn fib(n: i64) -> i64 {
if n <= 1 {
return if n == 1 { 1 } else { 0 };
}
n + fib(n - 1)
}
#[no_mangle]
pub extern "C" fn js_fib(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, fib_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => js::val(ctx, fib(n)),
}
}
use crate::{js::arg, qjs::run};
use async_io::Timer;
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::{os::raw::c_int, time::Duration};
#[js]
pub async fn sleep(n: u64) {
Timer::after(Duration::from_millis(n)).await;
}
#[no_mangle]
pub extern "C" fn js_sleep(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, sleep_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => run(ctx, async move {
sleep(n as u64).await;
Ok(())
}),
}
}
Som du kan se ovenfor, er alle de eksporterede funktioner defineret i mappen ./rust/src/export. Denne mappe mod.rs genereres automatisk, når ./rust/build.xsh køres, og alle .rs -filer eksporteres under den.
Læsning og validering af indgående js-parametre
Parametrene læses og valideres på src/js/arg.rs med følgende kode :
use crate::js::{self, arg};
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::os::raw::c_int;
#[js]
pub fn fib(n: i64) -> i64 {
if n <= 1 {
return if n == 1 { 1 } else { 0 };
}
n + fib(n - 1)
}
#[no_mangle]
pub extern "C" fn js_fib(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, fib_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => js::val(ctx, fib(n)),
}
}
use crate::{js::arg, qjs::run};
use async_io::Timer;
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::{os::raw::c_int, time::Duration};
#[js]
pub async fn sleep(n: u64) {
Timer::after(Duration::from_millis(n)).await;
}
#[no_mangle]
pub extern "C" fn js_sleep(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, sleep_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => run(ctx, async move {
sleep(n as u64).await;
Ok(())
}),
}
}
use crate::js::throw;
use quickjs_ffi::{JSContext, JSValue, JS_IsNumber, JS_ToInt64};
use std::{mem::MaybeUninit, os::raw::c_int};
pub(crate) fn arg_miss(ctx: *mut JSContext, argc: c_int, need: c_int) -> Result<(), JSValue> {
if argc < need {
throw(ctx, format!("miss : args need {need} pass {argc}"))?
}
Ok(())
}
pub(crate) fn arg_i64(ctx: *mut JSContext, argv: *mut JSValue, pos: isize) -> Result<i64, JSValue> {
unsafe {
let val = *argv.offset(pos);
if JS_IsNumber(val) == 0 {
throw(ctx, format!("not number : args position {pos}"))?
}
let mut n = MaybeUninit::uninit();
JS_ToInt64(ctx, n.as_mut_ptr() as _, val);
Ok(n.assume_init())
}
}
I øjeblikket er det kun antallet af argumenter, der kontrolleres, og i64-typen læses.
Du kan tilføje disse funktioner efter behov, se funktionerne i qjs_sys, der starter med JS_To.
Konvertering af datatyper fra rust til js
Typekonverteringen foretages på src/js/val.rs med følgende kode :
use crate::js::{self, arg};
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::os::raw::c_int;
#[js]
pub fn fib(n: i64) -> i64 {
if n <= 1 {
return if n == 1 { 1 } else { 0 };
}
n + fib(n - 1)
}
#[no_mangle]
pub extern "C" fn js_fib(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, fib_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => js::val(ctx, fib(n)),
}
}
use crate::{js::arg, qjs::run};
use async_io::Timer;
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::{os::raw::c_int, time::Duration};
#[js]
pub async fn sleep(n: u64) {
Timer::after(Duration::from_millis(n)).await;
}
#[no_mangle]
pub extern "C" fn js_sleep(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, sleep_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => run(ctx, async move {
sleep(n as u64).await;
Ok(())
}),
}
}
use crate::js::throw;
use quickjs_ffi::{JSContext, JSValue, JS_IsNumber, JS_ToInt64};
use std::{mem::MaybeUninit, os::raw::c_int};
pub(crate) fn arg_miss(ctx: *mut JSContext, argc: c_int, need: c_int) -> Result<(), JSValue> {
if argc < need {
throw(ctx, format!("miss : args need {need} pass {argc}"))?
}
Ok(())
}
pub(crate) fn arg_i64(ctx: *mut JSContext, argv: *mut JSValue, pos: isize) -> Result<i64, JSValue> {
unsafe {
let val = *argv.offset(pos);
if JS_IsNumber(val) == 0 {
throw(ctx, format!("not number : args position {pos}"))?
}
let mut n = MaybeUninit::uninit();
JS_ToInt64(ctx, n.as_mut_ptr() as _, val);
Ok(n.assume_init())
}
}
use quickjs_ffi::{JSContext, JSValue, JS_NewInt64, JS_NewString, JS_NULL, JS_UNDEFINED};
use std::ffi::CString;
pub enum Val {
None,
Undefined,
I64(i64),
CString(CString),
}
impl From<()> for Val {
fn from(_: ()) -> Self {
Val::Undefined
}
}
impl From<i64> for Val {
fn from(t: i64) -> Self {
Val::I64(t)
}
}
impl From<CString> for Val {
fn from(t: CString) -> Self {
Val::CString(t)
}
}
pub(crate) fn val(ctx: *mut JSContext, t: impl Into<Val>) -> JSValue {
match t.into() {
Val::None => JS_NULL,
Val::Undefined => JS_UNDEFINED,
Val::I64(n) => unsafe { JS_NewInt64(ctx, n) },
Val::CString(cstr) => unsafe { JS_NewString(ctx, cstr.as_ptr()) },
}
}
Der er kun defineret fire typer til konvertering fra None, (), i64og CString til js. Du kan tilføje så mange, som du vil.
Flere datatyper kan deklareres i funktionerne i qjs_sys, der starter med JS_New.
Udviklingsmiljø
Jeg udvikler på en Apple laptop, rust bruger 1.62.0-nightly.
Installer først direnv, gå til mappen og direnv allow i et stykke tid
Installer python3, og derefter pip3 install -r ./requirements.txt
køre ./build.xsh for at kompilere og køre demoen
Som standard vil det officielle quickjs-repositorium blive klonet, hvis du ønsker at ændre quickjs i Kraken-repositoriet, skal du først
git clone --recursive git@github.com:openkraken/kraken.git --depth=1
så gør følgende
rm -rf quickjs
ln -s ../kraken/bridge/third_party/quickjs .
Til sidst skal du genudføre ./build.xsh
Katalogstruktur
./quickjs_rust
Ændring af c-filen i quickjs-koden./quickjs_ffi
Eksporter funktionerne fraquickjsheaderfilen tilrust./rust
Brugrusttil at implementere funktionerne iquickjs./rust/src/qjs.rs
Gennemførelse af asynkrone opkald. Daquickjser single-threaded, skrives de funktionskald, der involvererquckjs, i hovedtråden.
./rust_macrorustGennemførelse af proceduremakroen#[js]I fremtiden kan du se wasmedge-quickjs for en automatisk eksport af rust-funktioner til js-funktioner. wasmedge-quickjs → JsFunctionTrampoline
Opbygning af scripts build.xsh
Lad os uden videre gå direkte til kildekoden til build.xsh build-scriptet
#!/usr/bin/env xonsh
from pathlib import Path
from os.path import dirname,abspath,exists,join
PWD = dirname(abspath(__file__))
cd @(PWD)
p".xonshrc".exists() && source .xonshrc
quickjs = 'quickjs'
if not exists(quickjs):
git clone git@github.com:bellard/@(quickjs).git --depth=1
./quickjs_rust/patch.py
./rust/build.xsh
./quickjs_rust/gen.py
def ln_s(li):
for arg in li.split(' '):
fp = join(quickjs,arg)
if not exists(fp):
ln -s @(PWD)/@(arg) @(fp)
ln_s('quickjs_rust rust quickjs_ffi rust_macro')
cd @(quickjs)
make qjs
cd @(PWD)
./quickjs/qjs --unhandled-rejection -m test.js 2>&1 | tee test.js.out
Forklaring af princippet
quickjs_rust/patch.py
Hvis du kører ./quickjs_rust/patch.py, vil der blive foretaget nogle mindre ændringer i kildekoden til quickjs.
En af funktionerne JS_AddRust bruges til at injicere i rustmodulet.
rust_run er indsprøjtet i JS_ExecutePendingJob for at kalde asynkrone funktioner.
Et skærmbillede af alle ændringerne vises nedenfor :

quickjs_rust.h
Af ovenstående ændringer kan du se, at vi har indført en ny header-fil quickjs_rust.h med følgende kode
#ifndef QUICKJS_RUST_H
#define QUICKJS_RUST_H
#include "../quickjs/quickjs.h"
#include "../rust/rust.h"
#define countof(x) (sizeof(x) / sizeof((x)[0]))
#define JS_RUSTFUNC_DEF(name) JS_CFUNC_DEF(#name, name##_args_len, js_##name)
#include "./js_rust_funcs.h"
static const unsigned int js_rust_funcs_count = countof(js_rust_funcs);
static int
js_rust_init(JSContext* ctx, JSModuleDef* m)
{
return JS_SetModuleExportList(ctx, m, js_rust_funcs,
js_rust_funcs_count);
}
#define JS_INIT_MODULE js_init_module_rust
JSModuleDef* JS_INIT_MODULE(JSContext* ctx, const char* module_name)
{
JSModuleDef* m;
m = JS_NewCModule(ctx, module_name, js_rust_init);
if (!m)
return NULL;
js_rust_init(ctx, m);
return m;
}
void JS_AddRust(JSContext* ctx, JSRuntime* rt)
{
JSModuleDef* m = JS_INIT_MODULE(ctx, "rust");
for (unsigned int i = 0; i < js_rust_funcs_count; i++) {
JS_AddModuleExport(ctx, m, js_rust_funcs[i].name);
}
rust_init(ctx, rt);
}
#endif
rust/rust.h
Du kan se, at quickjs_rust/quickjs_rust.h introducerer quickjs_rust/js_rust_funcs.h, som automatisk genereres fra rust-eksportfunktionens headerfil rust/rust.h og ikke bør ændres i hånden.
Og rust/rust.h genereres ved at kalde cbindgen fra ./rust/build.xsh.
rust/build.xsh
#!/usr/bin/env xonsh
from os.path import dirname,abspath
import platform
PWD = dirname(abspath(__file__))
cd @(PWD)
p"../.xonshrc".exists() && source ../.xonshrc
./src/export/mod.gen.py
system = platform.system().lower()
if system == 'darwin':
system = f'apple-{system}'
TARGET=f'{platform.machine()}-{system}'
def cbindgen():
cbindgen -q --config cbindgen.toml --crate rust --output rust.h
try:
cbindgen()
except:
cargo clean
cbindgen()
cargo build \
--release \
-Z build-std=std,panic_abort \
-Z build-std-features=panic_immediate_abort \
--target @(TARGET)
mv ./target/@(TARGET)/release/librust.a ./target/release
Udviklingsnoter
quickjs_ffi
Kode fra quijine/main/quijine_core/src/ffi.rs
med nogle mindre ændringer, idet den erstatter
use crate::js::{self, arg};
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::os::raw::c_int;
#[js]
pub fn fib(n: i64) -> i64 {
if n <= 1 {
return if n == 1 { 1 } else { 0 };
}
n + fib(n - 1)
}
#[no_mangle]
pub extern "C" fn js_fib(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, fib_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => js::val(ctx, fib(n)),
}
}
use crate::{js::arg, qjs::run};
use async_io::Timer;
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::{os::raw::c_int, time::Duration};
#[js]
pub async fn sleep(n: u64) {
Timer::after(Duration::from_millis(n)).await;
}
#[no_mangle]
pub extern "C" fn js_sleep(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, sleep_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => run(ctx, async move {
sleep(n as u64).await;
Ok(())
}),
}
}
use crate::js::throw;
use quickjs_ffi::{JSContext, JSValue, JS_IsNumber, JS_ToInt64};
use std::{mem::MaybeUninit, os::raw::c_int};
pub(crate) fn arg_miss(ctx: *mut JSContext, argc: c_int, need: c_int) -> Result<(), JSValue> {
if argc < need {
throw(ctx, format!("miss : args need {need} pass {argc}"))?
}
Ok(())
}
pub(crate) fn arg_i64(ctx: *mut JSContext, argv: *mut JSValue, pos: isize) -> Result<i64, JSValue> {
unsafe {
let val = *argv.offset(pos);
if JS_IsNumber(val) == 0 {
throw(ctx, format!("not number : args position {pos}"))?
}
let mut n = MaybeUninit::uninit();
JS_ToInt64(ctx, n.as_mut_ptr() as _, val);
Ok(n.assume_init())
}
}
use quickjs_ffi::{JSContext, JSValue, JS_NewInt64, JS_NewString, JS_NULL, JS_UNDEFINED};
use std::ffi::CString;
pub enum Val {
None,
Undefined,
I64(i64),
CString(CString),
}
impl From<()> for Val {
fn from(_: ()) -> Self {
Val::Undefined
}
}
impl From<i64> for Val {
fn from(t: i64) -> Self {
Val::I64(t)
}
}
impl From<CString> for Val {
fn from(t: CString) -> Self {
Val::CString(t)
}
}
pub(crate) fn val(ctx: *mut JSContext, t: impl Into<Val>) -> JSValue {
match t.into() {
Val::None => JS_NULL,
Val::Undefined => JS_UNDEFINED,
Val::I64(n) => unsafe { JS_NewInt64(ctx, n) },
Val::CString(cstr) => unsafe { JS_NewString(ctx, cstr.as_ptr()) },
}
}
pub use libquickjs_sys::*;
til
use crate::js::{self, arg};
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::os::raw::c_int;
#[js]
pub fn fib(n: i64) -> i64 {
if n <= 1 {
return if n == 1 { 1 } else { 0 };
}
n + fib(n - 1)
}
#[no_mangle]
pub extern "C" fn js_fib(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, fib_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => js::val(ctx, fib(n)),
}
}
use crate::{js::arg, qjs::run};
use async_io::Timer;
use quickjs_ffi::{JSContext, JSValue};
use rust_macro::js;
use std::{os::raw::c_int, time::Duration};
#[js]
pub async fn sleep(n: u64) {
Timer::after(Duration::from_millis(n)).await;
}
#[no_mangle]
pub extern "C" fn js_sleep(
ctx: *mut JSContext,
_this: JSValue,
argc: c_int,
argv: *mut JSValue,
) -> JSValue {
if let Err(err) = arg::arg_miss(ctx, argc, sleep_args_len) {
return err;
}
match arg::arg_i64(ctx, argv, 0) {
Err(err) => err,
Ok(n) => run(ctx, async move {
sleep(n as u64).await;
Ok(())
}),
}
}
use crate::js::throw;
use quickjs_ffi::{JSContext, JSValue, JS_IsNumber, JS_ToInt64};
use std::{mem::MaybeUninit, os::raw::c_int};
pub(crate) fn arg_miss(ctx: *mut JSContext, argc: c_int, need: c_int) -> Result<(), JSValue> {
if argc < need {
throw(ctx, format!("miss : args need {need} pass {argc}"))?
}
Ok(())
}
pub(crate) fn arg_i64(ctx: *mut JSContext, argv: *mut JSValue, pos: isize) -> Result<i64, JSValue> {
unsafe {
let val = *argv.offset(pos);
if JS_IsNumber(val) == 0 {
throw(ctx, format!("not number : args position {pos}"))?
}
let mut n = MaybeUninit::uninit();
JS_ToInt64(ctx, n.as_mut_ptr() as _, val);
Ok(n.assume_init())
}
}
use quickjs_ffi::{JSContext, JSValue, JS_NewInt64, JS_NewString, JS_NULL, JS_UNDEFINED};
use std::ffi::CString;
pub enum Val {
None,
Undefined,
I64(i64),
CString(CString),
}
impl From<()> for Val {
fn from(_: ()) -> Self {
Val::Undefined
}
}
impl From<i64> for Val {
fn from(t: i64) -> Self {
Val::I64(t)
}
}
impl From<CString> for Val {
fn from(t: CString) -> Self {
Val::CString(t)
}
}
pub(crate) fn val(ctx: *mut JSContext, t: impl Into<Val>) -> JSValue {
match t.into() {
Val::None => JS_NULL,
Val::Undefined => JS_UNDEFINED,
Val::I64(n) => unsafe { JS_NewInt64(ctx, n) },
Val::CString(cstr) => unsafe { JS_NewString(ctx, cstr.as_ptr()) },
}
}
pub use libquickjs_sys::*;
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/c.rs"));
Undefined symbols for architecture x86_64: "_JS_ToInt32"
Ændre ". /rust/Cargo.toml" som følger, idet kun staticlib'en bevares
[lib]
#crate-type = ["lib", "cdylib", "staticlib"]
crate-type = ["staticlib"]
Referencer
- Fra
JS-motoren tilJSruntime (øverst) (nederst) - Udvikling af et indfødt modul til
QuickJSi C - Brug Rust til at implementere JS API
- QuickJS eksempler
- rust-bindgen
- Sådan oprettes asynkron kode til
QuickJS - rquickjs → JS_NewPromiseCapability
- wasmedge-quickjs → new_promise
- wasmedge-quickjs → JsMethod
- wasmedge-quickjs → opkald
- Den umærkelige fælde - låser i Rust
Om
Dette projekt er en del af kodeprojektet rmw.link ( rmw.link ).