Úprava quickjs na import funkcií rustu - nový spôsob uvažovania o rozšírení Krakenu
Úložisko kódu
Genéza
Kraken je vysoko výkonné webové vykresľovacie jadro založené na adrese Flutter, ktoré ako skriptovacie jadro používa quickjs.
Chcel som napísať nejaké rozšírenia pre Kraken pomocou rust.
Kraken podporuje zápis rozšírení pomocou adresy dart .
Používanie stránky flutter_rust_bridge rust a dart.
Kombináciou týchto dvoch bodov nie je ťažké napísať rozšírenia Kraken pomocou stránky rust.
Výkonnostná réžia tohto riešenia je však vysoká, pretože pri volaní adresy dart na adresu rust dochádza k zníženiu výkonu a pri volaní adresy quickjs na adresu dart k ďalšiemu zníženiu výkonu.
Na druhej strane, zatiaľ čo komunita rust má rquickjs takéto volania knižnice quickjs na adrese rust.
Namiesto vloženia však volajú quickjs quickjs a nedajú sa použiť na čarovanie quickjs.
V tejto databáze som implementoval nové riešenie: priamo upravil zdrojový kód quickjs tak, aby podporoval rozšírenie rust.
Ide o všeobecné riešenie, ktoré možno použiť nielen na úpravu Krakenu, ale aj pre všetky frameworky a knižnice, ktoré obsahujú quickjs.
Ukážka
Kód test.js je nasledovný :
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, '*');
})()
Spustite ./quickjs/qjs test.js, výstup :
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)
Implementácia fib v hrdzi
Funkcia fib importovaná v js je z rust/src/export/fib.rs a kód je nasledujúci :
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)),
}
}
V súčasnosti makro procedúry #[js] len pridáva konštantu fib_args_len, ktorá identifikuje počet argumentov funkcie.
V budúcnosti bude možné napísať makro procedúry ./rust_macro, ktoré umožní plne automatický export funkcií.
Implementácia funkcie sleep v jazyku rust
Funkcia sleep importovaná v js je z rust/src/export/sleep.rs a kód je nasledovný :
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(())
}),
}
}
Ako vidíte vyššie, všetky exportované funkcie sú definované v adresári ./rust/src/export. Tento adresár mod.rs sa automaticky vygeneruje pri spustení ./rust/build.xsh, pričom sa do neho exportujú všetky súbory .rs.
Čítanie a overovanie príchodzích parametrov js
Parametre sa načítajú a overujú na adrese src/js/arg.rs pomocou nasledujúceho kódu :
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())
}
}
V súčasnosti sa kontroluje len počet argumentov a číta sa typ i64.
Tieto funkcie môžete pridať podľa potreby, pozri funkcie v qjs_sys začínajúce na JS_To.
Konverzia dátových typov z rustu do js
Konverzia typu sa vykonáva na adrese src/js/val.rs pomocou nasledujúceho kódu :
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()) },
}
}
Pre konverziu z None, (), i64a CString na js sú definované len štyri typy. Môžete ich pridať ľubovoľný počet.
Ďalšie dátové typy je možné deklarovať vo funkciách v qjs_sys počnúc JS_New.
Vývojové prostredie
Vyvíjam na notebooku Apple, rust používa 1.62.0-nightly.
Najprv nainštalujte direnv, prejdite do adresára a direnv allow na chvíľu
Nainštalujte python3, potom pip3 install -r ./requirements.txt
spustiť ./build.xsh na kompiláciu a spustenie ukážky
V predvolenom nastavení sa klonuje oficiálny repozitár quickjs, ak chcete upraviť quickjs v repozitári Kraken, najprv
git clone --recursive git@github.com:openkraken/kraken.git --depth=1
potom vykonajte nasledujúce kroky
rm -rf quickjs
ln -s ../kraken/bridge/third_party/quickjs .
Nakoniec znovu spustite ./build.xsh
Štruktúra adresára
./quickjs_rust
Úprava c-súboru kódu quickjs./quickjs_ffi
Export funkcií zo záhlavného súboruquickjsdorust./rust
Na implementáciu funkcií na stránkequickjspoužite stránkurust../rust/src/qjs.rs
Implementácia asynchrónnych volaní. Keďžequickjsje jednovláknový, volania funkcií zahŕňajúcequckjssa zapisujú v hlavnom vlákne.
./rust_macrorustImplementácia makra postupu#[js]V budúcnosti si pozrite wasmedge-quickjs pre automatický export rust funkcií do js funkcií. wasmedge-quickjs → JsFunctionTrampoline
Stavebné skripty build.xsh
Bez ďalších slov prejdime priamo k zdrojovému kódu zostavovacieho skriptu build.xsh
#!/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
Vysvetlenie princípu
quickjs_rust/patch.py
Spustením stránky ./quickjs_rust/patch.py sa v zdrojovom kóde stránky quickjs vykoná niekoľko menších zmien.
Jedna z funkcií JS_AddRust sa používa na injektovanie do modulu rust.
rust_run sa vkladá do stránky JS_ExecutePendingJob na volanie asynchrónnych funkcií.
Snímka obrazovky so všetkými zmenami je uvedená nižšie :

quickjs_rust.h
Z uvedených zmien vidíte, že sme zaviedli nový hlavičkový súbor quickjs_rust.h s nasledujúcim kódom
#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
Vidíte, že quickjs_rust/quickjs_rust.h zavádza quickjs_rust/js_rust_funcs.h, ktorý je automaticky generovaný zo súboru hlavičky exportnej funkcie rust rust/rust.h a nemal by sa ručne upravovať.
A stránka rust/rust.h sa generuje volaním cbindgen zo stránky ./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
Poznámky k vývoju
quickjs_ffi
Kód z quijine/main/quijine_core/src/ffi.rs
s malými úpravami, nahradením
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::*;
na
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"
Upraviť ". /rust/Cargo.toml' takto, pričom sa zachová len staticlib
[lib]
#crate-type = ["lib", "cdylib", "staticlib"]
crate-type = ["staticlib"]
Odkazy
- Od motora
JSk runtimeJS(hore ) (dole) - Vývoj natívneho modulu pre
QuickJSv jazyku C - Použitie Rustu na implementáciu rozhrania JS API
- Príklady QuickJS
- rust-bindgen
- Ako vytvoriť asynchrónny kód pre
QuickJS - rquickjs → JS_NewPromiseCapability
- wasmedge-quickjs → new_promise
- wasmedge-quickjs → JsMethod
- wasmedge-quickjs → volanie
- Nepostrehnuteľná pasca - zámky Rust
O stránke
Tento projekt je súčasťou projektu kódu rmw.link ( rmw.link ).