Core Types
Manage object lifetimes, work with text strings, wrap raw buffers, and measure rectangles.
On this page
Ultralight uses a small set of foundational types across almost every C++ class and callback signature. Learning how these types manage memory, handle text, and store layout bounds lets you work with the renderer without guessing about lifetimes or ownership.
The table below summarizes the core types you will encounter throughout the API:
| Type | Description |
|---|---|
RefPtr / WeakPtr |
Reference-counting smart pointers that manage and observe object lifetimes. |
String |
Text container that stores strings in null-terminated UTF-8 format. |
Buffer |
Reference-counted container for raw byte data. |
Rect / IntRect |
Floating-point and integer rectangles defined by their edge coordinates. |
RefPtr
Ultralight shares core objects (such as Renderer, View, Bitmap, and Buffer) between your application and the engine. Each object stores its reference count internally— RefPtr<T> acts as a smart pointer that holds one reference to keep it alive.
Managing Object Lifetime
Factory functions return a new instance wrapped in a RefPtr. Copying the pointer increments the reference count, while resetting it, assigning nullptr, or letting it exit scope decrements the count. Testing the pointer in a conditional checks whether it holds a non-null object.
Creating, copying, and resetting a RefPtr controls the underlying object's lifetime:
RefPtr<Bitmap> bitmap =
Bitmap::Create(100, 100, BitmapFormat::BGRA8_UNORM_SRGB);
RefPtr<Bitmap> other = bitmap; // two references to one Bitmap
bitmap.reset(); // one reference left
other = nullptr; // none left: the Bitmap is destroyed
🚧 Do Not Delete Objects Directly
Never call
deleteon a library object. Let the enclosingRefPtrreset or fall out of scope so the object deallocates itself when its reference count drops to zero.
Passing Raw Pointers
Many functions throughout the library accept or return a raw pointer T*. These pointers represent borrowed references that do not modify the object's reference count.
Borrowing an Instance
When a function expects a raw pointer, call get() on your RefPtr to pass the underlying object without altering its reference count.
This helper extracts a raw pointer to pass to a function that borrows the Bitmap:
void Paint(Bitmap* bitmap);
void Draw(const RefPtr<Bitmap>& bitmap) {
if (bitmap)
Paint(bitmap.get());
}
Retaining a Callback Pointer
Callbacks often supply a raw pointer (such as View* caller) that is valid only for the duration of the callback. Because reference counts live inside the object itself, assigning this borrowed pointer to a RefPtr safely increments the count and keeps the object alive after the callback returns.
Assigning a callback's raw pointer to a member RefPtr preserves the View:
class PageTracker : public LoadListener {
public:
void OnDOMReady(View* caller, uint64_t frame_id, bool is_main_frame,
const String& url) override {
ready_view_ = caller; // adds a reference: the View stays alive
}
private:
RefPtr<View> ready_view_;
};
Observing with WeakPtr
WeakPtr<T> observes a reference-counted object without keeping it alive. In Ultralight, WeakPtr supports Bitmap instances only, and using it with other types produces a compile error.
Calling Lock() returns a strong RefPtr that keeps the object alive while held, or nullptr if the object has already been destroyed. Avoid checking Expired() because another thread could destroy the object immediately after the check— test the return value of Lock() instead.
You can observe a Bitmap with a WeakPtr and lock it before drawing:
WeakPtr<Bitmap> weak(bitmap);
// Later:
if (RefPtr<Bitmap> strong = weak.Lock())
Paint(strong.get());
String
Ultralight uses its own String class across its C++ interface rather than std::string because standard library layouts differ across compilers and platforms. The JavaScript and DOM bridge APIs remain the exception, accepting std::string directly.
Creating and Copying Strings
String stores text in null-terminated UTF-8 format. It implicitly constructs from string literals, concatenates text with + and +=, and creates an independent copy of the character data when copied.
Constructing, appending to, and copying a String uses familiar syntax:
String greeting = "Howdy!";
greeting += " Welcome back.";
String copy = greeting; // an independent copy of the text
Accessing UTF-8 Bytes
Call utf8().data() to get a pointer to the null-terminated UTF-8 byte array. The utf8().length() method returns the buffer size in bytes rather than the number of Unicode characters.
You can inspect the underlying UTF-8 buffer and its byte length directly:
const char* text = greeting.utf8().data(); // null-terminated UTF-8
size_t bytes = greeting.utf8().length(); // bytes, not characters
Converting to Other Encodings
When interfacing with platform APIs that require wide characters (such as Windows system functions), call utf16() or utf32(). These methods convert the text and return an independent copy as a String16 or String32.
Calling utf16() produces a UTF-16 copy for wide-character platform APIs:
String16 wide = greeting.utf16(); // a UTF-16 copy for a wide-char API
Interoperating with std::string
Including the optional <Ultralight/StringSTL.h> header (which requires C++17) provides conversion helpers between String and standard library string types. The overloaded Convert() function converts bidirectionally based on the argument passed, while Convert<std::string_view>() creates a non-owning view of the internal bytes without allocating memory. If you work without this header, construct a String from std::string using String(s.data(), s.size()).
The conversion utilities translate between standard library strings and Ultralight strings:
#include <Ultralight/StringSTL.h>
#include <string>
#include <string_view>
using namespace ultralight;
void Cross(const String& title) {
std::string std_title = Convert(title);
String back = Convert(std_title);
std::string_view view = Convert<std::string_view>(title); // no copy
}
🚧 String Lifetime with String Views
A
std::string_viewcreated withConvert<std::string_view>()points directly into the buffer of the sourceString. ThatStringmust remain valid and unmodified for as long as you read the view.
Buffer
Buffer represents a reference-counted container for raw byte data, held and passed as a RefPtr<Buffer>. Ultralight uses it for loading files, passing font and image assets, and transferring binary payloads.
Wrapping Existing Memory
Buffer::Create() wraps existing memory without copying it. Your application retains responsibility for the allocation— the memory must stay valid and untouched until the library releases its last reference, at which point Ultralight invokes your destruction callback so you can free the memory.
Passing a destruction callback to Buffer::Create() frees your custom buffer when the library finishes with it:
#include <Ultralight/Buffer.h>
#include <cstdlib>
using namespace ultralight;
RefPtr<Buffer> WrapBytes(void* bytes, size_t size) {
return Buffer::Create(bytes, size, nullptr,
[](void* user_data, void* data) { free(data); });
}
Copying Raw Bytes
If you cannot guarantee that your memory outlives the buffer, use Buffer::CreateFromCopy(). This allocates a new internal block, copies the bytes into it, and frees the allocated memory automatically when the Buffer instance is destroyed.
Calling Buffer::CreateFromCopy() creates an independent owned buffer:
RefPtr<Buffer> copy = Buffer::CreateFromCopy(bytes, size);
Rect and IntRect
Ultralight defines two rectangle types for layout, painting, and hit testing: Rect uses floating-point coordinates, while IntRect uses integers. For instance, Surface::dirty_bounds() returns an IntRect for pixel-aligned drawing areas, whereas Window::window_control_bounds() returns a Rect for subpixel UI boundaries.
Defining Rectangle Edges
Both rectangle types store four boundary coordinates: left, top, right, and bottom, rather than an origin and dimensions. When defining a Rect from an origin and size, call Rect::FromXYWH(), which calculates the right and bottom bounds for you.
The helper constructor initializes a Rect from position and dimensions:
Rect button = Rect::FromXYWH(10, 10, 120, 32);
// button.left = 10, button.top = 10, button.right = 130, button.bottom = 42
Measuring and Hit Testing
Helper methods calculate dimensions and perform spatial queries. You can retrieve width() and height(), check whether a rectangle contains a point or another rectangle using Contains(), or test for overlap with Intersects().
Member methods query dimensions and test point containment:
float width = button.width(); // 120
bool hit = button.Contains(Point(40, 20)); // true
Checking Bounds and Area
Checking whether a rectangle has content involves two distinct methods. IsEmpty() returns true only when all four boundary coordinates equal zero, whereas IsValid() returns true only when both width() and height() are strictly positive.
Evaluating a degenerate rectangle highlights the difference between emptiness and validity:
Rect line = Rect::FromXYWH(50, 50, 0, 32);
line.IsEmpty(); // false: the edges aren't all zero
line.IsValid(); // false: it has no area
🚧 Test Visible Area with IsValid
A rectangle with zero width or height is not necessarily empty. Because its coordinate offsets are non-zero,
IsEmpty()returnsfalse. Always callIsValid()to check whether a rectangle encloses a usable area.