DelSkayn / rquickjs

High level bindings to the quickjs javascript engine
MIT License
504 stars 63 forks source link

Issue defining Native Modules #159

Closed DraftedDev closed 1 year ago

DraftedDev commented 1 year ago

Hi, I'm sorry for bothering you again, but I got an issue while trying to "register" a native module. I want to add the native module impl (not the macro one) to the module loader and the resolver, however with the code I've written, quickjs throws "Module "console" not found".

This is my code:

use std::path::{Path, PathBuf};
use log::debug;
use rquickjs::{Context, Runtime, Value};
use rquickjs::context::EvalOptions;
use rquickjs::loader::{BuiltinLoader, BuiltinResolver, FileResolver, ModuleLoader, NativeLoader, ScriptLoader};
use rquickjs::prelude::Func;
use crate::config::AeConfig;
use crate::debug;
use crate::debug::error::better_panic;
use crate::types::VERSION;

#[derive(Debug)]
pub struct AeRuntime {
    config: AeConfig, // just a { strict, global, std } struct, ignore
    loader: ModuleLoader,
    resolver: BuiltinResolver,
    paths: Vec<String>,
}

impl AeRuntime {
    pub fn new(config: AeConfig) -> Self {
        Self { config, loader: ModuleLoader::default(), resolver: BuiltinResolver::default(), paths: vec![] }
    }

    pub fn add_std(&mut self) {
        self.loader.add_module("console", aerow_lib::console::Console); // add to loader
        self.resolver.add_module("../../aerow_lib/src/console"); // add to resolver (no idea if I even need this)
    }

    pub fn run(mut self, script: &Path) {
        let config = self.config;
        if config.std { self.add_std(); }

        let runtime = Runtime::new().unwrap();

        runtime.set_loader( // set loader + resolver
            (
                FileResolver::default()
                    .with_native()
                    .with_paths(self.paths),
            ),
            (
                BuiltinLoader::default(),
                NativeLoader::default(),
                ScriptLoader::default(),
                self.loader,
            ));

        let context = Context::builder()
            .with::<rquickjs::context::intrinsic::All>() // use all ctx features
            .build(&runtime)
            .expect("Failed to create context");

        context.with(|ctx| {
            let globals = ctx.globals();

            { debug!("Setting Globals"); // some constants and debug fns
                globals.set("println", Func::new("println", |msg: String| {
                    println!("{}", msg);
                })).expect("Failed setting global (println)");

                globals.set("print", Func::new("print", |msg: String| {
                    print!("{}", msg);
                })).expect("Failed setting global (print)");

                globals.set("AEROW_VERSION", rquickjs::String::from_str(ctx, VERSION))
                    .expect("Failed setting global (AEROW_VERSION)");
            }

            ctx.eval_file_with_options(script, EvalOptions {
                global: true,
                strict: true,
                backtrace_barrier: false,
            }).unwrap_or_else(|err| {
                better_panic(&err, &ctx.catch()); // just unwraps the quickjs error and outputs engine errors
            });
        });
    }
}

My JS code:

console.log()

My example.rs code:

fn main() {
    AeRuntime::new(AeConfig::default().with_filter(LevelFilter::Debug))
        .run(Path::new("scripts/script.js"));
}

My console module:

// just simple console with empty log() function
pub struct Console;

impl ModuleDef for Console {
    fn declare(declare: &mut Declarations) -> rquickjs::Result<()> {
        declare.declare("log")?;
        Ok(())
    }

    fn evaluate<'js>(_ctx: Ctx<'js>, exports: &mut Exports<'js>) -> rquickjs::Result<()> {
        exports.export("log", Func::new("log", || {
            println!("is it working?");
        }))?;
        Ok(())
    }
}

Thanks for your help :D

DelSkayn commented 1 year ago

Hi!

I noticed a bunch of problems in your code. First, in order to load the module console it also needs to be resolved. So you need to call add_module on the builtin resolver with console.

Second, you declare the resolver but you don't use it in Runtime::set_loader. Third, console is a module so it isn't defined on the global object but must be imported, like so:

import * as console from "console";

console.log();

In order to import modules the javascript must also be a module, so instead of calling Ctx::eval and the like, use Module::evaluate.

Here is the code I ended up with experimenting with yours, this seems to work with the above javascript.

use rquickjs::context::EvalOptions;
use rquickjs::loader::{
    BuiltinLoader, BuiltinResolver, FileResolver, ModuleLoader, NativeLoader, ScriptLoader,
};
use rquickjs::module::ModuleDef;
use rquickjs::prelude::Func;
use rquickjs::{CatchResultExt, Context, Module, Runtime, Value};
use std::path::{Path, PathBuf};

mod console;

#[derive(Debug)]
pub struct AeRuntime {
    //config: AeConfig, // just a { strict, global, std } struct, ignore
    loader: ModuleLoader,
    resolver: BuiltinResolver,
    paths: Vec<String>,
}

impl AeRuntime {
    pub fn new() -> Self {
        Self {
            loader: ModuleLoader::default(),
            resolver: BuiltinResolver::default(),
            paths: vec![],
        }
    }

    pub fn add_std(&mut self) {
        self.loader.add_module("console", console::Console); // add to loader
        // Add console to be resolved.
        self.resolver.add_module("console");
    }

    pub fn run(mut self, script: &Path) {
        let runtime = Runtime::new().unwrap();

        self.add_std();

        runtime.set_loader(
            // set loader + resolver
            (
                FileResolver::default().with_native().with_paths(self.paths),
                // Add resolver as part of the loader.
                self.resolver,
            ),
            (
                BuiltinLoader::default(),
                NativeLoader::default(),
                ScriptLoader::default(),
                self.loader,
            ),
        );

        let context = Context::full(&runtime).expect("Failed to create context");

        let script = std::fs::read_to_string(script).unwrap();

        context.with(|ctx| {
            let globals = ctx.globals();

            // Evaluate the javascript as a module..
            Module::evaluate(ctx, "main", script)
                .map(|_| ())
                .catch(ctx)
                .unwrap();
        });
    }
}
DraftedDev commented 1 year ago

WOW! That helped 👍 Thanks for your help and sorry for the stupid issue.