docs

Platform Handlers in C

Configure custom platform handlers for file loading, font management, clipboard, and logging in C.

On this page

You can customize how the renderer loads files, resolves fonts, interacts with the clipboard, and outputs audio by configuring platform handlers in C. Each platform handler is a C struct of function pointers that you populate and pass to a ulPlatformSet*() setter function.

Read Setting Up the Platform first to learn which handlers the library requires and the order to install them. All handlers follow the shared memory and callback conventions in C API Conventions. The GPU driver uses a dedicated interface covered in GPU Driver in C.

Differences from C++

The C API replaces C++ handler classes with structs of function pointers.

C++ Feature C API Equivalent What Changes
Subclass FileSystem and call Platform::instance().set_file_system() Populate ULFileSystem and call ulPlatformSetFileSystem() The library copies the struct.
You own the handler object The library owns its copy of the struct Callback functions must stay valid until the Renderer or App is destroyed.
Access state through this user_data on three structs only Handlers without user_data share file-scope state (see the warning below).
Return RefPtr<Buffer>, RefPtr<FontFile>, RefPtr<ClipboardData>, or String Return an owned handle The library consumes and destroys the returned handle.
Stock handlers from <AppCore/Platform.h> ulEnable*() functions from <AppCore/CAPI.h> Call one function per stock handler.
#include <Ultralight/platform/AudioOutput.h> Nothing extra to include Keep the #if UL_HAS(MEDIA) guard.

Installing Handlers

Populate each handler struct and call its setter before creating the Renderer— you can mix custom handlers and AppCore's stock handlers freely.

C
#include <AppCore/CAPI.h>

void InstallFileSystem(void);  // our ULFileSystem (see Serving Files)

ULRenderer CreateRenderer(void) {
  InstallFileSystem();
  ulEnablePlatformFontLoader();
  ulEnablePlatformClipboard();
#if UL_HAS(MEDIA)
  ulEnablePlatformAudioOutput();
#endif

  ULConfig config = ulCreateConfig();
  ULRenderer renderer = ulCreateRenderer(config);
  ulDestroyConfig(config);
  return renderer;
}

Stock ulEnable*() functions ship in AppCore, so you must link against the AppCore library to use them. The C tabs in Setting Up the Platform show how to enable the platform font loader, file system, and logger.

When you call ulCreateApp(), the library automatically installs stock handlers for any handler you haven't set. For available configuration options, see Renderer in C.

Handler Structs and Setters

Every platform interface has a corresponding C struct and setter function.

Handler Struct Setter Function C++ Guide
ULFileSystem ulPlatformSetFileSystem() Custom File System
ULFontLoader ulPlatformSetFontLoader() Custom Font Loading
ULClipboard ulPlatformSetClipboard() Clipboard Integration
ULLogger ulPlatformSetLogger() Logging and Console Messages
ULSurfaceDefinition ulPlatformSetSurfaceDefinition() Render Surfaces
ULThreadFactory ulPlatformSetThreadFactory() Custom Threads
ULAudioOutput ulPlatformSetAudioOutput() Media and Audio (Pro edition and up)
ULProfiler ulPlatformSetProfiler() Profiling and Tracing (Pro edition and up)
ULGPUDriver ulPlatformSetGPUDriver() GPU Driver in C

Struct Lifetimes and Initialization

Keep these rules in mind when setting up and managing handler structs:

📘 Set Handlers Before Creating the Renderer

Set each handler once before calling ulCreateRenderer() or ulCreateApp(). Replacing a handler later deletes the previous handler, which the library may still be using.

🚧 Guard Shared State Across Threads

ULFileSystem, ULFontLoader, and ULLogger don't provide a user_data pointer. Their callbacks run concurrently on multiple threads (including file loaders, Web Workers, and logging threads). Guard any file-scope state they share with a lock.

Returning Handles from Callbacks

