Created
November 27, 2020 14:05
-
-
Save caiorss/1abdd1d36cd3e973cd1f11f5c20ef7eb to your computer and use it in GitHub Desktop.
QuickJS engine sample project - shows how to embedded in C++
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
cmake_minimum_required(VERSION 3.9) | |
project(QuickJS-Experiment) | |
#========== Global Configurations =============# | |
#----------------------------------------------# | |
set( CMAKE_CXX_STANDARD 17 ) | |
set( CMAKE_VERBOSE_MAKEFILE ON ) | |
set( CMAKE_CXX_EXTENSIONS OFF) | |
# ------------ Download CPM CMake Script ----------------# | |
## Automatically donwload and use module CPM.cmake | |
file(DOWNLOAD https://raw.githubusercontent.com/TheLartians/CPM.cmake/v0.26.2/cmake/CPM.cmake | |
"${CMAKE_BINARY_DIR}/CPM.cmake") | |
include("${CMAKE_BINARY_DIR}/CPM.cmake") | |
#----------- Add dependencies --------------------------# | |
CPMAddPackage( | |
NAME quickjs | |
GITHUB_REPOSITORY bellard/quickjs | |
GIT_TAG 204682fb87ab9312f0cf81f959ecd181180457bc | |
# DOWNLOAD_ONLY YES | |
) | |
# Add this directory where is this file (CMakeLists.txt) to include path. | |
include_directories( ${CMAKE_CURRENT_LIST_DIR} ) | |
# =============== QuickJS settings ====================================# | |
include_directories( ${quickjs_SOURCE_DIR}/ ) | |
message([TRACE] " quickjs source = ${quickjs_SOURCE_DIR} ") | |
file(GLOB quickjs_hpp ${quickjs_SOURCE_DIR}/*.h ) | |
file(GLOB quickjs_src ${quickjs_SOURCE_DIR}/quickjs.c | |
${quickjs_SOURCE_DIR}/libregexp.c | |
${quickjs_SOURCE_DIR}/libunicode.c | |
${quickjs_SOURCE_DIR}/cutils.c | |
${quickjs_SOURCE_DIR}/quickjs-libc.c | |
${quickjs_SOURCE_DIR}/libbf.c | |
) | |
add_library( qjs-engine ${quickjs_src} ${quickjs_hpp} ) | |
target_compile_options( qjs-engine PRIVATE | |
-MMD -MF | |
-Wno-sign-compare | |
-Wno-missing-field-initializers | |
-Wundef -Wuninitialized | |
-Wundef -Wuninitialized -Wwrite-strings -Wchar-subscripts | |
) | |
target_compile_definitions( qjs-engine PUBLIC | |
CONFIG_BIGNUM=y | |
CONFIG_VERSION="2020-11-08" | |
_GNU_SOURCE | |
) | |
if(UNIX) | |
target_link_libraries( qjs-engine PRIVATE m pthread dl) | |
endif() | |
# =========== Target Settings =========================================# | |
# QuickJS compiler. | |
add_executable( qjsc ${quickjs_SOURCE_DIR}/qjsc.c ) | |
target_compile_definitions( qjsc PUBLIC CONFIG_BIGNUM=y CONFIG_VERSION="2020-11-08" _GNU_SOURCE ) | |
target_link_libraries( qjsc qjs-engine ) | |
# Sample application that embeds the quickJS Javascript engine. | |
add_executable( main main.cpp ) | |
target_link_libraries( main qjs-engine ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <iostream> | |
#include <quickjspp.hpp> | |
class ChartXY | |
{ | |
private: | |
double x = 0.0, y = 0.0; | |
double width = 100.0, height = 100.0; | |
public: | |
ChartXY() | |
{ } | |
ChartXY(double w, double h): width(w), height(h) | |
{ } | |
void show() const | |
{ | |
std::cout << " [ĆhartXY Object] x = " << x << " ; y = " << y | |
<< " ; width = " << width << " height = " << height | |
<< '\n'; | |
} | |
void set_width(double width) | |
{ | |
this->width = width; | |
std::fprintf(stdout, " [ChartXY] Width set to %f \n", width); | |
} | |
void set_height(double height) | |
{ | |
this->height = height; | |
std::fprintf(stdout, " [ChartXY] Height set to %f \n", height); | |
} | |
double get_height() const { return this->height; } | |
double get_width () const { return this->width; } | |
void plot_points(std::vector<double> const& points) | |
{ | |
std::cout << " [ChartXY] Plotting points =>> "; | |
for(auto p : points) { std::cout << " " << p; } | |
std::cout << "\n"; | |
} | |
}; | |
qjs::Value | |
try_eval_module( | |
qjs::Context& context | |
, qjs::Runtime& runtime | |
, std::string const& code) | |
{ | |
try | |
{ | |
return context.eval(code, "<eval>", JS_EVAL_TYPE_MODULE); | |
} catch( const qjs::exception& ex) | |
{ | |
//js_std_dump_error(ctx); | |
auto exc = context.getException(); | |
std::cerr << (exc.isError() ? "Error: " : "Throw: ") << (std::string)exc << std::endl; | |
if((bool)exc["stack"]) | |
std::cerr << (std::string)exc["stack"] << std::endl; | |
js_std_free_handlers(runtime.rt); | |
return context.newObject(); | |
} | |
} | |
int main(int argc, char** argv) | |
{ | |
std::cout << " [INFO] Started Ok" << std::endl; | |
using namespace qjs; | |
Runtime runtime; | |
//JSRuntime* rt = runtime.rt; | |
Context context(runtime); | |
//JSContext* ctx = context.ctx; | |
js_std_init_handlers(runtime.rt); | |
/* loader for ES6 modules */ | |
JS_SetModuleLoaderFunc(runtime.rt, nullptr, js_module_loader, nullptr); | |
js_std_add_helpers(context.ctx, argc - 1, argv + 1); | |
/* system modules */ | |
js_init_module_std(context.ctx, "std"); | |
js_init_module_os(context.ctx, "os"); | |
std::fprintf(stderr, " [TRACE] Before loading code. \n"); | |
const char* str = R"( | |
/* | |
import * as std from 'std'; | |
import * as os from 'os'; | |
globalThis.std = std; | |
globalThis.os = os; | |
*/ | |
console.log(" [QUICJS] => =>> Script loaded. Ok. \n"); | |
for(n = 1; n <= 5; n++){ | |
console.log(` [QUICKJS-TRACE] n = ${n}/5 `); | |
} | |
// ----- Define user variables here ---- | |
asset_path = "/Users/mydir-macosx/data/blackjack.txt"; | |
game_score = 0.25156; | |
let x = 10.352; | |
datapoints = [ 0.251, 19.2363, 9.262, 100.125 ]; | |
console.log(`\n [QUICKJS] asset_path = ${asset_path}` ); | |
console.log(` [QUICKJS] score = ${100.0 * game_score} (in percent) \n`); | |
console.log(` [QUICKJS] data points = ${datapoints} `) | |
)"; | |
try | |
{ | |
context.eval(str); //, "", JS_EVAL_TYPE_MODULE); | |
} catch( const qjs::exception& ex) | |
{ | |
//js_std_dump_error(ctx); | |
auto exc = context.getException(); | |
std::cerr << (exc.isError() ? "Error: " : "Throw: ") << (std::string)exc << std::endl; | |
if((bool)exc["stack"]) | |
std::cerr << (std::string)exc["stack"] << std::endl; | |
js_std_free_handlers(runtime.rt); | |
return 1; | |
} | |
std::fprintf(stderr, " [TRACE] After loading code. \n"); | |
int number = (int) context.eval(" 10 * (3 + 1 + 10 ) - 1000 * 2"); | |
std::cout << " [RESULT] number = " << number << '\n'; | |
std::puts("\n [*] ===== Read configuration variables defined in the js code. ====\n"); | |
{ | |
auto var_asset_path = context.global()["asset_path"].as<std::string>(); | |
std::cout << " =>> asset_path = " << var_asset_path << '\n'; | |
auto score = context.global()["game_score"].as<double>(); | |
std::cout << " =>> game_score (%) = " << 100.0 * score << '\n'; | |
auto points = context.global()["datapoints"].as<std::vector<double>>(); | |
std::cout << " ==>> datapoints = [" << points.size() << "]( "; | |
for(auto p : points) { std::cout << p << ' '; } | |
std::cout << " ) \n"; | |
} | |
std::puts("\n [*] ===== Define variables in C++-side ====\n"); | |
{ | |
context.global()["user_name"] = context.newValue("Gaius Julius Caesar"); | |
context.global()["user_points"] = context.newValue(101235); | |
auto data = std::vector<std::string>{ "ADA", "RUST", "C++11", "C++17", "C++20" | |
, "Dlang", "OCaml", "C#(Csharp)" }; | |
context.global()["user_data"] = context.newValue(data); | |
// Note: This code should be within an exception handler. | |
context.eval(R"( | |
console.log(` [STEP 2] user_name = ${user_name} ; points = ${user_points} `); | |
console.log(` [STEP 2] user_data = ${user_data} ; type = ${ typeof(user_data) } `); | |
console.log(` [STEP 2] user_data[5] = ${ user_data[5] } `) | |
// Iterate over the array | |
for(let x in user_data){ console.log(user_data[x]); } | |
)"); | |
} | |
std::puts("\n [*] ===== Register class ChartXY ====\n"); | |
auto& module = context.addModule("chart"); | |
module.class_<ChartXY>("ChartXY") | |
.constructor() | |
.constructor<double, double>() | |
.fun<&ChartXY::show>("show") | |
.fun<&ChartXY::set_height>("set_height") | |
.fun<&ChartXY::set_width>("set_width") | |
.fun<&ChartXY::plot_points>("plot_points") | |
.property<&ChartXY::get_width, &ChartXY::set_width>("width") | |
.property<&ChartXY::get_height, &ChartXY::set_height>("height") | |
; | |
module.add("user_path", "/Users/data/assets/game/score/marks"); | |
module.add("user_points", 1023523); | |
module.function("myfunc", [](double x, double y){ return 4.61 * x + 10 * y * y; }); | |
const char* module_code = R"( | |
import { ChartXY } from "chart"; | |
import * as chart from "chart" | |
console.log(` [SCRIPT] chart.user_path = ${chart.user_path} \n\n`); | |
console.log(` [SCRIPT] chart.user_points = ${chart.user_points} \n\n`); | |
console.log(` [SCRIPT] Result = ${ chart.myfunc(5.61, 9.821) } \n`); | |
let ch = new ChartXY(200, 600); | |
ch.show(); | |
ch.set_width(800.0); | |
ch.set_height(700.0) | |
ch.show(); | |
console.log(" [QUICKJS] Change chart dimensions using properties "); | |
ch.width = 500; | |
ch.height = 660; | |
console.log(`\n <QUICKJS> Chart width = ${ch.width} ; Chart height = ${ch.height} \n`); | |
ch.plot_points( [ 10.522, 8.261, -100.24, 7.2532, 56.123, 89.23 ] ); | |
)"; | |
try_eval_module(context, runtime, module_code); | |
js_std_loop(context.ctx); | |
// ----- Shutdown virtual machine ---------------// | |
js_std_free_handlers(runtime.rt); | |
return 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Original project (autorship): https://github.com/ftk/quickjspp (Credits goes to this files) | |
* | |
* "QuickJSPP is header-only - put quickjspp.hpp into your include search path. | |
* Compiler that supports C++17 or later is required. The program needs to be linked against | |
* QuickJS. Sample CMake project files are provided. | |
* | |
* Original file URL: https://raw.githubusercontent.com/ftk/quickjspp/fd9e30676cf18e6c22a26d6731516fe96d757c28/quickjspp.hpp | |
*/ | |
#pragma once | |
#include <quickjs.h> | |
#include <quickjs-libc.h> | |
#include <vector> | |
#include <string_view> | |
#include <string> | |
#include <cassert> | |
#include <memory> | |
#include <cstddef> | |
#include <algorithm> | |
#include <tuple> | |
#include <functional> | |
#include <stdexcept> | |
namespace qjs { | |
/** Exception type. | |
* Indicates that exception has occured in JS context. | |
*/ | |
class exception {}; | |
/** Javascript conversion traits. | |
* Describes how to convert type R to/from JSValue. Second template argument can be used for SFINAE/enable_if type filters. | |
*/ | |
template <typename R, typename /*_SFINAE*/ = void> | |
struct js_traits | |
{ | |
/** Create an object of C++ type R given JSValue v and JSContext. | |
* This function is intentionally not implemented. User should implement this function for their own type. | |
* @param v This value is passed as JSValueConst so it should be freed by the caller. | |
* @throws exception in case of conversion error | |
*/ | |
static R unwrap(JSContext * ctx, JSValueConst v); | |
/** Create JSValue from an object of type R and JSContext. | |
* This function is intentionally not implemented. User should implement this function for their own type. | |
* @return Returns JSValue which should be freed by the caller or JS_EXCEPTION in case of error. | |
*/ | |
static JSValue wrap(JSContext * ctx, R value); | |
}; | |
/** Conversion traits for JSValue (identity). | |
*/ | |
template <> | |
struct js_traits<JSValue> | |
{ | |
static JSValue unwrap(JSContext * ctx, JSValueConst v) noexcept | |
{ | |
return JS_DupValue(ctx, v); | |
} | |
static JSValue wrap(JSContext * ctx, JSValue v) noexcept | |
{ | |
return v; | |
} | |
}; | |
/** Conversion traits for integers. | |
*/ | |
template <typename Int> | |
struct js_traits<Int, std::enable_if_t<std::is_integral_v<Int> && sizeof(Int) <= sizeof(int64_t)>> | |
{ | |
/// @throws exception | |
static Int unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
if constexpr (sizeof(Int) > sizeof(int32_t)) | |
{ | |
int64_t r; | |
if(JS_ToInt64(ctx, &r, v)) | |
throw exception{}; | |
return static_cast<Int>(r); | |
} | |
else | |
{ | |
int32_t r; | |
if(JS_ToInt32(ctx, &r, v)) | |
throw exception{}; | |
return static_cast<Int>(r); | |
} | |
} | |
static JSValue wrap(JSContext * ctx, Int i) noexcept | |
{ | |
if constexpr (std::is_same_v<Int, uint32_t> || sizeof(Int) > sizeof(int32_t)) | |
return JS_NewInt64(ctx, static_cast<Int>(i)); | |
else | |
return JS_NewInt32(ctx, static_cast<Int>(i)); | |
} | |
}; | |
/** Conversion traits for boolean. | |
*/ | |
template <> | |
struct js_traits<bool> | |
{ | |
static bool unwrap(JSContext * ctx, JSValueConst v) noexcept | |
{ | |
return JS_ToBool(ctx, v); | |
} | |
static JSValue wrap(JSContext * ctx, bool i) noexcept | |
{ | |
return JS_NewBool(ctx, i); | |
} | |
}; | |
/** Conversion trait for void. | |
*/ | |
template <> | |
struct js_traits<void> | |
{ | |
/// @throws exception if jsvalue is neither undefined nor null | |
static void unwrap(JSContext * ctx, JSValueConst value) | |
{ | |
if(JS_IsException(value)) | |
throw exception{}; | |
} | |
}; | |
/** Conversion traits for float64/double. | |
*/ | |
template <> | |
struct js_traits<double> | |
{ | |
/// @throws exception | |
static double unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
double r; | |
if(JS_ToFloat64(ctx, &r, v)) | |
throw exception{}; | |
return r; | |
} | |
static JSValue wrap(JSContext * ctx, double i) noexcept | |
{ | |
return JS_NewFloat64(ctx, i); | |
} | |
}; | |
namespace detail { | |
/** Fake std::string_view which frees the string on destruction. | |
*/ | |
class js_string : public std::string_view | |
{ | |
using Base = std::string_view; | |
JSContext * ctx = nullptr; | |
friend struct js_traits<std::string_view>; | |
js_string(JSContext * ctx, const char * ptr, std::size_t len) : Base(ptr, len), ctx(ctx) | |
{} | |
public: | |
template <typename... Args> | |
js_string(Args&& ... args) : Base(std::forward<Args>(args)...), ctx(nullptr) | |
{} | |
js_string(const js_string& other) = delete; | |
operator const char * () const { | |
return this->data(); | |
} | |
~js_string() | |
{ | |
if(ctx) | |
JS_FreeCString(ctx, this->data()); | |
} | |
}; | |
} // namespace detail | |
/** Conversion traits from std::string_view and to detail::js_string. */ | |
template <> | |
struct js_traits<std::string_view> | |
{ | |
static detail::js_string unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
size_t plen; | |
const char * ptr = JS_ToCStringLen(ctx, &plen, v); | |
if(!ptr) | |
throw exception{}; | |
return detail::js_string{ctx, ptr, plen}; | |
} | |
static JSValue wrap(JSContext * ctx, std::string_view str) noexcept | |
{ | |
return JS_NewStringLen(ctx, str.data(), str.size()); | |
} | |
}; | |
/** Conversion traits for std::string */ | |
template <> // slower | |
struct js_traits<std::string> | |
{ | |
static std::string unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
auto str_view = js_traits<std::string_view>::unwrap(ctx, v); | |
return std::string{str_view.data(), str_view.size()}; | |
} | |
static JSValue wrap(JSContext * ctx, const std::string& str) noexcept | |
{ | |
return JS_NewStringLen(ctx, str.data(), str.size()); | |
} | |
}; | |
/** Conversion from const char * */ | |
template <> | |
struct js_traits<const char *> | |
{ | |
static JSValue wrap(JSContext * ctx, const char * str) noexcept | |
{ | |
return JS_NewString(ctx, str); | |
} | |
static detail::js_string unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
return js_traits<std::string_view>::unwrap(ctx, v); | |
} | |
}; | |
namespace detail { | |
/** Helper function to convert and then free JSValue. */ | |
template <typename T> | |
T unwrap_free(JSContext * ctx, JSValue val) | |
{ | |
if constexpr(std::is_same_v<T, void>) | |
{ | |
JS_FreeValue(ctx, val); | |
return js_traits<T>::unwrap(ctx, val); | |
} else | |
{ | |
try | |
{ | |
T result = js_traits<std::decay_t<T>>::unwrap(ctx, val); | |
JS_FreeValue(ctx, val); | |
return result; | |
} | |
catch(...) | |
{ | |
JS_FreeValue(ctx, val); | |
throw; | |
} | |
} | |
} | |
template <class Tuple, std::size_t... I> | |
Tuple unwrap_args_impl(JSContext * ctx, JSValueConst * argv, std::index_sequence<I...>) | |
{ | |
return Tuple{js_traits<std::decay_t<std::tuple_element_t<I, Tuple>>>::unwrap(ctx, argv[I])...}; | |
} | |
/** Helper function to convert an array of JSValues to a tuple. | |
* @tparam Args C++ types of the argv array | |
*/ | |
template <typename... Args> | |
std::tuple<std::decay_t<Args>...> unwrap_args(JSContext * ctx, JSValueConst * argv) | |
{ | |
return unwrap_args_impl<std::tuple<std::decay_t<Args>...>>(ctx, argv, std::make_index_sequence<sizeof...(Args)>()); | |
} | |
/** Helper function to call f with an array of JSValues. | |
* @tparam R return type of f | |
* @tparam Args argument types of f | |
* @tparam Callable type of f (inferred) | |
* @param ctx JSContext | |
* @param f callable object | |
* @param argv array of JSValue's | |
* @return converted return value of f or JS_NULL if f returns void | |
*/ | |
template <typename R, typename... Args, typename Callable> | |
JSValue wrap_call(JSContext * ctx, Callable&& f, JSValueConst * argv) noexcept | |
{ | |
try | |
{ | |
if constexpr(std::is_same_v<R, void>) | |
{ | |
std::apply(std::forward<Callable>(f), unwrap_args<Args...>(ctx, argv)); | |
return JS_NULL; | |
} else | |
{ | |
return js_traits<std::decay_t<R>>::wrap(ctx, | |
std::apply(std::forward<Callable>(f), | |
unwrap_args<Args...>(ctx, argv))); | |
} | |
} | |
catch(exception) | |
{ | |
return JS_EXCEPTION; | |
} | |
} | |
/** Same as wrap_call, but pass this_value as first argument. | |
* @tparam FirstArg type of this_value | |
*/ | |
template <typename R, typename FirstArg, typename... Args, typename Callable> | |
JSValue wrap_this_call(JSContext * ctx, Callable&& f, JSValueConst this_value, JSValueConst * argv) noexcept | |
{ | |
try | |
{ | |
if constexpr(std::is_same_v<R, void>) | |
{ | |
std::apply(std::forward<Callable>(f), std::tuple_cat(unwrap_args<FirstArg>(ctx, &this_value), | |
unwrap_args<Args...>(ctx, argv))); | |
return JS_NULL; | |
} else | |
{ | |
return js_traits<std::decay_t<R>>::wrap(ctx, | |
std::apply(std::forward<Callable>(f), | |
std::tuple_cat( | |
unwrap_args<FirstArg>(ctx, &this_value), | |
unwrap_args<Args...>(ctx, argv)))); | |
} | |
} | |
catch(exception) | |
{ | |
return JS_EXCEPTION; | |
} | |
} | |
template <class Tuple, std::size_t... I> | |
void wrap_args_impl(JSContext * ctx, JSValue * argv, Tuple tuple, std::index_sequence<I...>) | |
{ | |
((argv[I] = js_traits<std::decay_t<std::tuple_element_t<I, Tuple>>>::wrap(ctx, std::get<I>(tuple))), ...); | |
} | |
/** Converts C++ args to JSValue array. | |
* @tparam Args argument types | |
* @param argv array of size at least sizeof...(Args) | |
*/ | |
template <typename... Args> | |
void wrap_args(JSContext * ctx, JSValue * argv, Args&& ... args) | |
{ | |
wrap_args_impl(ctx, argv, std::make_tuple(std::forward<Args>(args)...), | |
std::make_index_sequence<sizeof...(Args)>()); | |
} | |
} // namespace detail | |
/** A wrapper type for free and class member functions. | |
* Pointer to function F is a template argument. | |
* @tparam F either a pointer to free function or a pointer to class member function | |
* @tparam PassThis if true and F is a pointer to free function, passes Javascript "this" value as first argument: | |
*/ | |
template <auto F, bool PassThis = false /* pass this as the first argument */> | |
struct fwrapper | |
{ | |
/// "name" property of the JS function object (not defined if nullptr) | |
const char * name = nullptr; | |
}; | |
/** Conversion to JSValue for free function in fwrapper. */ | |
template <typename R, typename... Args, R (* F)(Args...), bool PassThis> | |
struct js_traits<fwrapper<F, PassThis>> | |
{ | |
static JSValue wrap(JSContext * ctx, fwrapper<F, PassThis> fw) noexcept | |
{ | |
return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, | |
JSValueConst * argv) noexcept -> JSValue { | |
if constexpr(PassThis) | |
return detail::wrap_this_call<R, Args...>(ctx, F, this_value, argv); | |
else | |
return detail::wrap_call<R, Args...>(ctx, F, argv); | |
}, fw.name, sizeof...(Args)); | |
} | |
}; | |
/** Conversion to JSValue for class member function in fwrapper. PassThis is ignored and treated as true */ | |
template <typename R, class T, typename... Args, R (T::*F)(Args...), bool PassThis/*=ignored*/> | |
struct js_traits<fwrapper<F, PassThis>> | |
{ | |
static JSValue wrap(JSContext * ctx, fwrapper<F, PassThis> fw) noexcept | |
{ | |
return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, | |
JSValueConst * argv) noexcept -> JSValue { | |
return detail::wrap_this_call<R, std::shared_ptr<T>, Args...>(ctx, F, this_value, argv); | |
}, fw.name, sizeof...(Args)); | |
} | |
}; | |
/** Conversion to JSValue for const class member function in fwrapper. PassThis is ignored and treated as true */ | |
template <typename R, class T, typename... Args, R (T::*F)(Args...) const, bool PassThis/*=ignored*/> | |
struct js_traits<fwrapper<F, PassThis>> | |
{ | |
static JSValue wrap(JSContext * ctx, fwrapper<F, PassThis> fw) noexcept | |
{ | |
return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, | |
JSValueConst * argv) noexcept -> JSValue { | |
return detail::wrap_this_call<R, std::shared_ptr<T>, Args...>(ctx, F, this_value, argv); | |
}, fw.name, sizeof...(Args)); | |
} | |
}; | |
/** A wrapper type for constructor of type T with arguments Args. | |
* Compilation fails if no such constructor is defined. | |
* @tparam Args constructor arguments | |
*/ | |
template <class T, typename... Args> | |
struct ctor_wrapper | |
{ | |
static_assert(std::is_constructible<T, Args...>::value, "no such constructor!"); | |
/// "name" property of JS constructor object | |
const char * name = nullptr; | |
}; | |
/** Conversion to JSValue for ctor_wrapper. */ | |
template <class T, typename... Args> | |
struct js_traits<ctor_wrapper<T, Args...>> | |
{ | |
static JSValue wrap(JSContext * ctx, ctor_wrapper<T, Args...> cw) noexcept | |
{ | |
return JS_NewCFunction2(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, | |
JSValueConst * argv) noexcept -> JSValue { | |
if(js_traits<std::shared_ptr<T>>::QJSClassId == 0) // not registered | |
{ | |
#if defined(__cpp_rtti) | |
// automatically register class on first use (no prototype) | |
js_traits<std::shared_ptr<T>>::register_class(ctx, typeid(T).name()); | |
#else | |
JS_ThrowTypeError(ctx, "quickjspp ctor_wrapper<T>::wrap: Class is not registered"); | |
return JS_EXCEPTION; | |
#endif | |
} | |
auto proto = JS_GetPropertyStr(ctx, this_value, "prototype"); | |
if (JS_IsException(proto)) | |
return proto; | |
auto jsobj = JS_NewObjectProtoClass(ctx, proto, js_traits<std::shared_ptr<T>>::QJSClassId); | |
JS_FreeValue(ctx, proto); | |
if (JS_IsException(jsobj)) | |
return jsobj; | |
std::shared_ptr<T> ptr = std::apply(std::make_shared<T, Args...>, detail::unwrap_args<Args...>(ctx, argv)); | |
JS_SetOpaque(jsobj, new std::shared_ptr<T>(std::move(ptr))); | |
return jsobj; | |
// return detail::wrap_call<std::shared_ptr<T>, Args...>(ctx, std::make_shared<T, Args...>, argv); | |
}, cw.name, sizeof...(Args), JS_CFUNC_constructor, 0); | |
} | |
}; | |
/** Conversions for std::shared_ptr<T>. | |
* T should be registered to a context before conversions. | |
* @tparam T class type | |
*/ | |
template <class T> | |
struct js_traits<std::shared_ptr<T>> | |
{ | |
/// Registered class id in QuickJS. | |
inline static JSClassID QJSClassId = 0; | |
/** Register class in QuickJS context. | |
* | |
* @param ctx context | |
* @param name class name | |
* @param proto class prototype or JS_NULL | |
* @throws exception | |
*/ | |
static void register_class(JSContext * ctx, const char * name, JSValue proto = JS_NULL) | |
{ | |
if(QJSClassId == 0) | |
{ | |
JS_NewClassID(&QJSClassId); | |
} | |
auto rt = JS_GetRuntime(ctx); | |
if(!JS_IsRegisteredClass(rt, QJSClassId)) | |
{ | |
JSClassDef def{ | |
name, | |
// destructor | |
[](JSRuntime * rt, JSValue obj) noexcept { | |
auto pptr = reinterpret_cast<std::shared_ptr<T> *>(JS_GetOpaque(obj, QJSClassId)); | |
delete pptr; | |
} | |
}; | |
int e = JS_NewClass(rt, QJSClassId, &def); | |
if(e < 0) | |
{ | |
JS_ThrowInternalError(ctx, "Cant register class %s", name); | |
throw exception{}; | |
} | |
} | |
JS_SetClassProto(ctx, QJSClassId, proto); | |
} | |
/** Create a JSValue from std::shared_ptr<T>. | |
* Creates an object with class if #QJSClassId and sets its opaque pointer to a new copy of #ptr. | |
*/ | |
static JSValue wrap(JSContext * ctx, std::shared_ptr<T> ptr) | |
{ | |
if(QJSClassId == 0) // not registered | |
{ | |
#if defined(__cpp_rtti) | |
// automatically register class on first use (no prototype) | |
register_class(ctx, typeid(T).name()); | |
#else | |
JS_ThrowTypeError(ctx, "quickjspp std::shared_ptr<T>::wrap: Class is not registered"); | |
return JS_EXCEPTION; | |
#endif | |
} | |
auto jsobj = JS_NewObjectClass(ctx, QJSClassId); | |
if(JS_IsException(jsobj)) | |
return jsobj; | |
auto pptr = new std::shared_ptr<T>(std::move(ptr)); | |
JS_SetOpaque(jsobj, pptr); | |
return jsobj; | |
} | |
/// @throws exception if #v doesn't have the correct class id | |
static const std::shared_ptr<T>& unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
auto ptr = reinterpret_cast<std::shared_ptr<T> *>(JS_GetOpaque2(ctx, v, QJSClassId)); | |
if(!ptr) | |
throw exception{}; | |
return *ptr; | |
} | |
}; | |
/** Conversions for non-owning pointers to class T. | |
* @tparam T class type | |
*/ | |
template <class T> | |
struct js_traits<T *, std::enable_if_t<std::is_class_v<T>>> | |
{ | |
static JSValue wrap(JSContext * ctx, T * ptr) | |
{ | |
if(js_traits<std::shared_ptr<T>>::QJSClassId == 0) // not registered | |
{ | |
#if defined(__cpp_rtti) | |
js_traits<std::shared_ptr<T>>::register_class(ctx, typeid(T).name()); | |
#else | |
JS_ThrowTypeError(ctx, "quickjspp js_traits<T *>::wrap: Class is not registered"); | |
return JS_EXCEPTION; | |
#endif | |
} | |
auto jsobj = JS_NewObjectClass(ctx, js_traits<std::shared_ptr<T>>::QJSClassId); | |
if(JS_IsException(jsobj)) | |
return jsobj; | |
// shared_ptr with empty deleter since we don't own T* | |
auto pptr = new std::shared_ptr<T>(ptr, [](T *){}); | |
JS_SetOpaque(jsobj, pptr); | |
return jsobj; | |
} | |
static T * unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
auto ptr = reinterpret_cast<std::shared_ptr<T> *>(JS_GetOpaque2(ctx, v, | |
js_traits<std::shared_ptr<T>>::QJSClassId)); | |
if(!ptr) | |
throw exception{}; | |
return ptr->get(); | |
} | |
}; | |
namespace detail { | |
/** A faster std::function-like object with type erasure. | |
* Used to convert any callable objects (including lambdas) to JSValue. | |
*/ | |
struct function | |
{ | |
JSValue | |
(* invoker)(function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) = nullptr; | |
void (* destroyer)(function * self) = nullptr; | |
alignas(std::max_align_t) char functor[]; | |
template <typename Functor> | |
static function * create(JSRuntime * rt, Functor&& f) | |
{ | |
auto fptr = reinterpret_cast<function *>(js_malloc_rt(rt, sizeof(function) + sizeof(Functor))); | |
if(!fptr) | |
throw std::bad_alloc{}; | |
new(fptr) function; | |
auto functorptr = reinterpret_cast<Functor *>(fptr->functor); | |
new(functorptr) Functor(std::forward<Functor>(f)); | |
fptr->destroyer = nullptr; | |
if constexpr(!std::is_trivially_destructible_v<Functor>) | |
{ | |
fptr->destroyer = [](function * fptr) { | |
auto functorptr = reinterpret_cast<Functor *>(fptr->functor); | |
functorptr->~Functor(); | |
}; | |
} | |
return fptr; | |
} | |
}; | |
static_assert(std::is_trivially_destructible_v<function>); | |
} | |
template <> | |
struct js_traits<detail::function> | |
{ | |
inline static JSClassID QJSClassId = 0; | |
// TODO: replace ctx with rt | |
static void register_class(JSContext * ctx, const char * name) | |
{ | |
if(QJSClassId == 0) | |
{ | |
JS_NewClassID(&QJSClassId); | |
} | |
auto rt = JS_GetRuntime(ctx); | |
if(JS_IsRegisteredClass(rt, QJSClassId)) | |
return; | |
JSClassDef def{ | |
name, | |
// destructor | |
[](JSRuntime * rt, JSValue obj) noexcept { | |
auto fptr = reinterpret_cast<detail::function *>(JS_GetOpaque(obj, QJSClassId)); | |
assert(fptr); | |
if(fptr->destroyer) | |
fptr->destroyer(fptr); | |
js_free_rt(rt, fptr); | |
}, | |
nullptr, // mark | |
// call | |
[](JSContext * ctx, JSValueConst func_obj, JSValueConst this_val, int argc, | |
JSValueConst * argv, int flags) -> JSValue { | |
auto ptr = reinterpret_cast<detail::function *>(JS_GetOpaque2(ctx, func_obj, QJSClassId)); | |
if(!ptr) | |
return JS_EXCEPTION; | |
return ptr->invoker(ptr, ctx, this_val, argc, argv); | |
} | |
}; | |
int e = JS_NewClass(rt, QJSClassId, &def); | |
if(e < 0) | |
throw std::runtime_error{"Cannot register C++ function class"}; | |
} | |
}; | |
/** Traits for accessing object properties. | |
* @tparam Key property key type (uint32 and strings are supported) | |
*/ | |
template <typename Key> | |
struct js_property_traits | |
{ | |
static void set_property(JSContext * ctx, JSValue this_obj, Key key, JSValue value); | |
static JSValue get_property(JSContext * ctx, JSValue this_obj, Key key); | |
}; | |
template <> | |
struct js_property_traits<const char *> | |
{ | |
static void set_property(JSContext * ctx, JSValue this_obj, const char * name, JSValue value) | |
{ | |
int err = JS_SetPropertyStr(ctx, this_obj, name, value); | |
if(err < 0) | |
throw exception{}; | |
} | |
static JSValue get_property(JSContext * ctx, JSValue this_obj, const char * name) noexcept | |
{ | |
return JS_GetPropertyStr(ctx, this_obj, name); | |
} | |
}; | |
template <> | |
struct js_property_traits<uint32_t> | |
{ | |
static void set_property(JSContext * ctx, JSValue this_obj, uint32_t idx, JSValue value) | |
{ | |
int err = JS_SetPropertyUint32(ctx, this_obj, idx, value); | |
if(err < 0) | |
throw exception{}; | |
} | |
static JSValue get_property(JSContext * ctx, JSValue this_obj, uint32_t idx) noexcept | |
{ | |
return JS_GetPropertyUint32(ctx, this_obj, idx); | |
} | |
}; | |
class Value; | |
namespace detail { | |
template <typename Key> | |
struct property_proxy | |
{ | |
JSContext * ctx; | |
JSValue this_obj; | |
Key key; | |
/** Conversion helper function */ | |
template <typename T> | |
T as() const | |
{ | |
return unwrap_free<T>(ctx, js_property_traits<Key>::get_property(ctx, this_obj, key)); | |
} | |
/** Explicit conversion operator (to any type) */ | |
template <typename T> | |
explicit operator T() const { return as<T>(); } | |
/** Implicit converion to qjs::Value */ | |
operator Value() const; // defined later due to Value being incomplete type | |
template <typename Value> | |
property_proxy& operator =(Value value) | |
{ | |
js_property_traits<Key>::set_property(ctx, this_obj, key, | |
js_traits<Value>::wrap(ctx, std::move(value))); | |
return *this; | |
} | |
}; | |
// class member variable getter/setter | |
template <auto M> | |
struct get_set {}; | |
template <class T, typename R, R T::*M> | |
struct get_set<M> | |
{ | |
using is_const = std::is_const<R>; | |
static const R& get(const std::shared_ptr<T>& ptr) | |
{ | |
return *ptr.*M; | |
} | |
static R& set(const std::shared_ptr<T>& ptr, R value) | |
{ | |
return *ptr.*M = std::move(value); | |
} | |
}; | |
} // namespace detail | |
/** JSValue with RAAI semantics. | |
* A wrapper over (JSValue v, JSContext * ctx). | |
* Calls JS_FreeValue(ctx, v) on destruction. Can be copied and moved. | |
* A JSValue can be released by either JSValue x = std::move(value); or JSValue x = value.release(), then the Value becomes invalid and FreeValue won't be called | |
* Can be converted to C++ type, for example: auto string = value.as<std::string>(); qjs::exception would be thrown on error | |
* Properties can be accessed (read/write): value["property1"] = 1; value[2] = "2"; | |
*/ | |
class Value | |
{ | |
public: | |
JSValue v; | |
JSContext * ctx = nullptr; | |
public: | |
/** Use context.newValue(val) instead */ | |
template <typename T> | |
Value(JSContext * ctx, T&& val) : ctx(ctx) | |
{ | |
v = js_traits<std::decay_t<T>>::wrap(ctx, std::forward<T>(val)); | |
if(JS_IsException(v)) | |
throw exception{}; | |
} | |
Value(const Value& rhs) | |
{ | |
ctx = rhs.ctx; | |
v = JS_DupValue(ctx, rhs.v); | |
} | |
Value(Value&& rhs) | |
{ | |
std::swap(ctx, rhs.ctx); | |
v = rhs.v; | |
} | |
Value& operator=(Value rhs) | |
{ | |
std::swap(ctx, rhs.ctx); | |
std::swap(v, rhs.v); | |
return *this; | |
} | |
bool operator==(JSValueConst other) const | |
{ | |
return JS_VALUE_GET_TAG(v) == JS_VALUE_GET_TAG(other) && JS_VALUE_GET_PTR(v) == JS_VALUE_GET_PTR(other); | |
} | |
bool operator!=(JSValueConst other) const { return !((*this) == other); } | |
/** Returns true if 2 values are the same (equality for arithmetic types or point to the same object) */ | |
bool operator==(const Value& rhs) const | |
{ | |
return ctx == rhs.ctx && (*this == rhs.v); | |
} | |
bool operator!=(const Value& rhs) const { return !((*this) == rhs); } | |
~Value() | |
{ | |
if(ctx) JS_FreeValue(ctx, v); | |
} | |
bool isError() const { return JS_IsError(ctx, v); } | |
/** Conversion helper function: value.as<T>() | |
* @tparam T type to convert to | |
* @return type returned by js_traits<std::decay_t<T>>::unwrap that should be implicitly convertible to T | |
* */ | |
template <typename T> | |
auto as() const { return js_traits<std::decay_t<T>>::unwrap(ctx, v); } | |
/** Explicit conversion: static_cast<T>(value) or (T)value */ | |
template <typename T> | |
explicit operator T() const { return as<T>(); } | |
JSValue release() // dont call freevalue | |
{ | |
ctx = nullptr; | |
return v; | |
} | |
/** Implicit conversion to JSValue (rvalue only). Example: JSValue v = std::move(value); */ | |
operator JSValue() && { return release(); } | |
/** Access JS properties. Returns proxy type which is implicitly convertible to qjs::Value */ | |
template <typename Key> | |
detail::property_proxy<Key> operator [](Key key) | |
{ | |
return {ctx, v, std::move(key)}; | |
} | |
// add("f", []() {...}); | |
template <typename Function> | |
Value& add(const char * name, Function&& f) | |
{ | |
(*this)[name] = js_traits<decltype(std::function{std::forward<Function>(f)})>::wrap(ctx, | |
std::forward<Function>(f)); | |
return *this; | |
} | |
// add<&f>("f"); | |
// add<&T::f>("f"); | |
template <auto F> | |
std::enable_if_t<!std::is_member_object_pointer_v<decltype(F)>, Value&> | |
add(const char * name) | |
{ | |
(*this)[name] = fwrapper<F>{name}; | |
return *this; | |
} | |
// add_getter_setter<&T::get_member, &T::set_member>("member"); | |
template <auto FGet, auto FSet> | |
Value& add_getter_setter(const char * name) | |
{ | |
auto prop = JS_NewAtom(ctx, name); | |
using fgetter = fwrapper<FGet, true>; | |
using fsetter = fwrapper<FSet, true>; | |
int ret = JS_DefinePropertyGetSet(ctx, v, prop, | |
js_traits<fgetter>::wrap(ctx, fgetter{name}), | |
js_traits<fsetter>::wrap(ctx, fsetter{name}), | |
JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | |
); | |
JS_FreeAtom(ctx, prop); | |
if(ret < 0) | |
throw exception{}; | |
return *this; | |
} | |
// add_getter<&T::get_member>("member"); | |
template <auto FGet> | |
Value& add_getter(const char * name) | |
{ | |
auto prop = JS_NewAtom(ctx, name); | |
using fgetter = fwrapper<FGet, true>; | |
int ret = JS_DefinePropertyGetSet(ctx, v, prop, | |
js_traits<fgetter>::wrap(ctx, fgetter{name}), | |
JS_UNDEFINED, | |
JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE | |
); | |
JS_FreeAtom(ctx, prop); | |
if(ret < 0) | |
throw exception{}; | |
return *this; | |
} | |
// add<&T::member>("member"); | |
template <auto M> | |
std::enable_if_t<std::is_member_object_pointer_v<decltype(M)>, Value&> | |
add(const char * name) | |
{ | |
if constexpr (detail::get_set<M>::is_const::value) | |
{ | |
return add_getter<detail::get_set<M>::get>(name); | |
} | |
else | |
{ | |
return add_getter_setter<detail::get_set<M>::get, detail::get_set<M>::set>(name); | |
} | |
} | |
std::string toJSON(const Value& replacer = Value{nullptr, JS_UNDEFINED}, const Value& space = Value{nullptr, JS_UNDEFINED}) | |
{ | |
assert(ctx); | |
assert(!replacer.ctx || ctx == replacer.ctx); | |
assert(!space.ctx || ctx == space.ctx); | |
JSValue json = JS_JSONStringify(ctx, v, replacer.v, space.v); | |
return (std::string)Value{ctx, json}; | |
} | |
}; | |
/** Thin wrapper over JSRuntime * rt | |
* Calls JS_FreeRuntime on destruction. noncopyable. | |
*/ | |
class Runtime | |
{ | |
public: | |
JSRuntime * rt; | |
Runtime() | |
{ | |
rt = JS_NewRuntime(); | |
if(!rt) | |
throw std::runtime_error{"qjs: Cannot create runtime"}; | |
} | |
// noncopyable | |
Runtime(const Runtime&) = delete; | |
~Runtime() | |
{ | |
JS_FreeRuntime(rt); | |
} | |
}; | |
/** Wrapper over JSContext * ctx | |
* Calls JS_SetContextOpaque(ctx, this); on construction and JS_FreeContext on destruction | |
*/ | |
class Context | |
{ | |
public: | |
JSContext * ctx; | |
/** Module wrapper | |
* Workaround for lack of opaque pointer for module load function by keeping a list of modules in qjs::Context. | |
*/ | |
class Module | |
{ | |
friend class Context; | |
JSModuleDef * m; | |
JSContext * ctx; | |
const char * name; | |
using nvp = std::pair<const char *, Value>; | |
std::vector<nvp> exports; | |
public: | |
Module(JSContext * ctx, const char * name) : ctx(ctx), name(name) | |
{ | |
m = JS_NewCModule(ctx, name, [](JSContext * ctx, JSModuleDef * m) noexcept { | |
auto& context = Context::get(ctx); | |
auto it = std::find_if(context.modules.begin(), context.modules.end(), | |
[m](const Module& module) { return module.m == m; }); | |
if(it == context.modules.end()) | |
return -1; | |
for(const auto& e : it->exports) | |
{ | |
if(JS_SetModuleExport(ctx, m, e.first, JS_DupValue(ctx, e.second.v)) != 0) | |
return -1; | |
} | |
return 0; | |
}); | |
if(!m) | |
throw exception{}; | |
} | |
Module& add(const char * name, JSValue value) | |
{ | |
exports.push_back({name, {ctx, value}}); | |
JS_AddModuleExport(ctx, m, name); | |
return *this; | |
} | |
Module& add(const char * name, Value value) | |
{ | |
assert(value.ctx == ctx); | |
exports.push_back({name, std::move(value)}); | |
JS_AddModuleExport(ctx, m, name); | |
return *this; | |
} | |
template <typename T> | |
Module& add(const char * name, T value) | |
{ | |
return add(name, js_traits<T>::wrap(ctx, std::move(value))); | |
} | |
Module(const Module&) = delete; | |
Module(Module&&) = default; | |
//Module& operator=(Module&&) = default; | |
// function wrappers | |
/** Add free function F. | |
* Example: | |
* module.function<static_cast<double (*)(double)>(&::sin)>("sin"); | |
*/ | |
template <auto F> | |
Module& function(const char * name) | |
{ | |
return add(name, qjs::fwrapper<F>{name}); | |
} | |
/** Add function object f. | |
* Slower than template version. | |
* Example: module.function("sin", [](double x) { return ::sin(x); }); | |
*/ | |
template <typename F> | |
Module& function(const char * name, F&& f) | |
{ | |
return add(name, js_traits<decltype(std::function{std::forward<F>(f)})>::wrap(ctx, std::forward<F>(f))); | |
} | |
// class register wrapper | |
private: | |
/** Helper class to register class members and constructors. | |
* See fun, constructor. | |
* Actual registration occurs at object destruction. | |
*/ | |
template <class T> | |
class class_registrar | |
{ | |
const char * name; | |
qjs::Value prototype; | |
qjs::Context::Module& module; | |
qjs::Context& context; | |
public: | |
explicit class_registrar(const char * name, qjs::Context::Module& module, qjs::Context& context) : | |
name(name), | |
prototype(context.newObject()), | |
module(module), | |
context(context) | |
{ | |
} | |
class_registrar(const class_registrar&) = delete; | |
/** Add functional object f | |
*/ | |
template <typename F> | |
class_registrar& fun(const char * name, F&& f) | |
{ | |
prototype.add(name, std::forward<F>(f)); | |
return *this; | |
} | |
/** Add class member function or class member variable F | |
* Example: | |
* struct T { int var; int func(); } | |
* auto& module = context.addModule("module"); | |
* module.class_<T>("T").fun<&T::var>("var").fun<&T::func>("func"); | |
*/ | |
template <auto F> | |
class_registrar& fun(const char * name) | |
{ | |
prototype.add<F>(name); | |
return *this; | |
} | |
template <auto FGet, auto FSet = nullptr> | |
class_registrar& property(const char * name) | |
{ | |
if /*constexpr*/ (FSet == nullptr) | |
prototype.add_getter<FGet>(name); | |
else | |
prototype.add_getter_setter<FGet, FSet>(name); | |
return *this; | |
} | |
/** Add class constructor | |
* @tparam Args contructor arguments | |
* @param name constructor name (if not specified class name will be used) | |
*/ | |
template <typename... Args> | |
class_registrar& constructor(const char * name = nullptr) | |
{ | |
if(!name) | |
name = this->name; | |
Value ctor = context.newValue(qjs::ctor_wrapper<T, Args...>{name}); | |
JS_SetConstructor(context.ctx, ctor.v, prototype.v); | |
module.add(name, std::move(ctor)); | |
return *this; | |
} | |
/* TODO: needs casting to base class | |
template <class B> | |
class_registrar& base() | |
{ | |
assert(js_traits<std::shared_ptr<B>>::QJSClassId && "base class is not registered"); | |
auto base_proto = JS_GetClassProto(context.ctx, js_traits<std::shared_ptr<B>>::QJSClassId); | |
int err = JS_SetPrototype(context.ctx, prototype.v, base_proto); | |
JS_FreeValue(context.ctx, base_proto); | |
if(err < 0) | |
throw exception{}; | |
return *this; | |
} | |
*/ | |
~class_registrar() | |
{ | |
context.registerClass<T>(name, std::move(prototype)); | |
} | |
}; | |
public: | |
/** Add class to module. | |
* See \ref class_registrar. | |
*/ | |
template <class T> | |
class_registrar<T> class_(const char * name) | |
{ | |
return class_registrar<T>{name, *this, qjs::Context::get(ctx)}; | |
} | |
}; | |
std::vector<Module> modules; | |
private: | |
void init() | |
{ | |
JS_SetContextOpaque(ctx, this); | |
js_traits<detail::function>::register_class(ctx, "C++ function"); | |
} | |
public: | |
Context(Runtime& rt) : Context(rt.rt) | |
{} | |
Context(JSRuntime * rt) | |
{ | |
ctx = JS_NewContext(rt); | |
if(!ctx) | |
throw std::runtime_error{"qjs: Cannot create context"}; | |
init(); | |
} | |
Context(JSContext * ctx) : ctx{ctx} | |
{ | |
init(); | |
} | |
// noncopyable | |
Context(const Context&) = delete; | |
~Context() | |
{ | |
modules.clear(); | |
JS_FreeContext(ctx); | |
} | |
/** Create module and return a reference to it */ | |
Module& addModule(const char * name) | |
{ | |
modules.emplace_back(ctx, name); | |
return modules.back(); | |
} | |
/** returns globalThis */ | |
Value global() { return Value{ctx, JS_GetGlobalObject(ctx)}; } | |
/** returns new Object() */ | |
Value newObject() { return Value{ctx, JS_NewObject(ctx)}; } | |
/** returns JS value converted from c++ object val */ | |
template <typename T> | |
Value newValue(T&& val) { return Value{ctx, std::forward<T>(val)}; } | |
/** returns current exception associated with context, and resets it. Should be called when qjs::exception is caught */ | |
Value getException() { return Value{ctx, JS_GetException(ctx)}; } | |
/** Register class T for conversions to/from std::shared_ptr<T> to work. | |
* Wherever possible module.class_<T>("T")... should be used instead. | |
* @tparam T class type | |
* @param name class name in JS engine | |
* @param proto JS class prototype or JS_UNDEFINED | |
*/ | |
template <class T> | |
void registerClass(const char * name, JSValue proto = JS_NULL) | |
{ | |
js_traits<std::shared_ptr<T>>::register_class(ctx, name, proto); | |
} | |
Value eval(std::string_view buffer, const char * filename = "<eval>", unsigned eval_flags = 0) | |
{ | |
assert(buffer.data()[buffer.size()] == '\0' && "eval buffer is not null-terminated"); // JS_Eval requirement | |
JSValue v = JS_Eval(ctx, buffer.data(), buffer.size(), filename, eval_flags); | |
return Value{ctx, v}; | |
} | |
Value evalFile(const char * filename, unsigned eval_flags = 0) | |
{ | |
size_t buf_len; | |
auto deleter = [this](void * p) { js_free(ctx, p); }; | |
auto buf = std::unique_ptr<uint8_t, decltype(deleter)>{js_load_file(ctx, &buf_len, filename), deleter}; | |
if(!buf) | |
throw std::runtime_error{std::string{"evalFile: can't read file: "} + filename}; | |
return eval({reinterpret_cast<char *>(buf.get()), buf_len}, filename, eval_flags); | |
} | |
Value fromJSON(std::string_view buffer, const char * filename = "<fromJSON>") | |
{ | |
assert(buffer.data()[buffer.size()] == '\0' && "fromJSON buffer is not null-terminated"); // JS_ParseJSON requirement | |
JSValue v = JS_ParseJSON(ctx, buffer.data(), buffer.size(), filename); | |
return Value{ctx, v}; | |
} | |
/** Get qjs::Context from JSContext opaque pointer */ | |
static Context& get(JSContext * ctx) | |
{ | |
void * ptr = JS_GetContextOpaque(ctx); | |
assert(ptr); | |
return *reinterpret_cast<Context *>(ptr); | |
} | |
}; | |
/** Conversion traits for Value. | |
*/ | |
template <> | |
struct js_traits<Value> | |
{ | |
static Value unwrap(JSContext * ctx, JSValueConst v) | |
{ | |
return Value{ctx, JS_DupValue(ctx, v)}; | |
} | |
static JSValue wrap(JSContext * ctx, Value v) noexcept | |
{ | |
assert(ctx == v.ctx); | |
return v.release(); | |
} | |
}; | |
/** Convert to/from std::function. Actually accepts/returns callable object that is compatible with function<R (Args...)>. | |
* @tparam R return type | |
* @tparam Args argument types | |
*/ | |
template <typename R, typename... Args> | |
struct js_traits<std::function<R(Args...)>> | |
{ | |
static auto unwrap(JSContext * ctx, JSValueConst fun_obj) | |
{ | |
const int argc = sizeof...(Args); | |
if constexpr(argc == 0) | |
{ | |
return[jsfun_obj = Value{ ctx, JS_DupValue(ctx, fun_obj) }]()->R { | |
JSValue result = JS_Call(jsfun_obj.ctx, jsfun_obj.v, JS_UNDEFINED, 0, nullptr); | |
if(JS_IsException(result)) | |
{ | |
JS_FreeValue(jsfun_obj.ctx, result); | |
throw exception{}; | |
} | |
return detail::unwrap_free<R>(jsfun_obj.ctx, result); | |
}; | |
} | |
else | |
{ | |
return[jsfun_obj = Value{ ctx, JS_DupValue(ctx, fun_obj) }](Args&& ... args)->R { | |
JSValue argv[argc]; | |
detail::wrap_args(jsfun_obj.ctx, argv, std::forward<Args>(args)...); | |
JSValue result = JS_Call(jsfun_obj.ctx, jsfun_obj.v, JS_UNDEFINED, argc, const_cast<JSValueConst*>(argv)); | |
for(int i = 0; i < argc; i++) JS_FreeValue(jsfun_obj.ctx, argv[i]); | |
if(JS_IsException(result)) | |
{ | |
JS_FreeValue(jsfun_obj.ctx, result); | |
throw exception{}; | |
} | |
return detail::unwrap_free<R>(jsfun_obj.ctx, result); | |
}; | |
} | |
} | |
/** Convert from function object functor to JSValue. | |
* Uses detail::function for type-erasure. | |
*/ | |
template <typename Functor> | |
static JSValue wrap(JSContext * ctx, Functor&& functor) | |
{ | |
using detail::function; | |
assert(js_traits<function>::QJSClassId); | |
auto obj = JS_NewObjectClass(ctx, js_traits<function>::QJSClassId); | |
if(JS_IsException(obj)) | |
return JS_EXCEPTION; | |
auto fptr = function::create(JS_GetRuntime(ctx), std::forward<Functor>(functor)); | |
fptr->invoker = [](function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) { | |
assert(self); | |
auto f = reinterpret_cast<Functor *>(&self->functor); | |
return detail::wrap_call<R, Args...>(ctx, *f, argv); | |
}; | |
JS_SetOpaque(obj, fptr); | |
return obj; | |
} | |
}; | |
/** Convert from std::vector<T> to Array and vice-versa. If Array holds objects that are non-convertible to T throws qjs::exception */ | |
template <class T> | |
struct js_traits<std::vector<T>> | |
{ | |
static JSValue wrap(JSContext * ctx, const std::vector<T>& arr) noexcept | |
{ | |
try | |
{ | |
auto jsarray = Value{ctx, JS_NewArray(ctx)}; | |
for(uint32_t i = 0; i < (uint32_t) arr.size(); i++) | |
jsarray[i] = arr[i]; | |
return jsarray.release(); | |
} | |
catch(exception) | |
{ | |
return JS_EXCEPTION; | |
} | |
} | |
static std::vector<T> unwrap(JSContext * ctx, JSValueConst jsarr) | |
{ | |
int e = JS_IsArray(ctx, jsarr); | |
if(e == 0) | |
JS_ThrowTypeError(ctx, "js_traits<std::vector<T>>::unwrap expects array"); | |
if(e <= 0) | |
throw exception{}; | |
Value jsarray{ctx, JS_DupValue(ctx, jsarr)}; | |
std::vector<T> arr; | |
auto len = static_cast<int32_t>(jsarray["length"]); | |
arr.reserve((uint32_t) len); | |
for(uint32_t i = 0; i < (uint32_t) len; i++) | |
arr.push_back(static_cast<T>(jsarray[i])); | |
return arr; | |
} | |
}; | |
namespace detail { | |
template <typename Key> | |
property_proxy<Key>::operator Value() const | |
{ | |
return as<Value>(); | |
} | |
} | |
} // namespace qjs |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment