LibUCW is equipped with a general system for keeping track of resources (allocated memory, open files, …) and freeing them when requested to.
The resource tracker can be used either separately (in the form of explicitly managed resource pools) or within a transactional layer, which offers exceptions similar to those in higher-level languages. An exception then rolls back the transaction, freeing all temporary resources allocated within the transaction.
Resource pools: ucw/resource.h
A resource pool contains a stack of resources. When a new resource is created, it is pushed onto the stack. When freeing the pool, the resources are freed in the opposite order, which allows a resource refer to data of previously created resources.
A resource can be also freed separately (which unlinks it from the pool), or detached from the pool (which keeps the real resource, but forgets its meta-data, so the resource is no longer tracked).
In many cases, a combination of both methods is needed: some resources are marked as temporary, while some others are permanent. When the an operation is completed successfully (and rp_commit() is called), all temporary resources are freed and the permanent ones detached. When the operation fails, rp_delete() deletes all resources. By default, all resources are created as temporary. You can make a resource permanent by calling res_permanent(), or change the default in resource->default_res_flags.
For each thread, LibUCW remembers the currently active resource pool. One pool can be used for at most one thread at a time. All functions which create resources do so in the active pool. All other functions operating on resources work on both active and in-active pools.
struct respool { clist resources; const char *name; struct mempool *mpool; // If set, resources are allocated from the mempool, otherwise by xmalloc() struct resource *subpool_of; uint default_res_flags; // RES_FLAG_xxx for newly allocated resources };
A resource pool. It contains a name of the pool (which is printed in all debugging dumps, otherwise it is not used) and a bunch of fields for internal use.
struct resource { cnode n; struct respool *rpool; uint flags; // RES_FLAG_xxx const struct res_class *rclass; void *priv; // Private to the class // More data specific for the particular class can follow };
Each resource is represented by this structure. It is linked to a resource pool it belongs to. It contains a pointer to a resource class (which describes how to handle the resource) and data private to the resource class.
enum resource_flags { RES_FLAG_TEMP = 1, // Resource is temporary RES_FLAG_XFREE = 2, // Resource structure needs to be deallocated by xfree() };
Resource flags
struct respool *rp_new(const char *name, struct mempool *mp);
Creates a new resource pool. If a memory pool is given, meta-data of all resources will be allocated from this pool. Otherwise, they will be malloc’ed.
void rp_delete(struct respool *rp);
Deletes a resource pool, freeing all resources.
void rp_detach(struct respool *rp);
Deletes a resource pool, detaching all resources.
void rp_commit(struct respool *rp);
Deletes a resource pool. Temporary resources are freed, stable resources are detached.
void rp_dump(struct respool *rp, uint indent);
Prints out a debugging dump of a pool to stdout.
static inline struct respool *rp_current(void);
Returns a pointer to the currently active resource pool or NULL, if none exists.
static inline struct respool *rp_switch(struct respool *rp);
Makes the given resource pool active; returns a pointer to the previously active pool or NULL, if there was none. Calling with rp equal to NULL deactivates the pool.
void res_dump(struct resource *r, uint indent);
Prints out a debugging dump of the resource to stdout.
void res_free(struct resource *r);
Frees a resource, unlinking it from its pool. When called with a NULL pointer, it does nothing, but safely.
void res_detach(struct resource *r);
Unlinks a resource from a pool and releases its meta-data. However, the resource itself is kept. When called with a NULL pointer, it does nothing, but safely.
static inline void res_temporary(struct resource *r);
Marks a resource as temporary (sets RES_FLAG_TEMP).
static inline void res_permanent(struct resource *r);
Marks a resource as permanent (clears RES_FLAG_TEMP).
Resource classes
A resource class describes how to handle a particular type of resources. Most importantly, it defines a set of (optional) callbacks for performing operations on the resources:
-
dump() should print a description of the resource used for debugging to the standard output. The description should end with a newline character and in case of a multi-line description, the subsequent lines should be indented by indent spaces.
-
free() frees the resource; the struct resource is freed automatically afterwards.
-
detach() breaks the link between the struct resource and the real resource; the struct resource is freed automatically afterwards, while the resource continues to live.
The following functions are intended for use by the resource classes only.
struct res_class { const char *name; // The name of the class (included in debugging dumps) void (*detach)(struct resource *r); // The callbacks void (*free)(struct resource *r); void (*dump)(struct resource *r, uint indent); uint res_size; // Size of the resource structure (0=default) };
The structure describing a resource class.
static inline struct resource *res_init(struct resource *r, const struct res_class *rc, void *priv);
Initialize a pre-allocated buffer to the specific class of resource, setting its private data to priv. This resource can be added to the current pool by res_add().
void res_add(struct resource *r);
Links a pre-initialized resource to the active pool.
void res_drop(struct resource *r);
Unlinks a resource from a pool and releases its meta-data. Unlike res_detach(), it does not invoke any callbacks. The caller must make sure that no references to the meta-data remain, so this is generally safe only inside resource class code.
static inline struct resource *res_new(const struct res_class *rc, void *priv);
Creates a new resource of the specific class, setting its private data to priv. Dies if no resource pool is active.
Pre-defined resource classes
struct resource *res_for_fd(int fd);
Creates a resource that closes a given file descriptor.
void *res_malloc(size_t size, struct resource **ptr) LIKE_MALLOC;
Allocates memory and creates a resource for it.
void *res_malloc_zero(size_t size, struct resource **ptr) LIKE_MALLOC;
Allocates zero-initialized memory and creates a resource for it.
void *res_realloc(struct resource *res, size_t size);
Re-allocates memory obtained by res_malloc() or res_malloc_zero().
struct resource *res_subpool(struct respool *rp);
Converts the resource pool rp to a resource inside the current resource pool (i.e., its sub-pool). You can delete the sub-pool either by freeing this resource, or by calling rp_delete() on it, which removes the resource automatically.
struct resource *res_mempool(struct mempool *mp);
Creates a resource for the specified memory pool.
struct resource *res_eltpool(struct eltpool *ep);
Creates a resource for the specified element pool.
Transactions: ucw/trans.h
Upon the resource pools, a transactional mechanism is built. A transaction consists of a piece of code and a resource pool for temporary objects created by the code. Whenever the transaction is running, this pool is set as current. You are allowed to switch to a different pool, but please do so carefully.
When a transaction ends, the pool is destroyed and the previous active pool is popped off the transaction stack. The fate of the resources inside the pool depends on the operation used to end the transaction:
-
commit — permanent resources are detached from the pool, temporary resources are freed
-
rollback — all resources are freed
-
fold — instead of destroying the pool, it is added as a subpool to the parent transaction (which must exist)
A transaction is tied to a thread which has created it. A transaction can create a sub-transaction, so every thread keeps a stack of running transactions in its per-thread data. Calling trans_init() is optional, but trans_cleanup() should be used before a thread exits in order to free resources used by transaction system.
Each transaction also includes a memory pool, from which all temporary structures (including all resources created by the transaction) are allocated. Feel free to allocate your temporary data from this pool, too; they will be freed when the transaction is committed or rolled back. When the transaction ends with a fold, this pool gets included inside the parent transaction’s pool.
(More precisely, there is actually a shared transaction pool per thread and the transaction logic uses mp_push() and mp_pop() to keep a stack of per-transaction data.)
Exceptions
Transactions are commonly used together with exceptions (which are similar to how exceptions work in other languages, but they differ in subtle details, so please read carefully). When a failure condition of some kind is detected, an exception is raised ("thrown" is also sometimes used). It involves creating an exception object and jumping out of the transaction by a longjmp(). The exception object (struct exception) contains an identification of the error and possibly additional data.
Usually, creation of an transaction and handling of exceptions is done using helper macros (it is not strictly necessary, but highly recommended):
TRANS_TRY { // Code that runs inside the transaction. } TRANS_CATCH(x) { // When an exception is raised, execution continues here. } TRANS_END;
The code inside the transaction ends with an implicit trans_commit(). If you want to end the transaction in a different way, you can do so, but you need to use a break statement to skip the implicit commit.
The exception handling code gets a local variable x pointing to the exception object. When the exception is handled (for example, an error message is logged), trans_caught() is called automatically, which rolls back the transaction and frees all its resources. Again, you can use the break statement to skip this.
Alternatively, when you are in a nested transaction, you can throw a different exception or re-throw the original one. This raises an exception in the context of the parent transaction. In this case, the child transaction is not rolled back, but its pools are folded as sub-pools of the parent transaction and kept until trans_caught() is called finally.
When an exception is thrown outside a transaction, it is converted to a plain die().
Memory management and lifetime of various objects and pools deserve special attention, as usually when non-local jumps are taking place. When an exception is raised, the exception structure is allocated from the memory pool of the current transaction. When the exception is propagated through the stack of transactions, no transaction is ever rolled back — all of them are folded and their pools remain accessible until trans_caught() is called at the end. Therefore exceptions can carry pointers to the objects which have failed without a risk of the object becoming invalid. However, you need to avoid pointing to on-stack data like local variables of functions, because these are of course destroyed during the longjmp().
Functions and structures
struct trans { struct trans *prev_trans; struct mempool_state *trans_pool_state; struct respool *rpool; struct respool *prev_rpool; struct exception *thrown_exc; jmp_buf jmp; };
A structure describing a transaction. All fields are for internal use only.
void trans_init(void);
Initializes the transaction system for the current thread. Called automatically as needed.
void trans_cleanup(void);
Frees memory occupied by the transaction system pools for the current thread.
struct trans *trans_open(void);
Creates a new transaction. Used inside TRANS_TRY.
struct trans *trans_get_current(void);
Get a pointer to the currently running transaction, or NULL if there is none.
void trans_commit(void);
Commits the current transaction.
void trans_rollback(void);
Rolls back the current transaction.
void trans_fold(void);
Folds the current transaction to its parent.
void trans_dump(void);
Prints out a debugging dump of the transaction stack to stdout.
struct exception { const char *id; // Hierarchical identifier of the exception const char *msg; // Error message to present to the user void *object; // Object on which the exception happened // More data specific for the particular `id' can follow };
Data associated with an exception. Usually, this structure is created by calling trans_throw(), but if you want to pass more data, you can create your own exception and throw it using trans_throw_exc().
void trans_throw(const char *id, void *object, const char *fmt, ...) FORMAT_CHECK(printf,3,4) NONRET;
Creates an exception and throws it. The error message can contain printf-like formatting.
void trans_vthrow(const char *id, void *object, const char *fmt, va_list args) NONRET;
A va_list variant of trans_throw().
void trans_throw_exc(struct exception *x) NONRET;
Throw an already constructed exception (or re-throw an exception you have caught).
void trans_caught(void);
Declare the current exception caught and roll back the current transaction. Called from TRANS_END.
struct exception *trans_current_exc(void);
Return the exception in flight, or NULL if there is none.