Michael Heilmann's Automatic Resource Management System Mark 1
This is the documentation for Michael Heilmann's Automatic Resource Management System Mark 1, henceforth ARMS1. ARMS1 is a precise stop the world garbage collector to be used for programs written in C. ARMS1 is available at michaelheilmann.com/repository/Arms1.
1. Files
You can find the sources of ARMS1 in my GitHub repository https://github.com/michaelheilmann/michaelheilmann.com. The subdirectory of Arms1 in the repository is here https://github.com/michaelheilmann/michaelheilmann.com/tree/main/repository/Arms1.
- The (sources of the) library resides in the directory https://github.com/michaelheilmann/michaelheilmann.com/tree/main/repository/Arms1/Sources
- The (sources of) demos reside in the directory https://github.com/michaelheilmann/michaelheilmann.com/tree/main/repository/Arms1/Demos
- The (sources of this very documentation you are reading) documentation reside in the directory https://github.com/michaelheilmann/michaelheilmann.com/tree/main/repository/Arms/Documentation
ARMS1 supports various platforms (including but not restricted to Windows, Linux, and many more), however, we currently only officially support Windows. Find instructions on how to build, test, and use R1 under various systems, please refer to README.md in the root folder of the repository.
2. How a ARMS1 works
ARMS1 allows for the creation of objects and maintains a set of these created objects called the universe \(U\). When an object \(x\) is created, a type \(a\) is associated with that object. That type \(a\) provides means to ARMS1 to determine the objects reachable from that object \(x\) and to properly destroy the object \(x\). To determine which objects are to be retained and are to be destroyed, ARMS1 starts from a subset of the universe called the root set \(R \subseteq U\) and visits all objects transitively reachable from \(R\). The reachable objects are called live objects \(L \subseteq U\). All other objects are called dead objects \(D\). \(L\) and \(D\) are disjoint and partition the universe in such that \(U = L \cup D\) (also note that \(R \subseteq L\) and \(R \cap D = \emptyset\)). ARMS1 removes all dead objects \(D\) from the universe \(U\) such that \(U@new = U@old - D\) and destroys them.
3. Starting up and shutting down
To use the services of ARMS1, a user must acquire an handle to ARMS1. The user acquires a handle by a succesfull call to Arms_Status Arms_startup()
.
This function returns Arms_Status_Success
to indicate success and returns status code different from Arms_Status_Success
on failure.
The following table lists the possible values returned in case of failure
Value | Description |
Arms_Status_AllocationFailed | an allocation failed |
Arms_Status_OperationInvalid | there are too many handles |
A user may acquire more than one handle.
If the user no longer requires the services of ARMS1, a user must relinquish the acquired handles.
The user relinquishes a handle by a successful call to Arms_Status Arms_shutdown()
.
This function returns Arms_Status_Success
to indicate success and returns status code different from Arms_Status_Success
on failure.
The following table lists the possible values returned in case of failure
Value | Description |
Arms_Status_OperationInvalid | if there are objects in \(U\). This is usually the result of illicit usage of ARMS1. |
Arms_Status_OperationInvalid | if the last handle was already relinquished. This is usually the result of illicit usage of ARMS1 |
Warning: Using the services of ARMS1 without a handle is undefined behavior.
3. Managed Memory
3.1 Creating types
The user adds a type to the ARMS1 by invoking Arms_Status Arms_addType(const char* name, size_t nameLength,
void* context, Arms_TypeRemovedCallbackFunction* typeRemoved, Arms_VisitCallbackFunction *visit, Arms_FinalizeCallbackFunction* finalize)
. The first argument name
is
a pointer to an array of nameLength
Bytes denoting the type name. No two types of the same name can be
registered and this function fails with Arms_Status_TypeExists
if an attempt is made to do so. visit
must point to a Arms_VisitCallbackFunction
or must be a null pointer. finalize
must point to a
Arms_FinalizeCallbackFunction
or must be a null pointer. typeRemoved
must point to a Arms_TypeRemovedCallbackFunction
or must be a null pointer. context
is an opaque pointer which is passed to the type removed callback function,
the visit callback function, and the finalize callback function. If Arms_addType
fails it returns a value different from
Arms_Status_Success
. The following table lists the possible values returned in case of failure
Value | Description |
Arms_Status_ArgumentInvalid | pObject is a null pointer |
Arms_Status_ArgumentInvalid | name is a null pointer |
Arms_Status_ArgumentInvalid | nameLength exceeds limits |
Arms_Status_ArgumentInvalid | size exceeds limits |
Arms_Status_AllocationFailed | an allocation failed |
Type removed callback function
A type removed function is a function of the signature void Arms_TypeRemovedCallbackFunction(void* context, Arms_Natural8 const* name, Arms_Size nameLength)
.
The type removed callback function is supplied to a type when that type is created.
The function is invoked when the type is removed (for example, when Arms_shutdown
is invoked).
The first argument is the context as supplied to the Arms_addType
.
The second argument is an array of Bytes (the name of the type) and the third argument the length of that array.
struct File {
...;
};
...
static bool g_registered = false;
...
void File_typeRemoved(void* context, Arms_Natural8 const* name, Arms_Size nameLength) {
g_registered = false;
}
...
int main(int argc, char**argv) {
Arms_startup();
if (!g_registered) {
Arms_addType("File", strlen("File"), NULL, &typeRemovedCallback, NULL, NULL);
g_registered = true;
}
Arms_shutdown();
return EXIT_SUCCESS;
}
Note that types can be created with a null pointer for the finalize function.
Visit callback function
A visit function is a function of the signature void Arms_VisitCallbackFunction(void* context, void *object)
.
The visit callback function is supplied to a type when that type is created via Arms_addType
.
The first argument is the context as supplied to the Arms_addType
.
The second argument is a pointer to the object.
The second argument is an array of Bytes (the name of the type) and the third argument the length of that array.
The visit function shall invoke void Arms_visit(void* object)
on any object directly reachable from the specified object.
In the following example, File_visit
is implemented to visit the field fn
of struct File
if it was not null.
struct File {
void* fn;
...
};
...
void File_visit(void* context, File* file) {
if (file->fn) {
Arms_visit(file->fn);
file->fn = NULL;
}
}
...
int main(int argc, char**argv) {
Arms_startup();
Arms_addType("File", strlen("File"), NULL, NULL, &visitFile, NULL);
struct File* file;
Arms_allocate(&file, "File", strlen("File"), sizeof(struct File));
file->fn = NULL;
Arms_lock(file);
Arms_RunStatistics statistics = { .destroyed = 0 };
Arms_run(&statistics);
Arms_shutdown();
return EXIT_SUCCESS;
}
Note that types can be created with a null pointer for the visit function.
Finalize callback function
A finalize function is a function of the signature void Arms_FinalizeCallbackFunction(void* context, void *object)
.
The finalize callback function is supplied to a type when that type is created via Arms_addType
.
The first argument is the context as supplied to the Arms_addType
.
The second argument is a pointer to the object.
The finalize function shall perform cleanup of unmanaged resources like unmanaged memory, file handles, etc.
In the following example, File_finalize
is implemented to invoke fclose
on the field fd
of struct File
if it was not null.
struct File {
FILE* fd;
};
...
void File_finalize(void* context, File* file) {
if (file->fd) {
fclose(file->fd);
file->fd = NULL;
}
}
...
int main(int argc, char**argv) {
Arms_startup();
Arms_addType("File", strlen("File"), NULL, NULL, NULL, &finalizeFile);
struct File* file;
Arms_allocate(&file, "File", strlen("File"), sizeof(struct File));
file->fd = fopen(...);
Arms_RunStatistics statistics = { .destroyed = 0 };
Arms_run(&statistics);
Arms_shutdown();
return EXIT_SUCCESS;
}
Note that types can be created with a null pointer for the finalize function.
3.2 Creating objects
To create an object, the user of ARMS1 creates an object by invoking Arms_Status Arms_allocate(void** pObject,
char const* name, size_t nameLength, size_t size)
. name
is a pointer to an array of nameLength
Bytes
denoting the type name of the type to be assigned to object. size
denotes the size, in Bytes, of the object to allocated
(0 is a valid size). If this function is invoked successfully, the *pObject
is assigned a pointer to an object of the
specified size. The contents of the Bytes are unspecified. The object is assigned the type of the specified name.
If this function fails it returns a value different from Arms_Status_Success
.
The following table lists the possible values returned in case of failure
Value | Description |
Arms_Status_ArgumentInvalid | pObject is a null pointer |
Arms_Status_ArgumentInvalid | name is a null pointer |
Arms_Status_ArgumentInvalid | nameLength exceeds limits |
Arms_Status_ArgumentInvalid | size exceeds limits |
Arms_Status_TypeNotExists | the type does not exist |
Arms_Status_AllocationFailed | an allocation failed |
3.3 Freeing resources
To relinqish resources, the user invokes Arms_Status Arms_run()
which destroys dead objects \(D\) such that the new universe
only contains live objects, in other terms \(U@new = U@old - D\) or equivalently \(U@new = L\) holds.
Arms_run
always succeeds.
3.4 Locks
A locked object \(x\) is element of the root set \(R\). Hence this object and all objects reachable from this object are live.
To add a lock to an object, the user invokes Arms_Status Arms_lock(void* object)
.
If this function is invoked successfully, the lock count of the object pointed to by object
was incremented by one. The initial lock count of an object is 0. A lock count of 0 means an object is not locked.
If this function fails it returns a value different from Arms_Status_Success
.
The following table lists the possible values returned in case of failure
Value | Description |
Arms_Status_ArgumentInvalid | object is a null pointer |
Arms_Status_OperationInvalid | the lock would overflow |
Arms_Status_AllocationFailed | an allocation failed |
To remove a lock from an object, the user invokes Arms_Status Arms_unlock(void* object)
.
If this function is invoked successfully, the lock count of the object pointed to by object
was decremented by one. If this function fails it returns a value different from Arms_Status_Success
.
The following table lists the possible values returned in case of failure
Value | Description |
Arms_Status_ArgumentInvalid | object is a null pointer |
Arms_Status_OperationInvalid | the lock count would underflow |
4. Unmanaged Memory
ARMS1 allows for allocation, reallocation, and deallocation of unmanaged memory via memory managers.
There are two different memory managers available for different use cases. Currently, the default memory manager and the slab memory manager are available. The former is described in section "Default Memory Manager", the latter is described in section "Slab Memory Manager". All memory manager share a commmon interface which is described in section "Memory Manager Interface".
4.1 Memory Manager Interface
This section describes the interface of the memory managers.
4.1.1 Arms_MemoryManager_allocate
Arms_MemoryManager_Status Arms_MemoryManager_allocate(Arms_MemoryManager* memoryManager, void** p, Arms_Size n);
Allocates an unmanaged memory block of size n
using the specified memory manager.
If this function succeeds:
A pointer to the beginning of the memory block is stored in *p
.
Arms_MemoryManager_Status_Success
is returned.
If this function fails: The environment is not observably changed. The function returns one of the status code in the table below.
Status Code | Description |
Arms_MemoryManager_Status_AllocationFailed | The allocation failed. |
Arms_MemoryManager_Status_ArgumentValueInvalid | p is a null pointer. |
Note that n
can be 0 as 0 is a valid size for a memory block.
4.1.2 Arms_MemoryManager_reallocate
Arms_MemoryManager_Status Arms_reallocate(Arms_MemoryManager* memoryManager, void** p, size_t n);
If this function succeeds:
A pointer to the beginning of the new memory block is stored in *p
.
The first
is min(n,
where n
is the size of the
new memory block and Arms_MemoryManager_Status_Success
is returned.
If this function fails: The environment is not observably changed. The function returns one of the status code in the table below.
Status Code | Description |
Arms_MemoryManager_Status_AllocationFailed | The allocation failed. |
Arms_MemoryManager_Status_ArgumentValueInvalid | p is a null pointer. |
Note that n
can be 0 as 0 is a valid size for a memory block.
4.1.3 Arms_MemoryManager_deallocate
Arms_MemoryManager_Status Arms_MemoryManager_deallocate(Arms_MemoryManager* memoryManager, void* p);
Deallocates an unmanaged memory block.
If this function succeeds:
The memory block pointed to by p
was deallocated.
If this function fails:
The environment is not observably changed.
This function fails only if p
is a null pointer. In that case, it returns Arms_MemoryManager_Status_ArgumentValueInvalid
.
4.2 Default Memory Manager
Obtain a pointer to the default memory manager by a call to to the function
Arms_MemoryManager* Arms_getDefaultMemoryManager()
This function always succeeds if ARMS1 is initialized. Otherwise its behaviour is undefined. The memory manager returned is valid as long as ARMS1 is initialized.
4.3 Slab Memory Manager
Obtain a pointer to the slab memory manager by a call to to the function
Arms_MemoryManager* Arms_getSlabMemoryManager()
This function always succeeds if ARMS1 is initialized. Otherwise its behaviour is undefined. The memory manager returned is valid as long as ARMS1 is initialized.
Warning: The Slab Memory Manager is work in progress.