Back <

Executing Zig Code with JavaScriptCore

In this blog post, we'll delve into a code snippet that merges the Zig programming language with JavaScriptCore, a JavaScript engine library from WebKit. This fusion of languages offers the possibility of integrating JavaScript functionalities into applications developed in Zig.

Code Breakdown

Let's explore the code step by step to understand its functionality and how Zig and JavaScriptCore intertwine.

Imports and Initial Setup

The code begins with necessary imports in Zig, including the use of the standard library (std) and the definition of the printing function (print). It also imports the C interface of JavaScriptCore through @cImport, enabling interoperability with the C library.

// main.zig 
const std = @import("std");
const print = std.debug.print;

const jsc = @cImport({
    @cInclude("JavaScriptCore/JavaScript.h");
});

Main Function

The main function (main) is responsible for executing the core logic of the program. First, it creates a Zig GeneralPurposeAllocator for memory management.

// main.zig 
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

Creating the JavaScript Context

Next, a JavaScript context is created using the JSGlobalContextCreate function provided by JavaScriptCore. This context will serve as the execution environment for JavaScript code.

// main.zig 
    const context: jsc.JSGlobalContextRef = jsc.JSGlobalContextCreate(null);

Executing JavaScript Code

A JavaScript string is created and evaluated using the JSEvaluateScript function. In this case, the JavaScript code simply assigns a string value and then returns it.

// main.zig 
    const jsCode: jsc.JSStringRef = jsc.JSStringCreateWithUTF8CString("const some = 'hello world'; some");
    const result: jsc.JSValueRef = jsc.JSEvaluateScript(context, jsCode, null, null, 0, null);
    const jsString: jsc.JSStringRef = jsc.JSValueToStringCopy(context, result, null);

Converting JavaScript String to Zig String and Printing

After the JavaScript result has been obtained, the code proceeds to allocate a buffer using Zig's allocator to store the converted string. This buffer is used to hold the result of converting the JavaScript string to a Zig string.

// main.zig 
    var buffer = try allocator.alloc(u8, jsc.JSStringGetMaximumUTF8CStringSize(jsString));
    defer allocator.free(buffer);

The jsc.JSStringGetMaximumUTF8CStringSize(jsString) function is used to determine the maximum size required for the UTF-8 representation of the JavaScript string. The buffer is allocated accordingly, and a defer statement is used to ensure the buffer is freed once it goes out of scope.

Next, the code retrieves the UTF-8 encoded string from the JavaScript string using jsc.JSStringGetUTF8CString. The resulting string is then printed using Zig's print function.

// main.zig 
    const string_length = jsc.JSStringGetUTF8CString(jsString, buffer.ptr, buffer.len);
    const string = buffer[0..string_length];
    print("{s}", .{string});

This part of the code ensures that the JavaScript string is converted to a format that Zig can work with and is then printed to the console.

Releasing Resources

Finally, the code releases the resources associated with the JavaScript string and the JavaScript context. This is important to prevent memory leaks.

// main.zig 
    jsc.JSStringRelease(jsString);
    jsc.JSGlobalContextRelease(context);

Releasing the JavaScript string is essential to free the memory used by it, and releasing the JavaScript context is necessary to clean up the resources allocated for JavaScript execution.

In summary, the complete code snippet demonstrates how to execute JavaScript code within a Zig program, convert the JavaScript result to a Zig string, and then print it. Additionally, it emphasizes the importance of releasing resources to maintain memory efficiency and avoid potential issues.

Full Code

// main.zig       
const std = @import("std");
const print = std.debug.print;

const c = @cImport({
    @cInclude("stdio.h");
});

const jsc = @cImport({
    @cInclude("JavaScriptCore/JavaScript.h");
});

pub fn main() !void {
    // Create a GeneralPurposeAllocator for memory allocation
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    // Create a JavaScript context
    const context: jsc.JSGlobalContextRef = jsc.JSGlobalContextCreate(null);

    // Create a JavaScript string
    const jsCode: jsc.JSStringRef = jsc.JSStringCreateWithUTF8CString("const some = 'hello world'; some");

    // Evaluate the JavaScript code in the context
    const result: jsc.JSValueRef = jsc.JSEvaluateScript(context, jsCode, null, null, 0, null);

    // Convert the result to a string
    const jsString: jsc.JSStringRef = jsc.JSValueToStringCopy(context, result, null);

    // Allocate memory to store the string in UTF-8 format
    var buffer = try allocator.alloc(u8, jsc.JSStringGetMaximumUTF8CStringSize(jsString));
    defer allocator.free(buffer);

    // Get the string in UTF-8 format
    const string_length = jsc.JSStringGetUTF8CString(jsString, buffer.ptr, buffer.len);
    const string = buffer[0..string_length];

    // Print the string
    print("{s}", .{string});

    // Release resources
    jsc.JSStringRelease(jsString);
    jsc.JSGlobalContextRelease(context);
}

Run Code

zig run main.zig -I/usr/include/webkitgtk-4.0 -ljavascriptcoregtk-4.0 -lc

I hope this extended explanation provides a more thorough understanding of the entire code, including the parts that were initially omitted.