essence-os/help/API Documentation.md

25 KiB

Random number generator

Definitions

uint8_t EsRandomU8(); 
uint64_t EsRandomU64();
void EsRandomAddEntropy(uint64_t x); 
void EsRandomSeed(uint64_t x); 

Description

Used to generate pseudo-random numbers. Note: the algorithm used is not suitable for cryptographic or statistical applications. These functions are thread-safe.

EsRandomU8 generates a single byte of random data, and EsRandomU64 generates a random uint64_t.

EsRandomSeed begins a new sequence of random numbers. For a given seed, subsequent calls to EsRandomU64 will form the same sequence every time.

EsRandomAddEntropy is used to move to a different point in the sequence of numbers, where there is no obvious link between the input value and the new sequence position.

Example

EsPrint("A random number between 1 and 100 is:\n", 1 + (EsRandomU64() % 100));

Performance timers

Definitions

void EsPerformanceTimerPush();
double EsPerformanceTimerPop();

Description

Used to accurately time sections of code.

EsPerformanceTimerPush pushes the current time onto a stack. EsPerformanceTimerPop removes the top item, and returns the elapsed time since that item was added.

The stack must not exceed more than 100 items.

Example

EsPerformanceTimerPush();

EsPerformanceTimerPush();
PerformStep1();
double timeStep1 = EsPerformanceTimerPop();

EsPerformanceTimerPush();
PerformStep2();
double timeStep2 = EsPerformanceTimerPop();

double timeTotal = EsPerformanceTimerPop();

EsPrint("Total time: %F seconds.\n", timeTotal);
EsPrint("\tStep 1 took %F seconds.\n", timeStep1);
EsPrint("\tStep 2 took %F seconds.\n", timeStep2);

Threads

Definitions

struct EsThreadInformation {
	EsHandle handle;
	uint64_t tid;
}

#define ES_CURRENT_THREAD ((EsHandle) (0x10))

typedef void (*EsThreadEntryCallback)(EsGeneric argument);

EsError EsThreadCreate(EsThreadEntryCallback entryFunction, EsThreadInformation *information, EsGeneric argument); 
uint64_t EsThreadGetID(EsHandle thread);
void EsThreadTerminate(EsHandle thread); 

Description

Threads are used to execute code in parallel. Threads can be created and terminated. A process can manipulate threads via handles. A handle to a thread can be closed with EsHandleClose. Each thread has a unique ID. A thread ID will not be reused until all handles to the thread that previously used the ID have been closed. Threads may not necessarily execute in parallel; the system may simulate the effect of parallel execution by causing the CPU to switch rapidly between which thread it is executing.

EsThreadCreate creates a new thread, starting at the provided entryFunction, which will be passed argument. After the call, information will be filled with a handle to the newly created thread, and the thread's unique ID in tid. This function returns ES_SUCCESS if the thread was successfully created.

EsThreadGetID gets the ID of a thread from its handle.

EsThreadTerminate instructs a thread to terminate. If the thread is executing privileged code at the time of the request, it will complete the prviledged code before terminating. If a thread is waiting on a synchronisation object, such as a mutex or event, it will stop waiting and terminate regardless. Note: if a thread owns a mutex or spinlock when it is terminated, it will not release the object. You should ensure that a thread releases all synchronisation objects still needed by the process before it terminates.

A thread can always use the handle ES_CURRENT_THREAD to access itself. This handle should not be closed.

Example

void MyThread(EsGeneric number) {
	while (true) { 
		EsPrint("Thread %d has ID %d!\n", number.i, EsThreadGetID(ES_CURRENT_THREAD));
	}
}

for (uintptr_t number = 1; number <= 5; i++) {
	EsThreadInformation information;
	EsError error = EsThreadCreate(MyThread, &information, number);

	if (error != ES_SUCCESS) {
		EsPrint("Thread %d could not be created.\n", number);
	} else {
		EsPrint("Started thread %d with ID %d.\n", number, information.tid);

		// Close the handle to the thread.
		EsHandleClose(information.handle):
	}
}

Mutexes

Definitions

void EsMutexAcquire(EsMutex *mutex); 
void EsMutexDestroy(EsMutex *mutex); 
void EsMutexRelease(EsMutex *mutex); 

Description

A mutex is a synchronisation primitive. Threads can acquire and release it. Only one thread can have acquired the mutex at a time. Before another thread can acquire it, the original thread must release it. When a thread tries to acquire an already-acquired mutex, it will wait until the mutex is released, and then proceed to acquire it.

The EsMutex structure contains a mutex. It should be initialised to zero. When the mutex is no longer needed, it can be destroyed with EsMutexDestroy. A mutex must not be acquired when it is destroyed.

To acquire a mutex, call EsMutexAcquire with a pointer to the mutex. To release a mutex, call EsMutexRelease. A thread must not attempt to acquire a mutex it already owns, and it must not attempt to release a mutex it does not own.

When a thread is waiting for a mutex to be released so that it may acquire it, the thread will not consume CPU time. This allows other threads to run instead, or if there are none, then it gives the CPU an opportunity to reduce its power consumption. As a side effect, this means there is some delay after a thread waits for a mutex before it wakes up and continues executing. If this behaviour is undesirable, consider using EsSpinlock instead.

Example

In this example, the function IncrementCount can safely be called on different threads at the same time.

EsMutex mutex;
#define INITIAL_COUNT (10)

void IncrementCount(int *count) {
	EsMutexAcquire(&mutex);

	if (count == 0) {
		// count is uninitialised, so initialise it now.
		*count = INITIAL_COUNT;
	}

	count++;

	EsMutexRelease(&mutex);
}

Without the mutex, unexpected behaviour may occur, as the effective operation of threads may be arbitrarily interleaved.

Consider two threads, A and B, that both call IncrementCount:

  • Thread A enters IncrementCount and sees that count = 0.
  • Thread B enters IncrementCount and sees that count = 0.
  • Thread A sets count to 10.
  • Thread A increments count by 1 to 11.
  • Thread B sets count to 10.
  • Thread B increments count by 1 to 11.

Despite IncrementCount being called twice, the count is only incremented once.

Another possibility is:

  • Thread A enters IncrementCount and reads count into a register; it has the value 10.
  • Thread A adds 1 to the register, giving 11.
  • Thread B enters IncrementCount and reads count into a register; it has the value 10.
  • Thread B adds 1 to the register, giving 11.
  • Thread A stores the value in its register into count, 11.
  • Thread B stores the value in its register into count, 11.

Again, despite IncrementCount being called twice, the count is only incremented once.

Deadlock

Mutexes must be acquired in a consistent order. For example, if the following pattern appears in your code:

  1. Acquire mutex X.
  2. Acquire mutex Y.
  3. Release mutex Y.
  4. Release mutex X.

Then the following pattern must not occur:

  1. Acquire mutex Y.
  2. Acquire mutex X.
  3. Release mutex X.
  4. Release mutex Y.

To explain why, suppose thread A executes the first pattern and thread B executes the second.

  • Thread A acquires mutex X.
  • Thread B acquires mutex Y.
  • Thread A attempts to acquire mutex Y. Mutex Y is owned by thread B, so thread A starts waiting for thread B to release it.
  • Thread B attempts to acquire mutex X. Mutex X is owned by thread A, so thread B starts waiting for thread A to release it.
  • Both threads will continue to wait indefinitely for the other to perform an operation.

Spinlocks

Definitions

struct EsSpinlock {
	volatile uint8_t state;
}

void EsSpinlockAcquire(EsSpinlock *spinlock); 
void EsSpinlockRelease(EsSpinlock *spinlock); 

Description

A spinlock is a synchronisation primitive. Threads can acquire and release it. Only one thread can have acquired the spinlock at a time. Before another thread can acquire it, the original thread must release it. When a thread tries to acquire an already-acquired spinlock, it will wait until the spinlock is released, and then proceed to acquire it.

The EsSpinlock structure contains a spinlock. It should be initialised to zero. There is no need to perform any special destruction of a spinlock.

To acquire a spinlock, call EsSpinlockAcquire with a pointer to the spinlock. To release a spinlock, call EsSpinlockRelease. A thread must not attempt to acquire a spinlock it already owns, and it must not attempt to release a spinlock it does not own.

When a thread is waiting for a spinlock to be released so that it may acquire it, the thread will "spin"; that is, it will continue executing, constantly checking whether the spinlock has been released so that it may acquire it. This consumes CPU power, and prevents other threads from running instead. However, this means that as soon as the spinlock becomes available a thread may instantly acquire it and continue with its work. Spinlocks are best used in situations where the code they synchronise is small. If this behaviour is undesirable, consider using EsMutex instead.

INI files

Definitions

struct EsINIState {
	char *buffer, *section, *key, *value;
	size_t bytes, sectionBytes, keyBytes, valueBytes;
};

bool EsINIParse(EsINIState *s);
bool EsINIPeek(EsINIState *s);
size_t EsINIFormat(EsINIState *s, char *buffer, size_t bytes);
void EsINIZeroTerminate(EsINIState *s);

Description

To parse an INI file, first initialise a blank EsINIState structure. Set buffer to point to the INI data, and set bytes to the byte count of the data. Then, call EsINIParse repeatedly, until it returns false, indicating it has reached the end of the data. After each call to EsINIParse, the fields of EsINIState are updated to give the information about the last parsed line in the INI file. EsINIPeek is the same as EsINIParse except it does not advance to the next line in the INI data. Fields in EsINIState that are not applicable to the parsed line are set to empty strings. Comment lines set key to ; and value contains the comment itself. Aside for empty strings, the fields in EsINIState will always point into the provided buffer.

For example, the line [world] will set section to "world", and key and value to empty strings. When followed by the line k=v, section will retain its previous value, and key will be set to "k" and value will be set to "v".

EsINIFormat formats the contents of EsINIState into a buffer. bytes gives the size of buffer. The return value gives the number of bytes written to buffer; this is clipped to the end of the buffer.

EsINIZeroTerminate zero-terminates section, key and value in the EsINIState. It cannot be used after calling EsINIPeek.

Example

size_t fileBytes;
void *file = EsFileReadAll(EsLiteral("|Settings:/Default.ini"), &fileBytes);

EsINIState s = {};
s.buffer = file;
s.bytes = fileBytes;

while (EsINIParse(&s)) {
	EsINIZeroTerminate(&s);
	EsPrint("section = %z, key = %z, value = %z\n", s.section, s.key, s.value);
}

EsHeapFree(file);

Heap allocator

Definitions

void *EsHeapAllocate(size_t size, bool zeroMemory, EsHeap *heap = ES_NULL);
void EsHeapFree(void *address, size_t expectedSize = 0, EsHeap *heap = ES_NULL);
void *EsHeapReallocate(void *oldAddress, size_t newAllocationSize, bool zeroNewSpace, EsHeap *heap = ES_NULL);
void EsHeapValidate(); 

Description

The heap allocator is a general-purpose allocator. It is thread-safe. It is designed to handle regions of memory of arbitrary size, and arbitrary lifetime.

To allocate memory, call EsHeapAllocate. Set size to be the size of the region in bytes. Set zeroMemory to true for the contents of the region to be automatically zeroed, otherwise false. Set heap to NULL; this parameter is reserved. The return value is the address of the start of allocated region. It will be suitably aligned, given the specified region size and current processor architecture. If size is 0, then NULL is returned.

To free memory, call EsHeapFree. Set address to be the start of the previously allocated region. Optionally, set expectedSize to be the size of the allocated region; if non-zero, the system will assert that this value is correct. Set heap to NULL; this parameter is reserved. If address is NULL, this function will do nothing.

To grow or shrink an existing region, call EsHeapReallocate. Set oldAddress to be the start of the previously allocated region. Set newAllocationSize to be new the size of the region. Set heap to NULL; this parameter is reserved. If oldAddress is NULL, this call is equivalent to EsHeapAllocate. If newAllocationSize is 0, this call is equivalent to EsHeapFree. The return value gives the new start address of the region. This may be the same as oldAddress, if the region was able to change size in place. If the region was not able to change size in place, the old contents will be copied to the new region. If zeroNewSpace is set, and newAllocationSize is greater than the previous size of the region, then the newly accessible bytes at the end of the region will be cleared to zero.

