diff --git a/Makefile b/Makefile index 3d79adc..bba38dc 100644 --- a/Makefile +++ b/Makefile @@ -2,9 +2,9 @@ RACK_DIR ?= ../.. FLAGS += CFLAGS += -CXXFLAGS += +CXXFLAGS += -g -LDFLAGS += +LDFLAGS += -g SOURCES += $(wildcard src/*.cpp) @@ -24,6 +24,17 @@ $(duktape): cd dep && $(SHA256) duktape-2.4.0.tar.xz 86a89307d1633b5cedb2c6e56dc86e92679fc34b05be551722d8cc69ab0771fc cd dep && $(UNTAR) duktape-2.4.0.tar.xz +# QuickJS +quickjs := dep/QuickJS/libquickjs.a +DEPS += $(quickjs) +OBJECTS += $(quickjs) +FLAGS += -Idep/QuickJS +LDFLAGS += -Ldep/QuickJS -lquickjs + +$(quickjs): + cd dep && git clone "https://github.com/JerrySievert/QuickJS" + cd dep/QuickJS && $(MAKE) + # # LuaJIT # luajit := dep/lib/luajit.a diff --git a/src/QuickJSEngine.cpp b/src/QuickJSEngine.cpp new file mode 100644 index 0000000..d6b691e --- /dev/null +++ b/src/QuickJSEngine.cpp @@ -0,0 +1,323 @@ +#include "ScriptEngine.hpp" +#include + +static JSClassID QuickJSEngineClass; + +static std::string ErrorToString(JSContext *ctx) { + JSValue exception_val, val; + const char *stack; + const char *str; + bool is_error; + std::string ret; + size_t s1, s2; + + exception_val = JS_GetException(ctx); + is_error = JS_IsError(ctx, exception_val); + str = JS_ToCStringLen(ctx, &s1, exception_val); + + if (!str) { + return std::string("error thrown but no error message"); + } + + if (!is_error) { + ret = std::string("Throw:\n") + std::string(str); + } else { + val = JS_GetPropertyStr(ctx, exception_val, "stack"); + + if (!JS_IsUndefined(val)) { + stack = JS_ToCStringLen(ctx, &s2, val); + ret = std::string(str) + std::string("\n") + std::string(stack); + JS_FreeCString(ctx, stack); + } + JS_FreeValue(ctx, val); + } + + JS_FreeCString(ctx, str); + JS_FreeValue(ctx, exception_val); + + return ret; +} + + +struct QuickJSEngine : ScriptEngine { + JSRuntime *rt = NULL; + JSContext *ctx = NULL; + + QuickJSEngine() { + rt = JS_NewRuntime(); + } + + ~QuickJSEngine() { + if (ctx) { + JS_FreeContext(ctx); + } + if (rt) { + JS_FreeRuntime(rt); + } + } + + std::string getEngineName() override { + return "JavaScript"; + } + + int run(const std::string& path, const std::string& script) override { + assert(!ctx); + // Create quickjs context + ctx = JS_NewContext(rt); + if (!ctx) { + setMessage("Could not create QuickJS context"); + return -1; + } + + // Initialize globals + // user pointer + JSValue global_obj = JS_GetGlobalObject(ctx); + JSValue handle = JS_NewObjectClass(ctx, QuickJSEngineClass); + JS_SetOpaque(handle, this); + JS_SetPropertyStr(ctx, global_obj, "p", handle); + + // console + JSValue console = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, console, "log", + JS_NewCFunction(ctx, native_console_log, "log", 1)); + JS_SetPropertyStr(ctx, console, "info", + JS_NewCFunction(ctx, native_console_info, "info", 1)); + JS_SetPropertyStr(ctx, console, "debug", + JS_NewCFunction(ctx, native_console_debug, "debug", 1)); + JS_SetPropertyStr(ctx, console, "warn", + JS_NewCFunction(ctx, native_console_warn, "warn", 1)); + JS_SetPropertyStr(ctx, global_obj, "console", console); + + // display + JS_SetPropertyStr(ctx, global_obj, "display", + JS_NewCFunction(ctx, native_display, "display", 1)); + + // config: Set defaults + JSValue config = JS_NewObject(ctx); + // frameDivider + JSValue fd =JS_NewInt32(ctx, 32); + JS_SetPropertyStr(ctx, config, "frameDivider", fd); + JS_FreeValue(ctx, fd); + // bufferSize + JSValue bs = JS_NewInt32(ctx, 1); + JS_SetPropertyStr(ctx, config, "bufferSize", bs); + JS_SetPropertyStr(ctx, global_obj, "config", config); + JS_FreeValue(ctx, bs); + + // Compile string + JSValue val = JS_Eval(ctx, script.c_str(), script.size(), path.c_str(), 0); + if (JS_IsException(val)) { + setMessage(ErrorToString(ctx)); + JS_FreeValue(ctx, val); + JS_FreeValue(ctx, global_obj); + + return -1; + } + + // config: Read values + config = JS_GetPropertyStr(ctx, global_obj, "config"); + { + // frameDivider + JSValue divider = JS_GetPropertyStr(ctx, config, "frameDivider"); + int32_t dividerValue; + if (JS_ToInt32(ctx, ÷rValue, divider) == 0) { + setFrameDivider(dividerValue); + } + + // bufferSize + JSValue buffer = JS_GetPropertyStr(ctx, config, "bufferSize"); + int32_t bufferValue; + if (JS_ToInt32(ctx, &bufferValue, buffer) == 0) { + block->bufferSize = rack::clamp(bufferValue, 1, MAX_BUFFER_SIZE); + } + + JS_FreeValue(ctx, config); + } + + // block + JSValue blockIdx = JS_NewObject(ctx); + { + // inputs + JSValue arr = JS_NewArray(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + JSValue buffer = JS_NewArrayBuffer(ctx, (uint8_t *) block->inputs[i], sizeof(float) * block->bufferSize, NULL, NULL, true); + if (JS_SetPropertyUint32(ctx, arr, i, buffer) < 0) { + rack::WARN("Unable to set property %d of inputs array", i); + } + } + JS_SetPropertyStr(ctx, blockIdx, "inputs", arr); + + // outputs + arr = JS_NewArray(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + JSValue buffer = JS_NewArrayBuffer(ctx, (uint8_t *) block->outputs[i], sizeof(float) * block->bufferSize, NULL, NULL, true); + if (JS_SetPropertyUint32(ctx, arr, i, buffer) < 0) { + rack::WARN("Unable to set property %d of outputs array", i); + } + } + JS_SetPropertyStr(ctx, blockIdx, "outputs", arr); + + // knobs + JSValue knobsIdx = JS_NewArrayBuffer(ctx, (uint8_t *) &block->knobs, sizeof(float) * NUM_ROWS, NULL, NULL, true); + JS_SetPropertyStr(ctx, blockIdx, "knobs", knobsIdx); + + // switches + JSValue switchesIdx = JS_NewArrayBuffer(ctx, (uint8_t *) &block->switches, sizeof(bool) * block->bufferSize, NULL, NULL, true); + JS_SetPropertyStr(ctx, blockIdx, "switches", switchesIdx); + + // lights + arr = JS_NewArray(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + JSValue buffer = JS_NewArrayBuffer(ctx, (uint8_t *) &block->lights[i], sizeof(float) * 3, NULL, NULL, true); + if (JS_SetPropertyUint32(ctx, arr, i, buffer) < 0) { + rack::WARN("Unable to set property %d of lights array", i); + } + } + JS_SetPropertyStr(ctx, blockIdx, "lights", arr); + + // switchlights + arr = JS_NewArray(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + JSValue buffer = JS_NewArrayBuffer(ctx, (uint8_t *) &block->switchLights[i], sizeof(float) * 3, NULL, NULL, true); + if (JS_SetPropertyUint32(ctx, arr, i, buffer) < 0) { + rack::WARN("Unable to set property %d of switchLights array", i); + } + } + JS_SetPropertyStr(ctx, blockIdx, "switchLights", arr); + } + + JS_SetPropertyStr(ctx, global_obj, "block", blockIdx); + + // this is a horrible hack to get QuickJS to allocate the correct types + std::string updateTypes = "for (var i = 0; i < 6; i++) { block.inputs[i] = new Float32Array(block.inputs[i]); block.outputs[i] = new Float32Array(block.outputs[i]); " \ + "block.lights[i] = new Float32Array(block.lights[i]); block.switchLights[i] = new Float32Array(block.switchLights[i]); } " \ + "block.knobs = new Float32Array(block.knobs); block.switches = new Uint8Array(block.switches);"; + + JSValue hack = JS_Eval(ctx, updateTypes.c_str(), updateTypes.size(), "QuickJS Hack", 0); + if (JS_IsException(hack)) { + std::string errorString = ErrorToString(ctx); + rack::WARN("QuickJS: %s", errorString.c_str()); + setMessage(errorString.c_str()); + } + + JS_FreeValue(ctx, hack); + + JS_FreeValue(ctx, val); + JS_FreeValue(ctx, global_obj); + + if (JS_IsException(val)) { + std::string errorString = ErrorToString(ctx); + rack::WARN("QuickJS: %s", errorString.c_str()); + setMessage(errorString.c_str()); + + JS_FreeValue(ctx, val); + JS_FreeValue(ctx, blockIdx); + JS_FreeValue(ctx, global_obj); + + return -1; + } + + return 0; + } + + int process() override { + // global object + JSValue global_obj = JS_GetGlobalObject(ctx); + + // block + JSValue blockIdx = JS_GetPropertyStr(ctx, global_obj, "block"); + { + // sampleRate + JSValue sampleRate = JS_NewFloat64(ctx, (double) block->sampleRate); + JS_SetPropertyStr(ctx, blockIdx, "sampleRate", sampleRate); + + // sampleTime + JSValue sampleTime = JS_NewFloat64(ctx, (double) block->sampleTime); + JS_SetPropertyStr(ctx, blockIdx, "sampleTime", sampleTime); + + // bufferSize + JSValue bufferSize = JS_NewInt32(ctx, (double) block->bufferSize); + JS_SetPropertyStr(ctx, blockIdx, "bufferSize", bufferSize); + } + + JSValue process = JS_GetPropertyStr(ctx, global_obj, "process"); + + JSValue val = JS_Call(ctx, process, JS_UNDEFINED, 1, &blockIdx); + + if (JS_IsException(val)) { + std::string errorString = ErrorToString(ctx); + rack::WARN("QuickJS: %s", errorString.c_str()); + setMessage(errorString.c_str()); + + JS_FreeValue(ctx, val); + JS_FreeValue(ctx, process); + JS_FreeValue(ctx, blockIdx); + JS_FreeValue(ctx, global_obj); + + return -1; + } + + JS_FreeValue(ctx, val); + JS_FreeValue(ctx, process); + JS_FreeValue(ctx, global_obj); + + return 0; + } + + static QuickJSEngine* getQuickJSEngine(JSContext* ctx) { + JSValue global_obj = JS_GetGlobalObject(ctx); + JSValue p = JS_GetPropertyStr(ctx, global_obj, "p"); + QuickJSEngine* engine = (QuickJSEngine*) JS_GetOpaque(p, QuickJSEngineClass); + + JS_FreeValue(ctx, p); + JS_FreeValue(ctx, global_obj); + + return engine; + } + + static JSValue native_console_log(JSContext* ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc) { + const char *s = JS_ToCString(ctx, argv[0]); + rack::INFO("VCV Prototype: %s", s); + } + return JS_UNDEFINED; + } + static JSValue native_console_debug(JSContext* ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc) { + const char *s = JS_ToCString(ctx, argv[0]); + rack::DEBUG("VCV Prototype: %s", s); + } + return JS_UNDEFINED; + } + static JSValue native_console_info(JSContext* ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc) { + const char *s = JS_ToCString(ctx, argv[0]); + rack::INFO("VCV Prototype: %s", s); + } + return JS_UNDEFINED; + } + static JSValue native_console_warn(JSContext* ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc) { + const char *s = JS_ToCString(ctx, argv[0]); + rack::WARN("VCV Prototype: %s", s); + } + return JS_UNDEFINED; + } + static JSValue native_display(JSContext* ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc) { + const char *s = JS_ToCString(ctx, argv[0]); + getQuickJSEngine(ctx)->setMessage(s); + } + return JS_UNDEFINED; + } +}; + + +ScriptEngine* createQuickJSEngine() { + return new QuickJSEngine(); +} diff --git a/src/ScriptEngine.hpp b/src/ScriptEngine.hpp index 49aecd9..be3d0b7 100644 --- a/src/ScriptEngine.hpp +++ b/src/ScriptEngine.hpp @@ -50,12 +50,12 @@ struct ScriptEngine { // List of ScriptEngines // Add your createMyEngine() function here. -ScriptEngine* createDuktapeEngine(); +ScriptEngine* createQuickJSEngine(); inline ScriptEngine* createScriptEngine(std::string ext) { ext = rack::string::lowercase(ext); if (ext == "js") - return createDuktapeEngine(); + return createQuickJSEngine(); // Add your file extension check here. return NULL; }