docs

Renderer in C

Configure and create the renderer, manage sessions, and run the frame loop in C.

On this page

The C API exposes the renderer singleton, library configuration, sessions, and the frame loop through opaque handles. It supplements Creating the Renderer, Sessions and Site Data, and 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.

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.

Creating the Renderer

Register your platform handlers on the platform singleton before creating the renderer— see Platform Handlers in C.

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. 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.

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.

To coordinate animations with your display's refresh timing, call ulRefreshDisplayWithTimestamp() instead of ulRefreshDisplay(). See Updating and Rendering.

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:

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).

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.