EsHeapValidate will check the current process's heap for errors. If it finds an error, it will crash the process. Do not rely on any behaviour of this function. It is only intended to aid debugging memory errors.

Example

int *array = (int *) EsHeapAllocate(sizeof(int) * 10, true);

for (int i = 0; i < 5; i++) {
	array[i] = i + 1;
}

for (int i = 0; i < 10; i++) {
	EsPrint("%d ", array[i]); // Prints 1 2 3 4 5 0 0 0 0 0.
}

EsPrint("\n");

array = (int *) EsHeapReallocate(array, sizeof(int) * 15, true);

for (int i = 0; i < 15; i++) {
	EsPrint("%d ", array[i]); // Prints 1 2 3 4 5 0 0 0 0 0 0 0 0 0 0.
}

EsHeapFree(array);

Rectangles

Definitions

struct EsRectangle {
	int32_t l;
	int32_t r;
	int32_t t;
	int32_t b;
};

#define ES_RECT_1(x) ((EsRectangle) { (int32_t) (x), (int32_t) (x), (int32_t) (x), (int32_t) (x) })
#define ES_RECT_1I(x) ((EsRectangle) { (int32_t) (x), (int32_t) -(x), (int32_t) (x), (int32_t) -(x) })
#define ES_RECT_2(x, y) ((EsRectangle) { (int32_t) (x), (int32_t) (x), (int32_t) (y), (int32_t) (y) })
#define ES_RECT_2I(x, y) ((EsRectangle) { (int32_t) (x), (int32_t) -(x), (int32_t) (y), (int32_t) -(y) })
#define ES_RECT_2S(x, y) ((EsRectangle) { 0, (int32_t) (x), 0, (int32_t) (y) })
#define ES_RECT_4(x, y, z, w) ((EsRectangle) { (int32_t) (x), (int32_t) (y), (int32_t) (z), (int32_t) (w) })
#define ES_RECT_4PD(x, y, w, h) ((EsRectangle) { (int32_t) (x), (int32_t) ((x) + (w)), (int32_t) (y), (int32_t) ((y) + (h)) })
#define ES_RECT_WIDTH(_r) ((_r).r - (_r).l)
#define ES_RECT_HEIGHT(_r) ((_r).b - (_r).t)
#define ES_RECT_TOTAL_H(_r) ((_r).r + (_r).l)
#define ES_RECT_TOTAL_V(_r) ((_r).b + (_r).t)
#define ES_RECT_ALL(_r) (_r).l, (_r).r, (_r).t, (_r).b
#define ES_RECT_VALID(_r) (ES_RECT_WIDTH(_r) > 0 && ES_RECT_HEIGHT(_r) > 0)

EsRectangle EsRectangleAdd(EsRectangle a, EsRectangle b);
EsRectangle EsRectangleAddBorder(EsRectangle rectangle, EsRectangle border);
EsRectangle EsRectangleBounding(EsRectangle a, EsRectangle b);
EsRectangle EsRectangleCenter(EsRectangle parent, EsRectangle child);
EsRectangle EsRectangleCut(EsRectangle a, int32_t amount, char side);
EsRectangle EsRectangleFit(EsRectangle parent, EsRectangle child, bool allowScalingUp);
EsRectangle EsRectangleIntersection(EsRectangle a, EsRectangle b);
EsRectangle EsRectangleSplit(EsRectangle *a, int32_t amount, char side, int32_t gap = 0);
EsRectangle EsRectangleSubtract(EsRectangle a, EsRectangle b);
EsRectangle EsRectangleTranslate(EsRectangle a, EsRectangle b);
bool EsRectangleEquals(EsRectangle a, EsRectangle b);
bool EsRectangleContains(EsRectangle a, int32_t x, int32_t y);

Description

EsRectangle is used to store an integral rectangular region. l gives the offset of the left edge, r the right edge, t the top edge, and b the bottom edge. Note that this means the rectangle does not contain the pixels with x coordinate r and y coordinate b. The edges are stored are 32-bit signed integers. TODO Diagram.

EsRectangleAdd performs a component-wise sum of two rectangles.

EsRectangleAddBorder is similar to EsRectangleAdd, but it negates the r and b fields of border before the addition. TODO Diagram.