The library takes ownership of every handle your callbacks return and destroys it when finished— never destroy a handle after returning it. Any argument passed into your callback is borrowed and stays valid only for the duration of the call. For full ownership rules, see C API Conventions.

Callback Return Type What NULL Means
open_file ULBuffer The file cannot be opened.
load ULFontFile The font cannot be loaded, and the renderer falls back to another font.
read (clipboard) ULClipboardData The clipboard is empty.
get_file_mime_type, get_file_charset, get_fallback_font, get_fallback_font_for_characters ULString Never return NULL— return "application/unknown" or "utf-8" when unsure.

Serving Files

Implement the four callbacks in ULFileSystem to serve page assets and renderer resources from memory or your own asset pipeline.

C
#include <Ultralight/CAPI.h>

static bool OnFileExists(ULString path) {
  (void)path;
  // Pseudo-code, look ulStringGetData(path) up in your asset store here.
  return false;
}

static ULString OnGetFileMimeType(ULString path) {
  (void)path;
  // Pseudo-code, pick the MIME type from the path's extension here.
  return ulCreateString("application/unknown");
}

static ULString OnGetFileCharset(ULString path) {
  (void)path;
  return ulCreateString("utf-8");
}

static void OnReleaseAsset(void* user_data, void* data) {
  (void)user_data;
  (void)data;
  // Pseudo-code, unmap the asset's bytes here (this can run on any thread).
}

static ULBuffer OnOpenFile(ULString path) {
  (void)path;
  void* data = NULL;
  size_t size = 0;
  // Pseudo-code, map ulStringGetData(path) from your asset store here.
  if (!data)
    return NULL;
  return ulCreateBuffer(data, size, NULL, OnReleaseAsset);
}

void InstallFileSystem(void) {
  ULFileSystem file_system = {
      .file_exists = OnFileExists,
      .get_file_mime_type = OnGetFileMimeType,
      .get_file_charset = OnGetFileCharset,
      .open_file = OnOpenFile,
  };
  ulPlatformSetFileSystem(file_system);
}

Calling ulCreateBuffer() wraps existing memory without copying data. The destruction callback you pass to ulCreateBuffer() runs when the library releases the buffer, which can happen later and on another thread.

If source memory won't stay valid until the buffer is released (such as when reusing a scratch block), call ulCreateBufferFromCopy() instead so the library manages its own copy.

For details on MIME types, 16-byte alignment, and bundling the SDK's resources/ folder, see Custom File System.

Loading Fonts

Implement ULFontLoader to resolve font descriptions to font files on disk or in memory.

C
#include <Ultralight/CAPI.h>
#include <string.h>

static ULString OnGetFallbackFont(void) {
  return ulCreateString("Rocket Sans");
}

static ULString OnGetFallbackFontForCharacters(ULString characters,
                                               int weight, bool italic) {
  (void)characters;
  (void)weight;
  (void)italic;
  return ulCreateString("Rocket CJK");
}

static ULFontFile OnLoadFont(ULString family, int weight, bool italic) {
  (void)weight;
  (void)italic;
  const char* name = ulStringGetData(family);

  if (strcmp(name, "Rocket Sans") == 0) {
    const void* bytes = NULL;
    size_t size = 0;
    // Pseudo-code, fetch the TTF bytes for this weight here.
    ULBuffer buffer = ulCreateBufferFromCopy(bytes, size);
    ULFontFile font = ulFontFileCreateFromBuffer(buffer);
    ulDestroyBuffer(buffer);  // the font file holds its own reference
    return font;
  }

  if (strcmp(name, "Rocket CJK") == 0) {
    ULString path = ulCreateString("fonts/rocket-cjk.ttf");
    ULFontFile font = ulFontFileCreateFromFilePath(path);
    ulDestroyString(path);
    return font;
  }

  return NULL;
}

void InstallFontLoader(void) {
  ULFontLoader font_loader = {
      .get_fallback_font = OnGetFallbackFont,
      .get_fallback_font_for_characters = OnGetFallbackFontForCharacters,
      .load = OnLoadFont,
  };
  ulPlatformSetFontLoader(font_loader);
}

