The C API exposes the renderer singleton, library configuration, sessions, and the frame loop through opaque handles. It supplements [Creating the Renderer](/docs/2.0/creating-the-renderer), [Sessions and Site Data](/docs/2.0/sessions-and-site-data), and [Updating and Rendering](/docs/2.0/updating-and-rendering)— read those guides first for the core concepts.

For shared rules on memory ownership, callbacks, threading, and `NULL` handles, see [C API Conventions](/docs/2.0/c-api-conventions).

## Differences from C++

The C API provides C-style functions and opaque handles for renderer setup, sessions, and task scheduling.

| C++ Feature | C API Equivalent | How It Differs |
| :--- | :--- | :--- |
| `Platform::instance().set_config()` and `Renderer::Create()` | `ulCreateRenderer(config)` | Pass the configuration directly when creating the renderer, with no platform setter. |
| `Config` struct | `ULConfig` and `ulConfigSet*()` | Configure options through write-only setters, one per field. |
| `RefPtr<Renderer>` | `ULRenderer` | Release the handle manually with `ulDestroyRenderer()`. |
| `App::renderer()` | `ulAppGetRenderer()` | Returns a borrowed handle owned by the app. |
| `Renderer::CreateSession()` | `ulCreateSession()` | Returns an owned handle you must destroy. |
| `Renderer::default_session()` | `ulDefaultSession()` | Returns a borrowed handle owned by the renderer. |
| `Renderer::PostTask()` with a lambda | `ulRendererPostTask()` | Takes a function pointer, `user_data`, and a destroy hook. |
| `GCListener` and `Renderer::Recycle()` | `ulRendererSetGCRequestGCCallback()` and `ulRecycleEx()` | Available in the Pro edition and higher (`UL_HAS(GC_LISTENER)`)— see [Managing Memory](/docs/2.0/managing-memory#content-controlling-the-collector). |

## Creating the Renderer

Register your platform handlers on the platform singleton before creating the renderer— see [Platform Handlers in C](/docs/2.0/c-platform-handlers).

To set configuration options, allocate a configuration handle with `ulCreateConfig()` and call the `ulConfigSet*()` setters. Pass the configuration to `ulCreateRenderer()`, which returns `NULL` if initialization fails. The renderer copies the configuration immediately, so you should call `ulDestroyConfig()` right after creating the renderer.

For a complete setup example, see the C tab under Configuring the Library in [Creating the Renderer](/docs/2.0/creating-the-renderer#content-configuring-the-library). That page also lists all configuration options and their default values.

### Using AppCore

When using AppCore, calling `ulCreateApp()` creates and manages the renderer for you. Passing `NULL` for the configuration uses default options. Call `ulAppGetRenderer()` to retrieve the renderer handle— see [AppCore in C](/docs/2.0/c-appcore).

## Creating View Sessions

To give a view its own isolated storage, create a session with `ulCreateSession()` and pass it to `ulCreateView()`.

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

static ULView player_view = NULL;

void CreatePlayerView(void) {
  ULString name = ulCreateString("player-2");
  ULSession session = ulCreateSession(renderer, true, name);
  ulDestroyString(name);

  // NULL selects the default ViewConfig.
  player_view = ulCreateView(renderer, 800, 600, NULL, session);

  // The View keeps its own reference to the session.
  ulDestroySession(session);
}
```

Passing `NULL` for the session in `ulCreateView()` assigns the view to the default persistent session. You can also fetch the default session handle directly by calling `ulDefaultSession()`.

A session that is not attached to any view is freed when you call `ulDestroySession()` on its handle.

> 🚧 Never Destroy Borrowed Handles
>
> Never call `ulDestroySession()` on the handle returned by `ulDefaultSession()`, or `ulDestroyRenderer()` on the handle returned by `ulAppGetRenderer()`. The library owns both handles and cleans them up automatically.

## Running the Frame Loop

The frame loop calls `ulUpdate()`, `ulRefreshDisplay()`, and `ulRender()` in the same order as C++. For what each function does, see the C tab in [Updating and Rendering](/docs/2.0/updating-and-rendering#content-calling-the-renderer-each-frame).

To coordinate animations with your display's refresh timing, call `ulRefreshDisplayWithTimestamp()` instead of `ulRefreshDisplay()`. See [Updating and Rendering](/docs/2.0/updating-and-rendering#content-passing-presentation-timestamps).

## Posting Work from Other Threads

To dispatch work from a worker thread to the Renderer's thread, call `ulRendererPostTask()`.

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

typedef struct {
  int score;
} ScoreUpdate;

static void ApplyScore(void* user_data) {
  ScoreUpdate* update = user_data;

  // On the Renderer's thread, so View calls are safe here.
  ShowScore(update->score);
}

// Called on a worker thread.
void OnWorkerFinished(int score) {
  ScoreUpdate* update = malloc(sizeof(ScoreUpdate));
  update->score = score;

  // The library calls free(update) after ApplyScore() runs.
  ulRendererPostTask(renderer, ApplyScore, update, free);
}
```

Here is how posted tasks run and clean up their data:

- **You can call `ulRendererPostTask()` from any thread.** For threading rules, see [C API Conventions](/docs/2.0/c-api-conventions#content-threading-rules).
- **Tasks run during the next `ulUpdate()`.** They run on the Renderer's thread in posting order.
- **Allocate `user_data` on the heap.** The task runs after your worker function returns, so stack memory is invalid.
- **The destroy hook runs once.** It runs after the task finishes. If you destroy the renderer first, `ulDestroyRenderer()` calls the destroy hook without running the task.

### Posting Delayed Tasks

To schedule work after a delay, call `ulRendererPostDelayedTask()`.

Pass the delay in milliseconds. The task runs during the first `ulUpdate()` at or after the deadline— delivery waits while you aren't calling `ulUpdate()` (such as when your application is paused).

- **Prefer `ulAppPostTask()` in AppCore applications.** It wakes the app loop— see [AppCore in C](/docs/2.0/c-appcore).
- **Call `ulJSContextPostTask()` for a JavaScript context.** It dispatches tasks directly to that context— see [JavaScript in C](/docs/2.0/c-javascript).

## Shutting Down the Renderer

When shutting down the library, destroy your handles in this order:

1. Every `ULView` you created, by calling `ulDestroyView()`.
2. Any `ULSession` handles you still hold, by calling `ulDestroySession()`.
3. The renderer singleton, by calling `ulDestroyRenderer()`.

For a complete teardown example, see the C tab in [Your First Game UI](/docs/2.0/your-first-game-ui).