EsRectangleSubtract is similar to EsRectangleAdd, but it negates all fields of the second rectangle before the addition. TODO Diagram.

EsRectangleTranslate is similar to EsRectangleAdd, but it sets r to l and b to t in the second rectangle before the addition. TODO Diagram.

EsRectangleBounding computes the smallest possible rectangle that contains both parameters. TODO Diagram.

EsRectangleCenter centers the child rectangle within the parent rectangle. The origin of child is ignored; only its dimensions matter. TODO Diagram.

EsRectangleCut cuts a slice of a rectangle, by moving one of the edges of the input rectangle. The returned rectangle is the slice that was cut off. side determines the edge; it is a single character, set to be the same as the name of the field that will be modified. If amount is positive, then the edge will be moved inwards by that amount. If amount is negative, then the edge will be moved outwards by the absolute value. TODO Diagram.

EsRectangleSplit is similar to EsRectangleCut, except it modifies the input rectangle so that it contains the modified rectangle after a side is moved. gap may be optionally specified to change the distance between the two returned rectangles. TODO Diagram.

EsRectangleFit resizes and moves the child rectangle, while preserving its aspect ratio, so that it fits in, and is centered in, parent. If allowScalingUp is set to false, the function will never resize child to a larger size. TODO Diagram.

EsRectangleIntersection computes the intersection of the two parameters. TODO Diagram.

EsRectangleEquals returns true if the rectangles are identical, otherwise false.

EsRectangleContains returns true if the rectangle contains the specified point, otherwise false. Recall, as noted above, if x is r or larger, the point is considered to not be inside the rectangle, and similarly if y is b or larger.

ES_RECT_1 creates a rectangle where all fields are the same value. ES_RECT_1I creates a rectangle similarly, except r and b are negated.

ES_RECT_2 creates a rectangle where l and r are the same value, and t and b are the same value. ES_RECT_2I creates a rectangle similarly, except r and b are negated. ES_RECT_2S creates a rectangle with its top-left corner at (0, 0), with the specified width and height.

ES_RECT_4 creates a rectangle with the specified values of l, r, t and b. ES_RECT_4PD creates a rectangle with its top-left corner at (x, y) and a width of w and height of h.

ES_RECT_WIDTH returns the width of a rectangle. ES_RECT_HEIGHT returns the height. ES_RECT_TOTAL_H returns the sum of the left and right components. ES_RECT_TOTAL_V returns the sum of the top and bottom coponents.

ES_RECT_ALL splits a rectangle into its four components, separated by commas.

ES_RECT_VALID returns true if the rectangle has positive width and height, otherwise false.

Text plans

Definitions

struct EsTextRun {
	EsTextStyle style;
	uint32_t offset;
};

struct EsTextPlanProperties {
	EsCString cLanguage;
	uint32_t flags; 
	int maxLines;
};

#define ES_TEXT_H_LEFT 	                    (1 << 0)
#define ES_TEXT_H_CENTER                    (1 << 1)
#define ES_TEXT_H_RIGHT 	            (1 << 2)
#define ES_TEXT_V_TOP 	                    (1 << 3)
#define ES_TEXT_V_CENTER                    (1 << 4)
#define ES_TEXT_V_BOTTOM                    (1 << 5)
#define ES_TEXT_ELLIPSIS	            (1 << 6)
#define ES_TEXT_WRAP 	                    (1 << 7)
#define ES_TEXT_PLAN_SINGLE_USE             (1 << 8)
#define ES_TEXT_PLAN_TRIM_SPACES            (1 << 9)
#define ES_TEXT_PLAN_RTL	            (1 << 10)
#define ES_TEXT_PLAN_CLIP_UNBREAKABLE_LINES (1 << 11)

EsTextPlan *EsTextPlanCreate(EsElement *element, EsTextPlanProperties *properties, EsRectangle bounds, const char *string, const EsTextRun *textRuns, size_t textRunCount);

int EsTextPlanGetWidth(EsTextPlan *plan);
int EsTextPlanGetHeight(EsTextPlan *plan);
size_t EsTextPlanGetLineCount(EsTextPlan *plan);