🚧 Destroy Font Buffers but Not Font Files

When creating a font file with ulFontFileCreateFromBuffer(), destroy your ULBuffer handle immediately— the font file retains its own reference, so skipping ulDestroyBuffer() leaks the buffer handle. Never destroy the returned ULFontFile handle, because the library takes ownership and frees it.

ulFontFileCreateFromFilePath() takes an OS file path directly. The renderer does not route this path through your ULFileSystem.

For font collection files (such as .ttc files), call ulFontFileCreateFromBufferWithFaceIndex() or ulFontFileCreateFromFilePathWithFaceIndex(). Pass -1 for the face index to let the library pick the best match for the requested weight and style.

For font fallback rules and character coverage requirements, see Custom Font Loading.

Clipboard Integration

Implement ULClipboard to handle clipboard operations with the OS.

C
#include <Ultralight/CAPI.h>
#include <string.h>

static void OnClipboardClear(void* user_data) {
  (void)user_data;
  // Pseudo-code, empty the OS clipboard here.
}

static ULClipboardData OnClipboardRead(void* user_data) {
  (void)user_data;
  // Pseudo-code, fetch the OS clipboard's text here ("" when empty).
  const char* os_text = "";
  if (os_text[0] == '\0')
    return NULL;

  ULString text = ulCreateString(os_text);
  ULClipboardData data = ulCreateClipboardDataFromText(text);
  ulDestroyString(text);
  return data;
}

static void OnClipboardWrite(void* user_data, ULClipboardData data) {
  (void)user_data;
  // Pseudo-code, open and empty the OS clipboard here.
  size_t count = ulClipboardDataGetSize(data);
  for (size_t i = 0; i < count; ++i) {
    ULString type = ulClipboardDataGetTypeAt(data, i);
    ULString text = ulClipboardDataGetText(data, type);  // NULL for bytes
    if (text && strcmp(ulStringGetData(type), "text/plain") == 0) {
      // Pseudo-code, write ulStringGetData(text) as OS text here.
    }
    if (text)
      ulDestroyString(text);
    ulDestroyString(type);
  }
  // Pseudo-code, close the OS clipboard here.
}

void InstallClipboard(void* my_context) {
  ULClipboard clipboard = {
      .user_data = my_context,
      .clear = OnClipboardClear,
      .read = OnClipboardRead,
      .write = OnClipboardWrite,
  };
  ulPlatformSetClipboard(clipboard);
}

Follow these rules when working with clipboard callbacks and data:

For atomic clipboard writing rules and standard format types, see Clipboard Integration.

Other Platform Handlers

Audio Output

ULAudioOutput and ulPlatformSetAudioOutput() require the Pro edition or higher, so wrap any code using them in an #if UL_HAS(MEDIA) guard.

C
#if UL_HAS(MEDIA)
void InstallAudioOutput(void* mixer) {
  ULAudioOutput audio_output = {
      .user_data = mixer,
      .create_stream = OnCreateStream,
      .push_samples = OnPushSamples,
      .set_volume = OnSetVolume,
      .set_paused = OnSetPaused,
      .flush = OnFlush,
      .stop = OnStop,
      .destroy_stream = OnDestroyStream,
      // get_playback_position stays NULL: video follows the system clock.
  };
  ulPlatformSetAudioOutput(audio_output);
}
#endif

Leaving get_playback_position set to NULL causes the playback position to report 0— video then follows the system clock instead of the audio clock.

For audio stream lifecycle rules, threading considerations, and buffer backpressure, see Media and Audio.

Creating Threads

In ULThreadFactory, the create_thread callback receives the thread name as a plain const char* (which can be NULL). ULThreadHandle is an unsigned long long— on POSIX, cast a pthread_t through uintptr_t to store it. For which threads the renderer creates and what to report, see Custom Threads.