libplaybit API Conventions
This document contains general information on how to use the libplaybit APIs and the user interface library. Some sections are only relevant to developers using lower-level programming languages such as C; the bindings for languages like Python take care of implementation details like objects and callbacks.
Strings
All strings are assumed to be UTF-8, although most API functions do not verify them. When receiving strings from the system (either as a return value from an API function or as a parameter to a callback), do not assume the string will be valid UTF-8.
Functions may take a string as input through the PBStrSlice
structure. This contains two fields. The first is const char *ptr;
pointing to the first character of the string. The second is size_t len;
giving the number of bytes in the string. The receiving function must not (a) modify the string (b) cause the string to be accessed later after it returns (c) read before the start of the string or after the end (d) deallocate the string.
A PBStrSlice
structure can be produced through the PB_STR()
macro.
- Passing a
const char *
followed by a length sets the two fields as provided. - Passing a single
const char *
to the macro interprets it as a zero-terminated "C string" and callsstrlen
to set thelen
field. - Passing a
PBStr
orPBBuf
(to be discussed below) decays naturally to thePBStrSlice
type.
Functions may return a string through the PBStr
structure. This passes ownership of the string to the outer function, the caller. The PBStr
structure has two fields, char *ptr;
and size_t len;
. ptr
points to the first character of the string and len
gives the number of bytes in the string. ptr
must be allocated with PBHeapAllocate
and deallocated with PBHeapFree
. After returning a PBStr
, only the caller may access it; furthermore, the caller is responsible for deallocating it.
Here is an example of correctly passing and returning a string.
PBStr MakeUpperCase(PBStrSlice input) {
PBStr output;
output.ptr = PBHeapAllocate(input.len);
output.len = input.len;
for (int i = 0; i < input.len; i++) {
output.ptr[i] = toupper(input.ptr[i]);
}
return output;
}
void MyFunction() {
PBStr upper = MakeUpperCase(PB_STR("Hello"));
// ...
PBHeapFree(upper.ptr);
}
The final string type is PBBuf
. This allows the caller of a function to provide its own buffer in which the inner function places its result. The initial contents of the buffer may also be meaningful to the inner function; the inner function may choose to leave the contents as-is.
PBBuf
has 3 fields: char *ptr;
which points to the start of the buffer; size_t len
which gives the number of bytes of the string currently placed in the buffer; and size_t cap;
which gives the size of the buffer in bytes. Consequently, len
cannot be greater than cap
.
PBBuf
is non-resizable from the perspective of the inner function. It may modify the len
field and the bytes pointed to by ptr
, but it must not modify the ptr
or cap
fields.
Here is an example of correctly passing and returning a string through PBBuf
.
void MakeUpperCase(PBBuf *buf) {
for (int i = 0; i < buf->len; i++) {
buf->ptr[i] = toupper(buf->ptr[i]);
}
}
void MyFunction() {
char string[] = "Hello";
PBBuf buf;
buf.ptr = &string[0];
buf.len = 5;
buf.cap = 5;
MakeUpperCase(&buf);
// ...
}
Here is another example, where the inner function changes the length of the string.
void Append(PBBuf *buf, PBStrSlice str) {
for (int i = 0; i < str.len && buf->len < buf->cap; i++, buf->len++) {
buf->ptr[buf->len] = str.ptr[i];
}
}
void MyFunction() {
char string[10] = "Hello";
PBBuf buf;
buf.ptr = &string[0];
buf.len = 5;
buf.cap = 10;
Append(&buf, PB_STR(" world!"));
// string contains "Hello worl"
// ...
}
Objects
Internal system objects are made available through opaque handles to application developers. For example, PBImage
is an object that stores image data. Applications cannot directly access the data stored by an object, since its format may change between system versions, and the data may even by owned by a different process! Applications can only interact with objects through the API functions that use them. For example, PBImageCreateFromData
will create an image object, and PBImageGetSize
will return its size.
In the C API bindings, objects appear as pointers. For example, images are passed around with the type PBImage *
. But note well that these are only opaque handles; they may not actually be pointers! They are simply contain some value that the system can use to identify the object.
Each object has a counter called its reference count. When this count reaches 0
, the system will deallocate the object and close any resources it owns. You can increment this count with the object's respective -Retain
function, and decrement it with the -Release
function. When you create an object, its reference count will typically start at 1
. You are then responsible to call the relevant -Release
function exactly once when you no longer need the object.
The system may keep additional internal references to an object, but this will be completely transparent from the application's perspective. The application should call the -Release
function exactly once for each reference for which it is responsible. Each call to create an object will require one corresponding release, and each call to retain an object will require one corresponding release.
API functions that take object handles as parameters will either have the type -Ptr
(an owned handle) or -Ref
(a reference handle). A function that receives an owned handle then becomes responsible for releasing it; a referenced handle does not. See the below example:
void InnerFunction(PBImageRef image1, PBImagePtr image2) {
// We have been passed ownership of a reference to image2,
// so we must release it.
PBImageRelease(image2);
// We must not release image1;
// the reference is still owned by the caller.
}
void OuterFunction() {
PBImage *image1 = PBImageCreateFromData(...);
// This function has created image1, so it is
// responsible for calling PBImageRelease(image1).
PBImage *image2 = PBImageCreateFromData(...);
// This function has created image2, so it is
// responsible for calling PBImageRelease(image2).
InnerFunction(image1, image2);
// This passes ownership of image2 to InnerFunction,
// so we no longer are responsible for releasing it.
// But we must still release image1.
PBImageRelease(image1);
}
Retaining objects is useful when your function has not been passed ownership of a reference but it needs to store the object for later. It is also useful when passing an object to a second thread.
PBImage *globalImage;
void InnerFunction(PBImageRef image) {
// Keep a reference of the image available.
PBImageRetain(image);
if (globalImage) {
PBImageRelease(globalImage);
}
globalImage = image;
}
Functions that return objects, such as object creation functions, always return an owned handle.
PBTextStyle *style = PBTextStyleOpenByName(PBNamedTextStyle_CONTROL_DEFAULT);
// do something with the object...
PBTextStyleRelease(style);
Object type are arranged in a hierarchy. Some object types are subtypes of others. For example, PBButton
(a user interface button) is a subtype of PBElement
(a generic user interface element). You can always safely cast from the subtype:
PBButton *button = PBButtonCreate(...);
PBElement *element = (PBElement *) button; // okay!
// ...
PBElementRelease(element);
To cast to the subtype, use the relevant cast function. This performs a runtime assertion to check the cast is possible.
PBButton *button = PBButtonCast(element);
// If the element was not a button, this would have failed.
Callbacks
Before passing a function pointer to an API call, you must wrap it using the associated _Make
function.
void MyFilePickerClosed(PBClipboardReader *items, UserContext context) {
// ...
}
// Wrap the function pointer using the _Make function.
PBFilePickerShow(owner, configuration,
PBFilePickerCompletionHandler_Make(MyFilePickerClosed));
If you need to associate additional data with a callback (like a closure), things become more complicated.
First, you need to allocate a "table" for the callback. Typically this will need to be on the heap, but some API calls may specify that it is safe to place it on the stack. You can store your additional data after the table.
typedef struct MyFilePickerClosedData {
PBFilePickerCompletionHandler_Table table;
int myValue;
} MyFilePickerClosedData;
MyFilePickerClosedData *data = (MyFilePickerClosedData *)
malloc(sizeof(MyFilePickerClosedData));
data->myValue = 123;
Then you need to set the ifp
and ffp
fields in the table. These are function pointers. ifp
is called to invoke the callback: it has the same signature as a plain function pointer for the callback, but as its first argument it is passed the table pointer. ffp
is called when the callback is no longer needed. If you do not need to perform any cleanup, then ffp
can be NULL
, although this is rare.
void MyFilePickerClosedIFP(PBFilePickerCompletionHandler_Table *table,
PBClipboardReader *items, UserContext context) {
MyFilePickerClosedData *data = (MyFilePickerClosedData *) table;
// ...
}
void MyFilePickerClosedFFP(PBFilePickerCompletionHandler_Table *table) {
MyFilePickerClosedData *data = (MyFilePickerClosedData *) table;
// ...
free(data);
}
...
data->table.ifp = MyFilePickerClosedIFP;
data->table.ffp = MyFilePickerClosedFFP;
Finally we can populate the callback structure and pass it to the original API call.
PBFilePickerCompletionHandler handler;
handler.indirect = true; // This handler uses an indirect table.
handler.data.table = table; // Set the table.
PBFilePickerShow(owner, configuration, handler);
The reason that the IFP and FFP functions are separate is to account for the fact that callbacks may be called multiple times or not at all, in certain circumstances. For example, the PBFilePickerShow
function will never call the IFP if it fails to initialize the file picker. But the FFP is always called exactly once.
UI Element Hierarchy
User interfaces are contained in windows, represented by PBWindow
objects. Each window has a hierarchy (a rooted tree) of user interface elements, each represented by a PBElement
object. The window object is the root of hierarchy.
Each element has a set of flags stored in a uint32_t
. Bits 8-31 are defined by PBElementFlags
and are common to all element types. Bits 0-7 are defined for each type of element individually. For example, PBButton
has its additional flags specified in PBButtonFlags
.
Each element has a position in its window, stored as a PBRectangle64
, a rectangular bounding box.
Each element has two associated callbacks, the class message handler and the user message handler. These callbacks are sent "messages" which determine the behavior of the element. The class handler is defined by the type of the element. For example, buttons and sliders have different class handlers, although every button will have the same class handler and every slider will have the same class handler. Every instance of an element can have a different user message handler. It is set by PBElementSetUserMessageHandler
.
The class handler typically defines the main behavior of an element. The user handler can then override these behaviors as well as listen in for other events. For example, consider a text input field. The class message handler will receive keyboard and mouse input messages and update the contents of the field dutifully. The user message handler may choose to override a specific key press message, such as the escape key, and perform some other action. See PBElementMessage
for further details of message sending, and see PBMessage
for a list of system-defined messages.
Each element can exist in various states, such as hovered (the mouse cursor is within the element's bounding box), pressed (the mouse button was pressed while hovering over the element) and focused (the element will receive keyboard events).
Each element is responsible for drawing itself to the window's backing PBRenderSurface
after it is created and after it is updated. You can set the appearance of the element with either PBElementSetStyle
or PBElementSetCustomStyle
. The former takes a pre-defined system style ID. The latter lets you pass in a PBAppearance
object, which can contain custom styling. Alternatively, an element can have the flag PBElement_CUSTOM_PAINT
, in which case it will be sent PBMsg_CUSTOM_PAINT
messages in which it can perform arbitrary drawing commands.
Each element is responsible for determining the position of its child elements, a process called layouting. By default, this happens automatically: an element will either place its children in a vertical column or a horizontal row, depending on whether it has the PBElement_LAYOUT_HORIZONTAL
flag. Should this algorithm be insufficient, an element with the flag PBElement_CUSTOM_LAYOUT
will be sent various messages allowing it to perform the layout manually.
Applications can tag element with arbitrary data using the functions PBElementSetUserDataInt
and PBElementSetUserDataPtr
. Every element stores both an int64_t
and void *
internally for use by the application.
UI Event Loop
A running Playbit application has a thread that remains in a loop that alternates between waiting for external events and then running callbacks registered to handle those events.
An application begins this "event loop" by calling PBEventLoop
, typically in its main
function. The event loop exits when all the application's windows have been destroyed. Even a hidden window will keep the event loop running.
For example, when the user presses a key on their keyboard, a keyboard event is generated. The process that owns the active window will receive the event in its event loop. The event will then be dispatched as a message to the focused element in that window, through PBElementMessage
. At the end of the event, the system will perform any tasks that were buffered up during the main processing of the event, such as repainting modified elements.
Another example: when the user opens an application, the application's process will receive an "app create" event. This is dispatched as a message to the "application message handler", the callback to which is passed to PBEventLoop
.
Consider the following application:
intptr_t MyElement(PBElement *element, PBMessage *message) {
if (message->type == PBMsg_CUSTOM_PAINT
&& message->customPaint.atDepth == PBDepthIndex_MAIN) {
PBSize size = PBElementGetSize(element);
PBDrawRectangle(message->customPaint.painter,
PB_RECT_2S(size.width, size.height),
PBColorFromName(PBElementGetUserDataInt(element)));
} else if (message->type == PBMsg_KEY_DOWN) {
// Update the color.
PBElementSetUserDataInt(element,
PBNamedColor_ADAPTIVE_GREEN);
// Mark for repainting.
PBElementRepaintAll(element);
}
return 0;
}
intptr_t MyApp(PBMessage *message) {
if (message->type == PBMsg_APP_CREATE) {
PBWindowCreationOptions opts = {};
opts.type = PBWindowType_STANDARD;
opts.width = 800;
opts.height = 600;
PBWindow *window = PBWindowAdd(opts);
PBElement *e = PBElementCreate((PBElement *) window,
PBElement_CUSTOM_PAINT,
PBElementMessageHandler_Make(MyElement));
PBElementFocus(e, 0); // Set keyboard focus.
PBElementSetUserDataInt(e,
PBNamedColor_ADAPTIVE_RED); // Set the color.
PBElementRelease(e);
}
return 0;
}
int main() {
return PBEventLoop(PBApplicationMessageHandler_Make(MyApp));
}
A window is created containing an element that uses custom paint. The element stores its color in its user data integer. It handles PBMsg_CUSTOM_PAINT
by drawing a rectangle of that color. The element is also given keyboard focus. It handles PBMsg_KEY_DOWN
by changing its saved color and then requesting a repaint.
The flow of operations with respect to the event loop looks something like this:
main
PBEventLoop
[wait for event]
[receive app create event]
MyApp(PBMsg_APP_CREATE)
layout, paint and show window
[wait for event]
[receive key press event]
MyElement(e, PBMsg_KEY_DOWN)
paint window
[wait for event]
Most UI functions can only be used from within an event. This ensures that any buffered up operations (like repainting) are actually performed, since these only happen at the end of an event. Furthermore, it prevents multiple threads from accessing the UI at the same time, which naturally would cause issues.
In the C/C++ bindings for each function that creates an element there is a copy with -Create
replaced by -Add
. For example, PBButtonCreate
becomes PBButtonAdd
. These functions perform something like,
PBButton *PBButtonAdd(...) {
PBButton *button = PBButtonCreate(...);
PBElementRelease((PBElement *) button);
return button;
}
Immediately releasing an object after creating it typically means that the object will be deallocated. However, the system maintains an additional reference to all elements that have not been destroyed through PBElementDestroy
or similar. Therefore it is safe to continue using the element handle for the duration of the function that calls an -Add
function. If you wish, in the element's message callback you can handle the PBMsg_DEALLOCATE
message to detect exactly when the pointer becomes invalid.
Threading
The file docs/libplaybit-thread-safety.txt
gives a list of the thread-safety of each function in the libplaybit API.
There are various functions that operate upon a single object, and only one thread can operate on the object at a time. For example, it is safe to write to different streams on different threads at the same time with PBStreamWrite
, but it is not possible to write to the same stream on different threads at the same time.
As mentioned above, most functions in the libplaybit API can only be accessed within an event. If your code is not necessarily running in an event, you can run a callback function as if it were an event using PBRunAsEvent
and passing waitForCompletion = true
.
void FunctionB() {
PBTextDisplaySetContents(display, ...);
...
}
void FunctionA() {
PBRunAsEvent(PBRunAsEventCallback_Make(FunctionB), true /* waitForCompletion */);
}
If the caller to PBRunAsEvent
is already running in an event, it will simply invoke the callback and return. If it is not, it will wait for the currently processing event to finish (if there is one) and then process the event on the current thread and return.
If you instead want to always buffer up the callback to run on the main thread as an event, and you want PBRunAsEvent
to not wait for the callback to finish, then call PBRunAsEvent
with waitForCompletion = false
. This adds the callback to a queue that the main event loop thread will eventually pickup and process.
Terminology
Typically, in API symbols the term "User" refers to the user of the API, i.e. the application developer. For example, the functions PBElementGetUserDataPtr
and PBElementSetUserDataPtr
allow an application developer to associate an arbitrary pointer with each PBElement
; the system will neither access nor change these pointers.