Storing binary data in instances
The most common way of storing data in class instances is by defining member variables or constants that refer to the data. When dealing with existing C frameworks, however, it is often desirable to store C level data structures or pointers inside Alore objects. This section gives an overview of including arbitrary C level binary data within Alore objects.
See also: You can also use functions described in section Memory management for the same purpose.
Using per instance binary data
These macros can be used to allocate fixed size data blocks in each instance of the class. These blocks can be used to store any C level data such as pointers, structs or floating point numbers. Binary data is always initialized to zero early during object construction, even before create or #i is called. See section Accessing binary data below for information on accessing and modifying the binary data.
- A_BINARY_DATA(size)
- Include a data block of the given size, in bytes, in each instance of the class.
- A_BINARY_DATA_P(size, intPtr)
- Include a data block of the given size, in bytes, in each instance of the class. Store the offset of the data block in *intPtr (intPtr should have type int *). This offset can be used to access the data.
Using external data
- A_EXTERNAL_DATA()
- If this definition is present in a class definition, you can use the function ASetExternalDataSize to set the amount of external data not visible to the garbage collector that is attached to an object of the class. If this definition is included, you must also include the #f method in the class. A_EXTERNAL_DATA() can only be used in classes that do not inherit another class (other than std::Object). This macro also creates a single member slot in the class.
Defining initialization and finalization methods
The following special method names can be used for controlling the life cycle of objects:
#i
The method "#i", if defined, will be called during object construction, before calling create, at least when both of these conditions are satisfied:
- An object of a subclass of the original class (that defined #i) is being constructed (note that it is not called when an object of the original class is being constructed).
- The subclass (or one of its superclasses) has overridden the create method defined in the original class.
This method should be used to initialize any binary data attached to the object. Note that both the #i method and the original create method can be called during object construction: the create method defined in the subclass may call the original create method using the super keyword.
#f
The method "#f", if defined, is called just before an object is freed by the garbage collector. This method, also called the finalizer, is special in several ways, including these:
- The AThread *t argument is always NULL when this method is called. Any API functions that require an AThread *t argument cannot be used within a finalizer method.
- The function frame only has the self value (frame[0]) defined. The function should not accept any arguments or allocate temporary values in the frame.
Note also that when #f is called, the object might already be partially freed, and the heap might be in an inconsistent state. This means that:
- The method #f must not allocate garbage-collected memory.
- The method #f must not access the contents of any object in the garbage collected heap, except for the binary data stored in self and blocks allocated using AAllocMemFixed that are directly referenced by self.
In practice, the only thing that should be done in a #f method is to free any external resources (e.g. operating system resources, memory allocated using malloc) so that they will not be leaked when the object is freed. The method should check if any data has been left uninitialized, since it is possible that #f is called before the object has been fully constructed.
As a side effect of defining #f, the member slot 0 is reserved by the implementation for internal use. See Accessing member variable slots directly for additional information.
Note: You can only define #f in a class if the class inherits from std::Object or if one of the superclasses also defines #f.
Accessing binary data
The following API functions can be used to access binary data in instances. They are described in the API function reference, linked below.
All of these functions require the offset of the data to be accessed. This first A_BINARY_DATA definition in a class gets the offset 0, and additional A_BINARY_DATA definitions get larger offsets. Binary data defined in subclasses always gets larger offsets that data defined in superclasses.
For example, consider class X that inherits from Y. Y defines a single binary data block of 8 bytes and X defines two blocks sized 4 and 16 bytes. The following table lists the offsets and sizes of these blocks in objects of class X:
Offset | Size |
---|---|
0 | 8 |
8 (0 + 8) | 4 |
12 (0 + 8 + 4) | 16 |
Alignment of binary data
Binary data at offset 0 is aligned to a 32-bit boundary (if using a 32-bit AValue type) or to a 64-bit boundary (if using a 64-bit AValue type). If there is more than a single A_BINARY_DATA declaration in in a class hierarchy, the offset of a binary data declaration other than the first declaration (which always has offset 0) is simply the sum of the data sizes in previous declarations. Alignment is thus enforced only for the first binary data declaration in a class hierarchy. Example of unaligned binary data:
A_MODULE(...) A_CLASS("Example") A_BINARY_DATA(1) /* Offset 0 (32/64-bit aligned) */ A_BINARY_DATA(2) /* Offset 1 */ A_BINARY_DATA(8) /* Offset 3 */ ... A_END_CLASS() A_END_MODULE()
Since some architectures do not allow accessing unaligned data items directly, you either have to enforce the alignment manually by rounding the sizes of data blocks up, for example, or by copying the data to/from another properly aligned location using ADataPtr and memcpy.
You can use the macro A_VALUE_BITS to query the current size of the AValue type. Its value is currently either 32 or 64.
Example
This example defines a simple class that is a wrapper for data allocated using malloc. Very little error checking is performed to highlight the key issues. A more robust implementation would at least check that method parameters are in a valid range to avoid memory corruption.
#include <alore/alore.h> #include <stdlib.h> #include <stdio.h> #include <string.h> static AValue Create(AThread *t, AValue *frame) { int size; char *ptr; size = AGetInt(t, frame[1]); ptr = malloc(size); if (ptr == NULL) return ARaiseMemoryError(t); memset(ptr, 0, size); ASetData_M(frame[0], 0, char *, ptr); ASetExternalDataSize(t, frame[0], size); return frame[0]; } static AValue Finalize(AThread *t, AValue *frame) { char *ptr = AGetData_M(frame[0], 0, char *); free(ptr); /* Note that ptr may be 0. */ return ANil; } static AValue Set(AThread *t, AValue *frame) { int index = AGetInt(t, frame[1]); int value = AGetInt(t, frame[2]); char *ptr = AGetData_M(frame[0], 0, char *); ptr[index] = value; return ANil; } static AValue Get(AThread *t, AValue *frame) { int index = AGetInt(t, frame[1]); char *ptr = AGetData_M(frame[0], 0, char *); return AMakeInt(t, ptr[index]); } A_MODULE(malloc, "malloc") A_CLASS("Malloc") A_EXTERNAL_DATA() A_BINARY_DATA(sizeof(char *)) A_METHOD("create", 1, 0, Create) A_METHOD("#f", 0, 0, Finalize) A_METHOD("set", 2, 0, Set) A_METHOD("get", 1, 0, Get) A_END_CLASS() A_END_MODULE()