> 📘 Preview API
>
> The JavaScript C API ships as a preview in Ultralight 2.0. It may still change after 2.0.

The Ultralight C API lets you call JavaScript functions from C and expose native functions to page script.

This page covers what differs in C— read [About JavaScript Interop](/docs/2.0/about-javascript-interop) and [Extending JavaScript with Native API](/docs/2.0/extending-javascript-with-native-api) first for the underlying concepts.

Including `<Ultralight/CAPI.h>` gives you all five JavaScript headers automatically.

## Differences from C++

The JavaScript C API replaces C++ templates, exceptions, and automatic type conversion with opaque handles and explicit checks.

| C++ Feature | C API Equivalent | How It Differs |
| :--- | :--- | :--- |
| `js::API` | `ulCreateJSAPI()` and `ulDestroyJSAPI()` | You keep the handle alive while pages need the API (see [What Destroying a Handle Does](/docs/2.0/c-api-conventions#content-what-destroying-a-handle-does)). |
| `app["add"] = lambda` | `ulJSAPIBindFunction()` with `ULJSFunctionCallback` | Callbacks receive raw arguments without automatic arity or type checks. |
| Strict `Value::Or()` and `To<T>()` | `ulJSValueGetType()`, then `ulJSValueToNumber()` or `ulJSValueToString()` | Conversions follow JavaScript coercion rules, so check types first to enforce strict conversion. |
| Struct, vector, and enum marshaling with `js::TypeTraits` | None | Build objects property by property, or parse JSON with `ulCreateJSValueFromJSON()`. |
| `js::Result` and `js::Error` | `NULL` or `false` return with a `ULJSValue* exception` out-parameter | Page exceptions and the library's own errors write to the out-parameter. |
| `js::Error::WithCode()` | `ulCreateJSError()` and a `"code"` property | Set the `"code"` property explicitly using `ulJSObjectSetProperty()`. |
| `js::Resolver` | `ULJSPromiseResolver` | You own the resolver handle and must settle and destroy it. |
| `js::Task`, `co_await`, and `js::Await()` | None | Chain callbacks using `ulJSRunOnWorker()`, `ulJSPromiseResolverComplete()`, or `ulJSPromiseThen()`. |
| `DefineClass<T>()` | `ulCreateJSClass()` and `ulJSAPIRegisterClass()` | Instance ownership is set by an `adopt` flag or a custom holder. |
| `js::BindingGuard` | None | Call `ulJSAPIUnbind()` to remove a binding manually. |

## JavaScript Handle Ownership

JavaScript handles follow the general conventions described in [Managing Memory and Ownership](/docs/2.0/c-api-conventions#content-managing-memory-and-ownership), [Handling Errors](/docs/2.0/c-api-conventions#content-handling-errors), and [Passing NULL Handles](/docs/2.0/c-api-conventions#content-passing-null-handles)— this table shows what those rules mean for JavaScript calls.

| Handle | Who Owns It |
| :--- | :--- |
| Every handle a library function returns (values, properties, global objects, contexts, and errors) | You own the handle. Call `ulDestroyJSValue()` or `ulDestroyJSContext()` when finished. Each property read in a chain yields an owned handle that you must destroy. |
| Callback arguments, `this_value`, and `ctx` | The library owns them. They remain valid only during the callback. Call `ulCreateJSValueRef()` or `ulCreateJSContextRef()` to keep one longer. |
| Handles passed into library functions (arguments, property values, and resolve values) | They stay yours. The library only borrows them during the call, so destroy them when you're finished with them. |
| Handles your callback returns or stores in its `*exception` parameter | The library takes ownership. Do not destroy them. (An exception that a library call stores in your variable remains yours to destroy.) |

## Calling a Page Function

To call a JavaScript function on the page, look up the function property on the global object and invoke it with `ulJSFunctionCall()`.

```c
#include <Ultralight/CAPI.h>
#include <stdio.h>

/* Set with ulViewSetDOMReadyCallback(view, OnDOMReady, NULL, NULL). */
static void OnDOMReady(void* user_data, ULView caller,
                       unsigned long long frame_id, bool is_main_frame,
                       ULString url) {
  (void)user_data;
  (void)frame_id;
  (void)url;
  if (!is_main_frame)
    return;

  ULJSContext ctx = ulViewGetJSContext(caller);
  if (!ctx)
    return;

  ULJSValue global = ulJSContextGetGlobalObject(ctx);
  ULJSValue fn = ulJSObjectGetProperty(global, "computeTotal", NULL);
  ULJSValue args[2] = { ulCreateJSValueNumber(ctx, 3),
                        ulCreateJSValueNumber(ctx, 4) };
  ULJSValue exception = NULL;
  ULJSValue result = ulJSFunctionCall(fn, NULL, args, 2, &exception);

  if (ulJSValueGetType(result) == kULJSType_Number) {
    double total = 0;
    ulJSValueToNumber(result, &total, NULL);
    printf("total: %g\n", total);  /* total: 7 */
  } else if (exception) {
    ULJSErrorDetails details = {0};
    if (ulJSValueGetErrorDetails(exception, &details) && details.message)
      printf("script threw: %s (line %u)\n",
             ulStringGetData(details.message), details.line);
    ulJSErrorDetailsRelease(&details);
    ulDestroyJSValue(exception);
  }

  ulDestroyJSValue(result);
  ulDestroyJSValue(args[0]);
  ulDestroyJSValue(args[1]);
  ulDestroyJSValue(fn);
  ulDestroyJSValue(global);
  ulDestroyJSContext(ctx);
}
```

Keep these details in mind when calling a page function:

- **`ulViewGetJSContext()` returns `NULL` when the main frame cannot run scripts.** This happens when JavaScript is disabled or sandboxed. Each page receives its own context, so fetch a fresh context handle after every navigation.
- **Passing `NULL` for `this_value` uses the global object.** The function runs with the page's global object as `this`.
- **Inspect thrown errors with `ulJSValueGetErrorDetails()`.** It populates `ULJSErrorDetails` with owned strings. Release them with `ulJSErrorDetailsRelease()`.

For the underlying concepts, see [Calling into the Page](/docs/2.0/calling-into-the-page).

### Evaluating a Script

To evaluate a script expression and read its result as text without acquiring a context, call `ulViewEvaluateScript()`.

```c
ULString script = ulCreateString("document.title");
ULString title = ulViewEvaluateScript(view, script, NULL);
printf("%s\n", ulStringGetData(title));
ulDestroyString(script);
```

The strings returned by `ulViewEvaluateScript()` for both the result and the exception are borrowed strings owned by the View. Each call overwrites an internal buffer, so you must never destroy them. Copy them with `ulCreateStringFromCopy()` to keep them longer. See [Keeping a Borrowed Value](/docs/2.0/c-api-conventions#content-keeping-a-borrowed-value).

To evaluate scripts and receive typed JavaScript values instead of text, call `ulJSContextEvaluate()`. It takes a `ULJSContext` and returns an owned `ULJSValue` handle that you must release with `ulDestroyJSValue()`.

## Exposing a Function

To expose a native function to page script, register a callback with `ulJSAPIBindFunction()`, attach the API to your View, and keep the `ULJSAPI` handle alive for as long as pages need it.

```c
#include <Ultralight/CAPI.h>

static ULJSAPI g_api = NULL;

/* app.add(...numbers) */
static ULJSValue OnAdd(void* user_data, ULJSContext ctx, ULJSValue this_value,
                       const ULJSValue* args, size_t argc,
                       ULJSValue* exception) {
  (void)user_data;
  (void)this_value;
  double sum = 0;
  for (size_t i = 0; i < argc; i++) {
    if (ulJSValueGetType(args[i]) != kULJSType_Number) {
      *exception = ulCreateJSMarshalError(ctx, "app.add", i + 1, NULL,
                                          "number", args[i]);
      return NULL;
    }
    double n = 0;
    ulJSValueToNumber(args[i], &n, NULL);
    sum += n;
  }
  return ulCreateJSValueNumber(ctx, sum);
}

void SetupApp(ULView view) {
  g_api = ulCreateJSAPI("app");
  ulJSAPIBindFunction(g_api, "add", OnAdd, NULL, NULL);
  ulJSAPISetConstantString(g_api, "version", "2.1.0");

  if (ulViewAttachJSAPI(view, g_api, kULJSAPIAttachFlags_None, NULL, 0)) {
    ULString url = ulCreateString("file:///app.html");
    ulViewLoadURL(view, url);
    ulDestroyString(url);
  }
}

void ShutdownApp(void) {
  ulDestroyJSAPI(g_api);  /* detaches the API from every View */
  g_api = NULL;
}
```

Page script accesses the bound properties and functions through the global object:

```js
app.version;          // "2.1.0"
app.add(2, 3);        // 5
app.add(2, 'three');
// TypeError: app.add: argument 2: expected number, got 'three'
```

Follow these rules for return values, errors, and cleanup:

- **Callbacks return an owned handle.** Return an owned `ULJSValue` handle, or `NULL` for `undefined`. To throw an error, store an owned error handle in `*exception` and return `NULL`.
- **`ulCreateJSMarshalError()` builds the standard argument `TypeError`.** It sets the error code to `"ULJS_BAD_ARG"`, allowing page script to check the code without parsing the message.
- **Free binding state in `destroy_user_data`.** This hook runs on the Renderer's thread once no page holds its functions, during the last `ulDestroyJSAPI()` call or a later `ulUpdate()`.

> 🚧 Validate Argument Count and Types
>
> The library performs no automatic count or type validation on callback arguments, so reading `args[1]` when the page passed only one argument reads past the array. Conversion functions like `ulJSValueToNumber()` and `ulJSValueToString()` coerce values using JavaScript rules and can execute page script methods like `valueOf()`. Always verify `argc` and check argument types with `ulJSValueGetType()` before reading or converting values.

### Throwing an Error with a Code

To throw an error with a custom error code, set a `"code"` property on the error object before assigning it to `*exception`.

```c
static ULJSValue OnOpen(void* user_data, ULJSContext ctx, ULJSValue this_value,
                        const ULJSValue* args, size_t argc,
                        ULJSValue* exception) {
  (void)user_data;
  (void)this_value;
  if (argc < 1 || ulJSValueGetType(args[0]) != kULJSType_String) {
    ULJSValue error = ulCreateJSError(ctx, kULJSErrorType_TypeError,
                                      "path must be a string");
    ULJSValue code = ulCreateJSValueStringFromCString(ctx, "APP_BAD_PATH");
    ulJSObjectSetProperty(error, "code", code, kULJSPropertyAttributes_None,
                          NULL);
    ulDestroyJSValue(code);
    *exception = error;
    return NULL;
  }
  return ulCreateJSValueBoolean(ctx, true);
}
```

Setting the `"code"` property lets page script check `error.code` directly without parsing the message. For underlying concepts, see [Throwing JavaScript Errors from Native Code](/docs/2.0/extending-javascript-with-native-api#content-throwing-javascript-errors-from-native-code).

### Binding Properties, Constants, and Events

#### Live Properties

To bind a live property that runs getter and setter callbacks, call `ulJSAPIBindProperty()`. Passing `NULL` for the setter makes the property read-only. If your setter callback returns `false`, the assignment throws a `TypeError`.

#### Constant Values

To expose constant values that page script cannot modify, call `ulJSAPISetConstantNumber()`, `ulJSAPISetConstantBoolean()`, `ulJSAPISetConstantString()`, or `ulJSAPISetConstantJSON()`.

#### Emitting Events

To emit an event to page script from any thread, call `ulJSAPIEmitEventJSON()` with arguments formatted as a JSON array string.

```c
ulJSAPIEmitEventJSON(g_api, "saved", "[\"slot1.dat\"]");
```

The renderer delivers the event to each page during a later `ulUpdate()` call— see [Emitting Events](/docs/2.0/emitting-events) for how page script subscribes to them.

### Keeping a Page Callback

To store a JavaScript callback passed from page script, verify that the argument is callable, retain it with `ulCreateJSValueRef()`, and verify that the page is alive before invoking it later.

```c
#include <Ultralight/CAPI.h>

static ULJSValue g_on_score = NULL;

/* app.onScore(callback) */
static ULJSValue OnSetScoreListener(void* user_data, ULJSContext ctx,
                                    ULJSValue this_value,
                                    const ULJSValue* args, size_t argc,
                                    ULJSValue* exception) {
  (void)user_data;
  (void)this_value;
  if (argc < 1 || !ulJSValueIsCallable(args[0])) {
    *exception = ulCreateJSMarshalError(ctx, "app.onScore", 1, NULL,
                                        "function", argc ? args[0] : NULL);
    return NULL;
  }
  ulDestroyJSValue(g_on_score);
  g_on_score = ulCreateJSValueRef(args[0]);
  return NULL;
}

/* Later, on the Renderer's thread: */
void ReportScore(double score) {
  if (!ulJSValueIsAlive(g_on_score)) {  /* its page is gone */
    ulDestroyJSValue(g_on_score);
    g_on_score = NULL;
    return;
  }
  ULJSContext ctx = ulJSValueGetContext(g_on_score);
  ULJSValue arg = ulCreateJSValueNumber(ctx, score);
  ulDestroyJSValue(ulJSFunctionCall(g_on_score, NULL, &arg, 1, NULL));
  ulDestroyJSValue(arg);
  ulDestroyJSContext(ctx);
}
```

A retained function handle does not keep the page alive. Always call `ulJSValueIsAlive()` before invoking the callback— if the page navigated away, destroy the handle and discard it.

Destroying a handle whose page is gone remains safe and necessary to prevent memory leaks.

## Exposing Async Functions

To expose an asynchronous function that returns a Promise to the page, bind a callback with `ulJSAPIBindAsyncFunction()`.

```c
#include <Ultralight/CAPI.h>
#include <stdlib.h>

typedef struct {
  ULJSPromiseResolver resolver;
  ULJSContext ctx;  /* the completion callback gets no context */
  char path[256];
  bool ok;
} SaveJob;

/* A worker thread: no JavaScript here. */
static void SaveWork(void* user_data) {
  SaveJob* job = (SaveJob*)user_data;
  job->ok = job->path[0] != '\0';  /* write the file here */
}

/* The Renderer's thread, during a later ulUpdate(). */
static void SaveComplete(void* user_data) {
  SaveJob* job = (SaveJob*)user_data;
  if (job->ok) {
    ULJSValue result = ulCreateJSValueBoolean(job->ctx, true);
    ulJSPromiseResolverResolve(job->resolver, result);
    ulDestroyJSValue(result);
  } else {
    ULJSValue error = ulCreateJSError(job->ctx, kULJSErrorType_Error,
                                      "disk full");
    ulJSPromiseResolverReject(job->resolver, error);
    ulDestroyJSValue(error);
  }
}

/* Runs once, even when the work is discarded. */
static void FreeSaveJob(void* user_data) {
  SaveJob* job = (SaveJob*)user_data;
  ulDestroyJSPromiseResolver(job->resolver);  /* rejects if never settled */
  ulDestroyJSContext(job->ctx);
  free(job);
}

/* ulJSAPIBindAsyncFunction(g_api, "save", OnSave, NULL, NULL); */
static void OnSave(void* user_data, ULJSContext ctx, ULJSValue this_value,
                   const ULJSValue* args, size_t argc,
                   ULJSPromiseResolver resolver) {
  (void)user_data;
  (void)this_value;
  SaveJob* job = (SaveJob*)calloc(1, sizeof(SaveJob));
  job->resolver = resolver;
  job->ctx = ulCreateJSContextRef(ctx);
  size_t len = 0;
  if (argc > 0 && ulJSValueGetType(args[0]) == kULJSType_String &&
      ulJSValueGetUTF8(args[0], job->path, sizeof(job->path) - 1, &len,
                       NULL))
    job->path[len] = '\0';
  ulJSRunOnWorker(SaveWork, SaveComplete, job, FreeSaveJob);
}
```

The page receives a Promise immediately, while your callback receives an owned `ULJSPromiseResolver` handle to settle it. For underlying concepts, see [Async Callbacks](/docs/2.0/async-callbacks).

- **Retain a context reference for the completion callback.** The `ulJSRunOnWorker()` completion callback receives only `user_data`, so store an owned context reference from `ulCreateJSContextRef()` in your job struct.
- **Free job state in `destroy_user_data`.** This callback still runs if a Renderer shutdown discards queued work.
- **Settle a Promise from another thread with `ulJSPromiseResolverComplete()`.** It invokes a callback on the Renderer's thread during a later `ulUpdate()` to create the value.
- **Run code without a Promise using `ulJSContextPostTask()`.** It runs during a later `ulUpdate()` and is skipped if the page is gone.

> 🚧 Settle and Destroy Resolvers Carefully
>
> Call `ulDestroyJSPromiseResolver()` exactly once as the resolver's last use. Failing to destroy it leaks memory and leaves the page waiting forever, while destroying an unsettled resolver rejects the Promise with an Error. Call `ulJSPromiseResolverResolve()` and `ulJSPromiseResolverReject()` on the Renderer's thread only— from any other thread, settle the Promise with `ulJSPromiseResolverComplete()`.

### Waiting on a Page Promise

To wait for a Promise from the page to settle, call `ulJSPromiseThen()`. Your callback runs on the Renderer's thread during a later `ulUpdate()` when the Promise settles.

If `result` is `NULL` and `rejected` is `true`, the page went away before the Promise settled rather than page script rejecting it.

## Exposing Native Classes

To expose a native class to page script, define it with `ulCreateJSClass()`, register callbacks for its constructor, methods, and properties, and register it with `ulJSAPIRegisterClass()`— see [Exposing Native Classes](/docs/2.0/exposing-native-classes) for the underlying concepts.

In C, instance ownership depends on how you create the wrapper rather than C++ smart pointer types.

| How Wrapper Is Created | Owner | Lifetime Rule |
| :--- | :--- | :--- |
| `new` in page script (returned by your constructor callback) | The wrapper | The class destructor runs after garbage collection or when the page is gone, during a later `ulUpdate()`. |
| `ulCreateJSObjectWithClass()` with `adopt` set to `true` | The wrapper | The class destructor runs after garbage collection or when the page is gone, during a later `ulUpdate()`. |
| `ulCreateJSObjectWithClass()` with `adopt` set to `false` | You | You must keep the instance alive until its page is gone, or detach it before freeing. |
| `ulCreateJSObjectWithClassHolder()` | Your holder | The holder's destroy callback frees the instance. |

- **Set a class destructor for owned instances.** If you don't set one with `ulJSClassSetDestructor()`, owned instances are never destroyed.
- **Never adopt the same instance pointer twice.** If you adopt it into two owning wrappers (such as across two contexts or two classes), the library destroys it twice.
- **Retain your `ULJSClass` handle.** Functions that wrap, detach, or look up instances later take the class definition.

### Closing an Instance Early

To release scarce native resources before garbage collection runs, detach the instance with `ulJSObjectDetachInstance()`.

```c
#include <Ultralight/CAPI.h>
#include <stdio.h>

static ULJSClass g_log_class = NULL;  /* kept after ulJSAPIRegisterClass() */

/* The class destructor (ulJSClassSetDestructor()). */
static void LogFileDestroy(void* user_data, void* instance) {
  (void)user_data;
  fclose((FILE*)instance);
}

/* logFile.close() */
static ULJSValue LogFileClose(void* user_data, ULJSContext ctx,
                              void* instance, ULJSValue this_value,
                              const ULJSValue* args, size_t argc,
                              ULJSValue* exception) {
  (void)user_data;
  (void)ctx;
  (void)instance;
  (void)args;
  (void)argc;
  (void)exception;
  void* file = ulJSObjectDetachInstance(this_value, g_log_class, NULL);
  if (file)
    LogFileDestroy(NULL, file);  /* the destructor won't run for it now */
  return NULL;
}
```

Subsequent calls to methods or properties on a detached wrapper throw a `TypeError` with code `ULJS_DETACHED`. For general patterns, see [Closing an Instance Early](/docs/2.0/exposing-native-classes#content-closing-an-instance-early).

## Sharing Binary Data

To share native bytes with page script without copying, wrap your memory in a `ULBuffer` and pass it to `ulCreateJSArrayBufferFromBuffer()`— see [Binary Data](/docs/2.0/passing-data-across-the-bridge#content-binary-data) for the underlying concepts.

```c
#include <Ultralight/CAPI.h>
#include <stdlib.h>

static void FreePixels(void* user_data, void* data) {
  (void)user_data;
  free(data);
}

ULJSValue SharePixels(ULJSContext ctx, void* pixels, size_t size) {
  ULBuffer buffer = ulCreateBuffer(pixels, size, NULL, FreePixels);
  ULJSValue array_buffer = ulCreateJSArrayBufferFromBuffer(ctx, buffer);
  ulDestroyBuffer(buffer);  /* the ArrayBuffer keeps its own reference */
  return array_buffer;
}
```

Remember these rules about buffer lifetime and cleanup:

- **The destruction callback runs when the last reference is released.** This typically happens during a later `ulUpdate()`, after the `ArrayBuffer` is garbage-collected or detached.
- **Detach the buffer before freeing native memory.** Call `ulJSArrayBufferDetach()` to disconnect the page from memory before you free it in native code.
- **Accessing buffer bytes prevents detaching.** Once you call `ulJSArrayBufferGetBytes()`, `ulJSArrayBufferGetBuffer()`, or `ulJSTypedArrayGetInfo()`, neither native code nor the page can detach the `ArrayBuffer`— any later call to `ulJSArrayBufferDetach()` returns `false`.
- **Page-owned bytes are freed when the page goes away.** This happens even while you hold a pointer or `ULBuffer` handle— copy them with `ulCreateBufferFromCopy()` to keep them longer.

## Using JavaScriptCore Directly

To call JavaScriptCore API functions directly, call `ulViewLockJSContext()` to acquire the page's underlying `JSContextRef`. This locks the JavaScript VM for the current thread, so pair every call with `ulViewUnlockJSContext()`— see [Using JavaScriptCore Directly](/docs/2.0/using-javascriptcore-directly).

To convert between DOM element handles and their JavaScript object wrappers, call `ulDOMElementGetJSValue()` and `ulDOMElementFromJSValue()` from `<Ultralight/CAPI/CAPI_DOMElement.h>`. Both functions return owned handles that you must destroy when finished. For details, see [DOM Access in C](/docs/2.0/c-dom-access).
