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
to10
. - Thread A increments
count
by1
to11
. - Thread B sets
count
to10
. - Thread B increments
count
by1
to11
.
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 value10
. - Thread A adds
1
to the register, giving11
. - Thread B enters IncrementCount and reads
count
into a register; it has the value10
. - Thread B adds
1
to the register, giving11
. - 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:
- Acquire mutex X.
- Acquire mutex Y.
- Release mutex Y.
- Release mutex X.
Then the following pattern must not occur:
- Acquire mutex Y.
- Acquire mutex X.
- Release mutex X.
- 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; ifNULL
, the default language is used.maxLines
contains the maximum number of lines allowed in the layout, after which lines will be clipped; if0
, 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 of0
. The last item in the array should have itsoffset
field set to be the total length of the input string, i.e. the end of the last run. Thestyle
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.