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](/docs/2.0/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](/docs/2.0/c-api-conventions). The GPU driver uses a dedicated interface covered in [GPU Driver in C](/docs/2.0/c-gpu-driver).

## 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](/docs/2.0/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](/docs/2.0/c-renderer).

### 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](/docs/2.0/custom-filesystem) |
| `ULFontLoader` | `ulPlatformSetFontLoader()` | [Custom Font Loading](/docs/2.0/custom-font-loading) |
| `ULClipboard` | `ulPlatformSetClipboard()` | [Clipboard Integration](/docs/2.0/clipboard-integration) |
| `ULLogger` | `ulPlatformSetLogger()` | [Logging and Console Messages](/docs/2.0/logging-and-console-messages) |
| `ULSurfaceDefinition` | `ulPlatformSetSurfaceDefinition()` | [Render Surfaces](/docs/2.0/using-a-custom-surface) |
| `ULThreadFactory` | `ulPlatformSetThreadFactory()` | [Custom Threads](/docs/2.0/advanced-platform-hooks) |
| `ULAudioOutput` | `ulPlatformSetAudioOutput()` | [Media and Audio](/docs/2.0/media-and-audio) (Pro edition and up) |
| `ULProfiler` | `ulPlatformSetProfiler()` | [Profiling and Tracing](/docs/2.0/profiling-and-tracing) (Pro edition and up) |
| `ULGPUDriver` | `ulPlatformSetGPUDriver()` | [GPU Driver in C](/docs/2.0/c-gpu-driver) |

### Struct Lifetimes and Initialization

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

- **Setters copy the struct.** The library copies the struct by value, so you can pass a local variable.
- **Callbacks and user data must stay valid.** Keep your function pointers and any `user_data` pointer valid until the Renderer or App is destroyed.
- **The library never frees user data.** Platform handler structs don't provide a `destroy_user_data` hook, so you're responsible for cleaning up your state— see [C API Conventions](/docs/2.0/c-api-conventions#content-using-callbacks-and-user-data).
- **Zero-initialize handler structs.** Use designated initializers or `= {0}` so omitted fields default to `NULL` rather than garbage pointers. Optional callbacks set to `NULL` fall back safely.

> 📘 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](/docs/2.0/c-api-conventions#content-owned-and-borrowed-return-values).

| 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](/docs/2.0/custom-filesystem).

## 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](/docs/2.0/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:

- **User data reaches all three callbacks.** The `ULClipboard` struct passes its `user_data` pointer to `clear`, `read`, and `write`.
- **Build a payload to return from read.** `ulCreateClipboardDataFromText()` creates a payload containing a single `text/plain` entry. To support additional formats, call `ulCreateClipboardData()` and add entries with `ulClipboardDataSetText()` or `ulClipboardDataSetBytes()`.
- **The write payload is borrowed.** The payload expires when the `write` callback returns. Call `ulCreateClipboardDataRef()` to retain the payload past the call (such as for delayed rendering), and release it with `ulDestroyClipboardData()` when finished.
- **Getter functions return owned strings.** Calls like `ulClipboardDataGetTypeAt()` and `ulClipboardDataGetText()` return new `ULString` handles that you must destroy with `ulDestroyString()`. `ulClipboardDataGetText()` returns `NULL` when an entry contains binary bytes instead of text.
- **Callbacks run on the Renderer's thread.** The renderer invokes clipboard callbacks only on that thread.

For atomic clipboard writing rules and standard format types, see [Clipboard Integration](/docs/2.0/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](/docs/2.0/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](/docs/2.0/advanced-platform-hooks).
