docs

Custom File System

Customize file loading by providing your own FileSystem.

On this page

You can provide a custom FileSystem to serve pages and assets directly from an engine asset system, a pack file, or memory. The library routes every file:/// URL and internal resource file through this handler.

The Default File System

When you call App::Create(), the library automatically installs a file system that reads from the OS, rooted at Settings::file_system_path (./assets/ by default).

Renderer::Create() does not install a file system— one is required, so you must provide one. You can call GetPlatformFileSystem() to use the default OS file system without creating an App.

To replace the default, set a custom file system on Platform before calling App::Create() or Renderer::Create()— see Setting Up the Platform.

Implementing the FileSystem Interface

Subclass FileSystem, override its four methods, and pass an instance to Platform::instance().set_file_system().

Every file path passed to these methods is relative— it is the path that follows the file:/// prefix.

Method Description
FileExists() Whether a file exists at the specified path.
GetFileMimeType() The MIME type of the file, typically determined by its extension (eg, text/html). If the type is unknown, return application/unknown.
GetFileCharset() The character encoding of a text file (eg, utf-8). If the encoding cannot be determined, return utf-8.
OpenFile() The file contents wrapped in a Buffer, or nullptr if the file cannot be opened.

🚧 Thread Safety Required

The library calls these methods from the Renderer's thread and from a background thread used to load file:/// URLs. Both threads can access the file system at the same time, so the implementation must be safe to call from multiple threads at once.

Returning File Data from OpenFile()

OpenFile() returns the contents of a file wrapped in a Buffer.

File data addresses should be aligned to 16-byte boundaries— the ICU data file requires this alignment.

Wrapping Existing Memory

To avoid copying file data into the renderer, call Buffer::Create() to wrap existing memory (eg, a memory-mapped file).

You can pass an optional destruction callback that runs when the library releases the buffer, letting you unmap or free the data.

Copying File Data

Call Buffer::CreateFromCopy() to have the library allocate and manage its own copy of the data.

Use this when the source memory will not stay valid until the buffer is released (eg, a scratch buffer reused for subsequent files), or when you cannot guarantee 16-byte alignment.

Example Implementation

The following example implements a file system that wraps memory-mapped files and copies data from scratch memory.

C++
#include <Ultralight/Ultralight.h>
#include <cstring>

using namespace ultralight;

///
/// Does the path end with the given extension?
///
static bool HasExtension(const String8& path, const char* ext) {
  size_t ext_len = strlen(ext);
  return path.length() >= ext_len &&
         strcmp(path.data() + path.length() - ext_len, ext) == 0;
}

///
/// Runs when the library releases a Buffer we created with Buffer::Create.
///
static void OnUnmapAsset(void* user_data, void* data) {
  // Pseudo-code, unmap the file.
  UnmapAsset(data);
}

class MyFileSystem : public FileSystem {
 public:
  bool FileExists(const String& file_path) override {
    // Pseudo-code, ask your asset store whether the file exists.
    return AssetExists(file_path.utf8().data());
  }

  String GetFileMimeType(const String& file_path) override {
    ///
    /// Decide by extension, and admit it when we can't tell.
    ///
    const String8& path = file_path.utf8();
    if (HasExtension(path, ".html"))
      return "text/html";
    if (HasExtension(path, ".css"))
      return "text/css";
    if (HasExtension(path, ".js"))
      return "text/javascript";
    if (HasExtension(path, ".png"))
      return "image/png";
    return "application/unknown";
  }

  String GetFileCharset(const String& file_path) override {
    ///
    /// Only matters for text files-- our assets are all UTF-8.
    ///
    return "utf-8";
  }

  RefPtr<Buffer> OpenFile(const String& file_path) override {
    const char* path = file_path.utf8().data();
    void* data = nullptr;
    size_t size = 0;

    ///
    /// Big files come out of our archive memory-mapped, so wrap the
    /// mapping without a copy and unmap it in the destruction callback.
    ///
    // Pseudo-code, map the file if the archive has it.
    if (MapAsset(path, &data, &size))
      return Buffer::Create(data, size, nullptr, OnUnmapAsset);

    ///
    /// Small files are read into a scratch block we reuse, so hand the
    /// library its own (16-byte aligned) copy instead.
    ///
    // Pseudo-code, read the file into scratch memory (false if missing).
    if (ReadAssetIntoScratch(path, &data, &size))
      return Buffer::CreateFromCopy(data, size);

    return nullptr;
  }
};

MyFileSystem file_system;

void InitPlatform() {
  ///
  /// Install our file system before creating the Renderer. (Ownership
  /// stays with us, so 'file_system' must outlive the Renderer.)
  ///
  Platform::instance().set_file_system(&file_system);
}

File Caching

The renderer reads the page's HTML file through OpenFile() on every load.

Files that the page references (such as stylesheets, scripts, and images) stay in a memory cache— later requests can use the cached copy without calling OpenFile().

This reuse can happen when you load the same page again with View::LoadURL(), or when the page requests an asset after it finishes loading.

How long a file stays cached is not fixed— the library periodically frees files that no loaded page uses, so reuse across page loads is likely but not guaranteed.

Calling View::Reload() bypasses the memory cache and re-reads every file through the file system.

Bypassing the Cache

To force the renderer to load a fresh copy of a file, append a query string to its URL:

HTML
<link rel="stylesheet" href="file:///hud.css?v=2">

Changing the URL creates a new cache entry— your FileSystem still receives the path without the query string.

The change takes effect immediately when a script on the page updates the URL, or the next time the page loads if you update the markup before calling View::LoadURL().

Serving Library Resources

The renderer loads its internal resources through the file system under the directory configured by Config::resource_path_prefix (resources/ by default)— see Creating the Renderer.

You provide the SDK's resources/ folder in its entirety— the library fetches these files through the file system:

For instructions on staging these files with an application, see Linking to the Library.

Missing Resource Errors

If icudt67l.dat cannot be loaded, the library logs a LogLevel::Fatal message and the process exits at startup.

If cacert.pem cannot be loaded, the library logs an error and all HTTPS requests fail.