docs

Using JavaScriptCore Directly

Access the raw JavaScriptCore C API to port code or use advanced features.

On this page

You can write code against the raw JavaScriptCore C API directly. This C API sits under the ultralight::js typed layer and ships fully supported with the SDK. Use it to port existing JavaScriptCore code or to access features the typed layer omits. For most tasks the typed layer is still the easier path.

Getting a Context

To get a raw context, call LockJSContext() on your View (passing an iframe's name attribute to access that iframe) and include <JavaScriptCore/JavaScript.h>.

C++
#include <JavaScriptCore/JavaScript.h>

void MyApp::OnDOMReady(View* caller, uint64_t frame_id,
                       bool is_main_frame, const String& url) {
  RefPtr<JSContext> context = caller->LockJSContext();
  if (!context)
    return;
  JSContextRef ctx = context->ctx();

  JSStringRef script = JSStringCreateWithUTF8CString("startGame()");
  JSEvaluateScript(ctx, script, nullptr, nullptr, 0, nullptr);
  JSStringRelease(script);
}

The call returns a RefPtr that locks the JavaScript VM until it goes out of scope (returning nullptr when the page cannot run scripts). Call ctx() on it to get the JSContextRef.

🚧 Contexts Reset on Navigation

The context resets on every navigation. You must get it again for each page. LoadListener::OnWindowObjectReady() or LoadListener::OnDOMReady() are good places to set up state.

Retrieving From Typed Code

To get the underlying JSContextRef from an existing js::Context, call ulJSContextGetJSContextRef() (call this only on the Renderer's thread and never store the result).

C++
js::Context ctx(caller);
JSContextRef jsc_ctx = ulJSContextGetJSContextRef(ctx.raw());

What the Fork Adds

The library uses a fork of JavaScriptCore that adds to the classic C API. You can find the details in the headers for these additions: JSObjectRef.h, JSValueRef.h and JSTypedArray.h.

Addition Description
JSObjectMakeFunctionWithData Creates a real function object with a user-data pointer and a finalizer. Script methods like bind, call and apply work normally.
UTF-8 property functions Get, set and define properties by UTF-8 name (JSObjectGetPropertiesUTF8, JSObjectSetPropertiesUTF8, JSObjectMakeWithPropertiesUTF8, JSObjectGetOwnEntriesUTF8, JSObjectDefinePropertyUTF8, JSObjectDefineAccessorUTF8). You can assign several properties in one call. JSObjectFreeze maps to Object.freeze.
JSObjectMakeTypedError, JSValueGetErrorDetails Create an error of a specific type and read its type, message, source location and stack in one call.
JSPromiseGetStatus, JSPromiseGetResult, JSPromiseMarkAsHandled Read a promise's state and result without scheduling a reaction. You can mark a rejection that you handled natively.
JSArrayBufferDetach, JSArrayBufferIsDetached Free memory shared with a buffer so that later script access predictably fails.
JSObjectSetExternalMemoryHint Tell the garbage collector how much native memory a small wrapper keeps alive.
JSValueIsFunctionObject Returns true only for an actual function object (JSObjectIsFunction also returns true for anything callable like a callable Proxy).
JSClassDefinition version 1000 Callbacks ending in Ex receive the JSClassRef, and you gain access to per-class private data (JSClassGetPrivate and JSClassSetPrivate). Version 0 remains unchanged.

For example, you can create a frozen display object and define several properties in one call.

C++
const char* names[] = { "width", "height" };
JSValueRef values[] = { JSValueMakeNumber(ctx, 1280),
                        JSValueMakeNumber(ctx, 720) };
JSObjectRef display =
    JSObjectMakeWithPropertiesUTF8(ctx, names, values, 2, nullptr);
JSObjectFreeze(ctx, display, nullptr);

JSObjectRef global = JSContextGetGlobalObject(ctx);
JSObjectDefinePropertyUTF8(ctx, global, "display", display,
                           kJSPropertyAttributeReadOnly, nullptr);

Mixing Raw and Typed Code

There is no conversion step between a JSValueRef and a js::Value. The values meet on the page.

Raw values behave like ordinary page JavaScript so page script sees them as normal variables.

JavaScript
console.log(display.width);  // 1280
display.width = 1920;        // fails: display is frozen

The typed layer reads those values by name like any other page value.

C++
js::Context ctx(caller);
double width = ctx["display"]["width"].Or(0.0);

🚧 Raw Ownership Rules

The raw layer follows classic ownership rules. Anything you create you must release (like JSStringCreateWithUTF8CString and JSStringRelease, or JSClassCreate and JSClassRelease). A JSValueRef kept past the current call needs JSValueProtect and JSValueUnprotect. The typed layer's handles manage themselves.

If you are porting JSHelpers or 1.4-era JavaScriptCore code, see the tables in Porting from 1.4 to 2.0.