Usage
Evaluating JavaScript code in C
This example creates a runtime and a context, evaluates a simple JavaScript snippet and prints the result.
#include <quickjs.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// Create a JS runtime and context
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
fprintf(stderr, "Failed to create JS runtime\n");
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
fprintf(stderr, "Failed to create JS context\n");
JS_FreeRuntime(rt);
return 1;
}
// JavaScript code to execute
const char *js_code = "let a = 2; let b = 4; a * b;";
// Evaluate the code
JSValue result = JS_Eval(ctx, js_code, strlen(js_code), "<input>", JS_EVAL_TYPE_GLOBAL);
if (JS_IsException(result)) {
JSValue exception = JS_GetException(ctx);
const char *error = JS_ToCString(ctx, exception);
fprintf(stderr, "JavaScript exception: %s\n", error);
JS_FreeCString(ctx, error);
JS_FreeValue(ctx, exception);
} else {
// Try to convert the JSValue to an int
int32_t int_result;
if (JS_ToInt32(ctx, &int_result, result) == 0) {
printf("Result: %d\n", int_result);
} else {
// Convert to string instead
const char *str_result = JS_ToCString(ctx, result);
if (str_result) {
printf("Result: %s\n", str_result);
JS_FreeCString(ctx, str_result);
} else {
printf("Failed to convert result to string\n");
}
}
JS_FreeValue(ctx, result);
}
// Clean up
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
After compiling and executing this program, we get the following output:
Result: 8
Injecting a function with a native C backend
The true value of QuickJS is that it lets us expose C function to the JavaScript runtime, making it easy to add scripting to any C project.
To do this, we first write the function we want to expose in C. The function must take a JSContext, a JSValue for this
, an int for the number of arguments, and a list of JSValue arguments to the function. Additionally, it must return a JSValue.
The function below is a simple print function which prints all arguments to stdout, separated by spaces. Since we don't want to return a value, we return JS_UNDEFINED
.
static JSValue js_print(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
for (int i = 0; i < argc; i++) {
const char *str = JS_ToCString(ctx, argv[i]);
if (!str) {
return JS_EXCEPTION;
}
printf("%s", str);
JS_FreeCString(ctx, str);
if (i != argc - 1) {
printf(" ");
}
}
printf("\n");
return JS_UNDEFINED;
}
We can now create a JSValue from this function using JS_NewCFunction
:
JSValue print = JS_NewCFunction(ctx, js_print, "print", 1);
The 1
passed as the last parameter sets the length of the function, which typically corresponds to the number of expected arguments, but it doesn't actually limit the usage of the function to one argument. This is what will be returned if print.length
is read in JavaScript.
Finally, we can add our function to the global scope, making it available to JavaScript:
JSValue global_obj = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global_obj, "print", print);
JS_FreeValue(ctx, global_obj);
Note that we must not free the print
JSValue since ownership is transferred as part of the call to JS_SetPropertyStr.
After setting the property, we can once again evaluate JavaScript code, now using our new function:
const char *script = "print('Hello,', 'World!');";
JSValue result = JS_Eval(ctx, script, strlen(script), "<input>", JS_EVAL_TYPE_GLOBAL);
// Cleanup logic
This should print the string Hello, World!
to stdout.
Tracking custom context data
Often times we may find it useful to track custom state variables for a JSContext, that we can retrieve and set inside native functions. This can be done with the help of QuickJS's JS_SetContextOpaque
and JS_GetContextOpaque
functions, which let us set a custom pointer to a data structure we define, and retrieve it from the context.
// define a customs struct
struct my_data {
int n;
};
// At the start of the program
struct my_data *data = malloc(sizeof(my_data));
JS_SetContextOpaque(ctx, data);
// Custom JS function
static JSValue js_get_data(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
strucy my_data *data = (struct my_data *)JS_GetContextOpaque(ctx);
if (data) {
return JS_NewInt32(ctx, data->n);
}
return JS_UNDEFINED;
}
Injecting custom objects
Objects can be created using JS_NewObject
. Like functions, we can assign them to the global object using JS_SetPropertyStr
, and we can also use this same function to set its own properties:
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj, "myString", JS_NewString(ctx, "QuickJS"));
JS_SetPropertyStr(ctx, global, "myObject", obj);
// JS Can now access myObject.myString, which contains "QuickJS"
Sometimes, we want to run custom logic when the value of a property is accessed or modified. We can do this using JS_DefinePropertyGetSet
:
static js_get_mystring(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
return JS_UNDEFINED; // TODO Return custom string
}
static js_get_mystring(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
// TODO Set custom string
return JS_UNDEFINED;
}
// ...
JSAtom propname = JS_NewAtom(ctx, "mystring");
JSValue getter = JS_NewCFunction(ctx, js_get_mystring, "get_mystring", 0);
JSValue setter = JS_NewCFunction(ctx, js_set_mystring, "set_mystring", 1);
JSValue obj = JS_NewObject(ctx);
// Allow property to be written to, deleted, and iterated over
int flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE;
JS_DefinePropertyGetSet(ctx, obj, propname, getter, setter, flags);
JS_SetPropertyStr(ctx, global, "myObject", obj);
JS_FreeAtom(ctx, propname);
If we want to store custom data in the object that isn't exposed to JS, we can do so using a custom class.
struct my_class_data {
int n;
};
static JSClassID my_class_id;
static void my_class_finalizer(JSRuntime *rt, JSValue val) {
// This function acts as the destructor of the class
struct my_class_data *data = JS_GetOpaque(val, my_class_id);
if (data) {
free(data);
}
}
static JSClassDef my_class = {
.class_name = "MyClass",
.finalizer = my_class_finalizer,
};
// Register class
JS_NewClassID(&my_class_id);
JS_NewClass(rt, my_class_id, &my_class);
// Create object of class
JSValue obj = JS_NewObjectClass(ctx, my_class_id);
// Set custom data
struct my_class_data *obj_data = malloc(sizeof(struct my_class_data));
JS_SetOpaque(obj, obj_data);
// At a later point, retrieve data
struct my_class_data *obj_data = (struct my_class_data*)JS_GetOpaque(obj, my_class_id);
Injecting a native async function
In JavaScript, an async function is just a regular function that returns a Promise, which can be resolved or rejected at a later point. This can be done in QuickJS as well:
// Function C code
static JSValue js_read_async(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
assert(argc == 1); // TODO Raise a JS exception instead
// JSValues for the resolve and reject callbacks of the promise
JSValue resolve_reject[2];
// Create a new promise
JSValue promise = JS_NewPromiseCapability(ctx, resolve_reject);
char *path = JS_ToCString(ctx, argv[0]);
add_read_job(path, resolve_reject); // Just an example, replace with your own queue system
JS_FreeCString(ctx, path);
return promise;
}
// Create function and add it to the global object
JSValue read = JS_NewCFunction(ctx, js_read_async, "read", 1);
JSValue global_obj = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global_obj, "read", read);
JS_FreeValue(ctx, global_obj);
The above-defined function creates a new promise and returns it. Note that we neither free the promise, nor the callbacks as their lifetime exceeds this function. Whenever the job is complete and we want to resolve the promise, we need to do something like this:
JSValue result = JS_NewString(ctx, file_content);
// Call resolve_reject[0] to resolve the promise, and resolve_reject[1] to reject it
JSValue retval = JS_Call(ctx, resolve_reject[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(ctx, retval);
JS_FreeValue(ctx, result);
// Free callbacks
JS_FreeValue(ctx, resolve_reject[0]);
JS_FreeValue(ctx, resolve_reject[1]);
Memory management is particularly tricky here. Our example add_read_job
must itself copy over the content of resolve_reject
since the array will be popped off the stack when js_read_async
returns. Additionally, both callbacks must be freed when the promise is resolved or rejected. Ownership of the promise itself is transferred to the JavaScript runtime where it should eventually be garbage collected. If you get an error saying the Promise was not freed when you free the runtime, this likely means you are not freeing the return value of some call to JS_Call
or JS_Eval
, so a reference to the promise still exists.
If everything went correctly, we can now use our function in JavaScript like any other async function:
// Using then/catch
read("myfile.txt")
.then((content) => print("File content: " + content))
.catch(() => print("Failed to read file content"));
// Using async/await
async function myAsyncFunction() {
const content = await read("myfile.txt");
print("File content: " + content);
}
myAsyncFunction();