void EsTextPlanDestroy(EsTextPlan *plan);

void EsTextPlanReplaceStyleRenderProperties(EsTextPlan *plan, EsTextStyle *style);

Description

Before you can draw text, you first need to create a text plan, which contains all the necessary information to draw the text. The advantage of using text plans is that it enables you to draw the same block of text multiple times without needing the text shaping and layout to be recalculated.

To create a text plan, use EsTextPlanCreate.

  • element: The user interface element which will use the text plan. The text plan will inherit appropriate display settings from the element, such as the UI scaling factor.
  • properties: Contains properties to apply while laying out the text. cLanguage is the BCP 47 language tag as a string; if NULL, the default language is used. maxLines contains the maximum number of lines allowed in the layout, after which lines will be clipped; if 0, the number of lines will be unlimited. flags contains any combination of the following constants:
    • ES_TEXT_H/V_...: Sets the alignment of the text in the bounds.
    • ES_TEXT_WRAP: The text is allowed to wrap when it reaches the end of a line.
    • ES_TEXT_ELLIPSIS: If the text is to be truncated, an ellipsis will be inserted.
    • ES_TEXT_PLAN_SINGLE_USE: Set to automatically destroy the text plan after the first time it is drawn.
    • ES_TEXT_PLAN_TRIM_SPACES: Removes any leading and trailing spaces from each line.
    • ES_TEXT_PLAN_RTL: Sets the default text direction to right-to-left.
    • ES_TEXT_PLAN_CLIP_UNBREAKABLE_LINES: If a word is to long to be word-wrapped, prevent it being from being wrapped regardless, and clip it instead.
    • ES_TEXT_PLAN_NO_FONT_SUBSTITUTION: Prevents font substitution. Otherwise, glyphs missing from the selected font will be drawn in an automatically-selected font that does support them.
  • bounds: Gives the width and height of the bounds in which the text will be drawn. Only the dimensions of this rectangle matters. It is unused if word wrapping is disabled, and no text alignment flags are specified.
  • string: Gives the text string, in UTF-8. This string should remain accessible until the text plan is destroyed.
  • textRuns: An array of text runs, describing the offset and style of each run in the text. The length of each run is automatically calculated as the difference between its offset and the offset of the next run in the array. The first run should have an offset of 0. The last item in the array should have its offset field set to be the total length of the input string, i.e. the end of the last run. The style field of the last item is ignored.
  • textRunCount: The number of text runs in the array. Note that the last item in the array should not be included in this count, because it itself does not describe a run, it is only used to indicate the end position of the actual last run.

Once a text plan has been created, it can be drawn with EsDrawText. Information about the calculated text layout can be accessed with EsTextPlanGetWidth (returns the horizontal size of the layout), EsTextPlanGetHeight (returns the vertical size), and EsTextPlanGetLineCount (returns the number of lines in the layout, including lines containing wrapped text). The text plan can be destroyed with EsTextPlanDestroy.

After a text plan is created, some of the style properties can be replaced. These are called the render properties, because they only are used when the text is rendered, and do not affect the layout. These are the color, blur, decorations and decorationsColor fields in EsTextStyle. They can be replaced using EsTextPlanReplaceStyleRenderProperties. Note that this replaces the properties for all text runs in the text plan.

Example

void DrawElementText(EsElement *element, EsRectangle bounds, const char *string, size_t stringBytes) {
	EsTextRun textRun[2] = {};
	EsElementGetTextStyle(element, &textRun[0].style);
	textRun[0].offset = 0;
	textRun[1].offset = stringBytes;
	EsTextPlanProperties properties = {};
	EsTextPlan *plan = EsTextPlanCreate(element, &properties, bounds, string, textRun, 1);
	EsDrawText(painter, plan, bounds, nullptr, nullptr);
	EsTextPlanDestroy(plan);
}

CRT functions

Description

The Essence API provides a number of functions from the C standard library which may be used without requiring the POSIX subsystem. See the API header file for an authoritative list of which functions are available. Please note that the functions may not be fully compliant with standards where it would cause unwanted complexity. The functions are prefixed with EsCRT; to use the functions without needing this prefix, define ES_CRT_WITHOUT_PREFIX before including the API header.