PMEMOBJ_TX_PROCESS(3)

pmemobj_tx_abort(3) β€” straight outta the PMDK Programmer's Manual, and bestie, this one is a whole entire saga. It's long, so buckle up, we're doing this in parts. πŸ’–


✨ NAME ✨

Okay so here's the whole squad of functions and macros this page covers, all of them living under the umbrella of transactional object manipulation:

  • pmemobj_tx_stage()
  • pmemobj_tx_begin()
  • pmemobj_tx_lock()
  • pmemobj_tx_xlock()
  • pmemobj_tx_abort()
  • pmemobj_tx_commit()
  • pmemobj_tx_end()
  • pmemobj_tx_errno()
  • pmemobj_tx_process()
  • TX_BEGIN_PARAM()
  • TX_BEGIN_CB()
  • TX_BEGIN()
  • TX_ONABORT
  • TX_ONCOMMIT
  • TX_FINALLY
  • TX_END
  • pmemobj_tx_log_append_buffer()
  • pmemobj_tx_xlog_append_buffer()
  • pmemobj_tx_log_auto_alloc()
  • pmemobj_tx_log_snapshots_max_size()
  • pmemobj_tx_log_intents_max_size()
  • pmemobj_tx_set_user_data()
  • pmemobj_tx_get_user_data()
  • pmemobj_tx_set_failure_behavior()
  • pmemobj_tx_get_failure_behavior()

That's the cast. πŸŽ€


🌸 SYNOPSIS 🌸

#include <libpmemobj.h>

enum pobj_tx_stage pmemobj_tx_stage(void);

int pmemobj_tx_begin(PMEMobjpool *pop, jmp_buf *env, enum pobj_tx_param, ...);
int pmemobj_tx_lock(enum tx_lock lock_type, void *lockp);
int pmemobj_tx_xlock(enum tx_lock lock_type, void *lockp, uint64_t flags);
void pmemobj_tx_abort(int errnum);
void pmemobj_tx_commit(void);
int pmemobj_tx_end(void);
int pmemobj_tx_errno(void);
void pmemobj_tx_process(void);

TX_BEGIN_PARAM(PMEMobjpool *pop, ...)
TX_BEGIN_CB(PMEMobjpool *pop, cb, arg, ...)
TX_BEGIN(PMEMobjpool *pop)
TX_ONABORT
TX_ONCOMMIT
TX_FINALLY
TX_END

int pmemobj_tx_log_append_buffer(enum pobj_log_type type, void *addr, size_t size);
int pmemobj_tx_xlog_append_buffer(enum pobj_log_type type, void *addr, size_t size, uint64_t flags);
int pmemobj_tx_log_auto_alloc(enum pobj_log_type type, int on_off);
size_t pmemobj_tx_log_snapshots_max_size(size_t *sizes, size_t nsizes);
size_t pmemobj_tx_log_intents_max_size(size_t nintents);

void pmemobj_tx_set_user_data(void *data);
void *pmemobj_tx_get_user_data(void);

void pmemobj_tx_set_failure_behavior(enum pobj_tx_failure_behavior behavior);
enum pobj_tx_failure_behavior pmemobj_tx_get_failure_behavior(void);

πŸ’… DESCRIPTION πŸ’…

So like, the non-transactional functions and macros that get described over in pmemobj_alloc(3), pmemobj_list_insert(3) and POBJ_LIST_HEAD(3)? They only promise atomicity for one single operation on an object. One. That's it. πŸ™ˆ

But the second you want something more complex β€” multiple operations on one object, or allocating and modifying a whole bunch of objects β€” then data consistency and fail-safety are only gonna happen if you use atomic transactions. It's giving "no shortcuts, bestie".

A transaction is basically a series of operations on persistent memory objects where either everything happens, or nothing happens. Fr fr. And the important part: if execution of a transaction gets interrupted by a power failure or a system crash, it is guaranteed that after the system restarts, every change made as part of that uncompleted transaction gets rolled back β€” restoring the memory pool to the consistent state it was in the moment the transaction started. Iconic behaviour honestly. πŸ‘‘

⚠️ Okay important vibe check: transactions do not give you atomicity with respect to other threads. All the modifications you make inside a transaction are immediately visible to other threads. So it's your job, the application's job, to implement a proper thread synchronization mechanism. The library is not doing that for you, bestie. 😭

Each thread can only have one transaction open at a time β€” but that transaction is allowed to be nested. Nested transactions get flattened. Committing a nested transaction does not commit the outer one; however, errors in a nested transaction propagate all the way up to the outermost level, and that means the entire transaction gets interrupted. πŸ’€

Every transaction is visible only to the thread that started it. No other threads can add operations, commit, or abort a transaction that another thread initiated. But multiple threads can have transactions open on a given memory pool at the same time. 🌈

Please go read the CAVEATS section down below for the known limitations of the transactional API. Don't skip it, I'm serious.


πŸ¦‹ Transaction stages πŸ¦‹

The pmemobj_tx_stage() function returns the current transaction stage for a thread. Stages only ever change via the pmemobj_tx_*() functions. Here's the lineup:

  • TX_STAGE_NONE β€” no open transaction in this thread
  • TX_STAGE_WORK β€” transaction in progress
  • TX_STAGE_ONCOMMIT β€” successfully committed
  • TX_STAGE_ONABORT β€” starting the transaction failed or the transaction aborted
  • TX_STAGE_FINALLY β€” ready for clean up

πŸ’« pmemobj_tx_begin() πŸ’«

The pmemobj_tx_begin() function starts a new transaction in the current thread. If you call it while a transaction is already open, it starts a nested transaction instead. The caller can use the env argument to hand over a pointer to a calling environment that'll be restored in case the transaction aborts β€” and that info has to be provided by the caller using the setjmp(3) macro.

A new transaction can only be started if the current stage is TX_STAGE_NONE or TX_STAGE_WORK. If it works out, the stage changes to TX_STAGE_WORK. If it doesn't, the stage changes to TX_STAGE_ONABORT. No in-between, that's the deal. ✨

Optionally you can pass a list of parameters for the transaction. Each parameter is a type followed by however many values that type needs. Currently there are 4 types:

  • TX_PARAM_NONE β€” used as a termination marker. No following value.
  • TX_PARAM_MUTEX β€” followed by one value, a pmem-resident PMEMmutex
  • TX_PARAM_RWLOCK β€” followed by one value, a pmem-resident PMEMrwlock
  • TX_PARAM_CB β€” followed by two values: a callback function of type pmemobj_tx_callback, and a void pointer

Using TX_PARAM_MUTEX or TX_PARAM_RWLOCK makes the specified lock get acquired at the beginning of the transaction. TX_PARAM_RWLOCK acquires the lock for writing. It's guaranteed that pmemobj_tx_begin() will acquire all locks prior to successful completion, and they'll be held by the current thread until the outermost transaction is finished. Locks get taken in order, left to right. And to avoid deadlocks, the user is responsible for proper lock ordering. That one's on you, bestie. 🫢

TX_PARAM_CB registers the specified callback function to be executed at each transaction stage. For TX_STAGE_WORK, the callback runs prior to commit. For all other stages, the callback runs as the first operation after a stage change. It'll also be called after each transaction; in that case the stage parameter will be set to TX_STAGE_NONE. And pmemobj_tx_callback must be compatible with:

void func(PMEMobjpool *pop, enum pobj_tx_stage stage, void *arg)

Here pop is the pool identifier used in pmemobj_tx_begin(), stage is the current transaction stage, and arg is the second parameter of TX_PARAM_CB. Without considering transaction nesting, you can think of this mechanism as an alternative way to execute code between stages (instead of TX_ONCOMMIT, TX_ONABORT, etc). BUT β€” and this is the plot twist β€” there are 2 significant differences when nested transactions are in play:

  • The registered function is executed only in the outermost transaction, even if it was registered in an inner transaction.
  • There can be only one callback in the entire transaction, meaning the callback cannot be changed in an inner transaction.

Also note: TX_PARAM_CB does not replace the TX_ONCOMMIT, TX_ONABORT, etc. macros. You can totally use them together β€” the callback just gets executed before a TX_ONCOMMIT, TX_ONABORT, etc. section. πŸ’…

TX_PARAM_CB is the move when the code dealing with transaction stage changes is shared between multiple users, or when it must be executed only in the outer transaction. For example it can be super useful when the application has to synchronize persistent and transient state. Obsessed, actually.


πŸ”’ pmemobj_tx_lock() and pmemobj_tx_xlock() πŸ”’

The pmemobj_tx_lock() function acquires the lock lockp of type lock_type and adds it to the current transaction. lock_type may be TX_LOCK_MUTEX or TX_LOCK_RWLOCK; lockp must be of type PMEMmutex or PMEMrwlock, respectively. If lock_type is TX_LOCK_RWLOCK, the lock is acquired for writing. If the lock is not successfully acquired, the function returns an error number. This function must be called during TX_STAGE_WORK.

The pmemobj_tx_xlock() function behaves exactly the same as pmemobj_tx_lock() when flags equals POBJ_XLOCK_NO_ABORT. When flags equals 0 and the lock is not successfully acquired, the transaction is aborted. flags is a bitmask of the following values:

  • POBJ_XLOCK_NO_ABORT β€” if the function does not end successfully, do not abort the transaction.

πŸŽ€ pmemobj_tx_abort() πŸŽ€

pmemobj_tx_abort() aborts the current transaction and causes a transition to TX_STAGE_ONABORT. If errnum is equal to 0, the transaction error code is set to ECANCELED; otherwise, it is set to errnum. This function must be called during TX_STAGE_WORK.


⭐ pmemobj_tx_commit() ⭐

The pmemobj_tx_commit() function commits the current open transaction and causes a transition to TX_STAGE_ONCOMMIT. If called in the context of the outermost transaction, all the changes may be considered as durably written upon successful completion. This function must be called during TX_STAGE_WORK.


🧚 pmemobj_tx_end() 🧚

The pmemobj_tx_end() function performs a cleanup of the current transaction. If called in the context of the outermost transaction, it releases all the locks acquired by pmemobj_tx_begin() for outer and nested transactions. If called in the context of a nested transaction, it returns to the context of the outer transaction in TX_STAGE_WORK, without releasing any locks. The pmemobj_tx_end() function can be called during TX_STAGE_NONE if you transitioned to that stage using pmemobj_tx_process(). If it's not already in TX_STAGE_NONE, it causes the transition to TX_STAGE_NONE.

πŸ’€ pmemobj_tx_end must always be called for each pmemobj_tx_begin(), even if starting the transaction failed. And it must not be called during TX_STAGE_WORK. Not negotiable.

🌸 pmemobj_tx_errno() 🌸

The pmemobj_tx_errno() function returns the error code of the last transaction.


πŸ’œ pmemobj_tx_process() πŸ’œ

The pmemobj_tx_process() function performs the actions associated with the current stage of the transaction, and makes the transition to the next stage. It must be called in a transaction. The current stage must always be obtained by a call to pmemobj_tx_stage(). Here's the full stage-flow glow-up that pmemobj_tx_process() performs:

  • TX_STAGE_WORK -> TX_STAGE_ONCOMMIT
  • TX_STAGE_ONABORT -> TX_STAGE_FINALLY
  • TX_STAGE_ONCOMMIT -> TX_STAGE_FINALLY
  • TX_STAGE_FINALLY -> TX_STAGE_NONE
  • TX_STAGE_NONE -> TX_STAGE_NONE
πŸ™ˆ pmemobj_tx_process() must not be called after calling pmemobj_tx_end() for the outermost transaction. Just don't.

In addition to all the API above, libpmemobj(7) offers a much more intuitive way of building transactions using the set of macros described below. When you use these macros, the complete transaction flow looks like this:

TX_BEGIN(Pop) {
    /* the actual transaction code goes here... */
} TX_ONCOMMIT {
    /*
     * optional - executed only if the above block
     * successfully completes
     */
} TX_ONABORT {
    /*
     * optional - executed only if starting the transaction fails,
     * or if transaction is aborted by an error or a call to
     * pmemobj_tx_abort()
     */
} TX_FINALLY {
    /*
     * optional - if exists, it is executed after
     * TX_ONCOMMIT or TX_ONABORT block
     */
} TX_END /* mandatory */

okay bestie, and here's the whole family of starters:

TX_BEGIN_PARAM(PMEMobjpool *pop, ...)
TX_BEGIN_CB(PMEMobjpool *pop, cb, arg, ...)
TX_BEGIN(PMEMobjpool *pop)

TX_BEGIN_PARAM(), TX_BEGIN_CB() and TX_BEGIN() all kick off a brand new transaction exactly the way pmemobj_tx_begin() does, with one lil' glow-up: instead of using whatever environment buffer you (the caller) hand over, they set up their own local jmp_buf buffer and use it to catch the transaction abort. iconic behavior honestly πŸ’…

  • TX_BEGIN() starts a transaction with no options at all. simple queen.
  • TX_BEGIN_PARAM is the one you want when you need to grab locks before the transaction even starts (multi-threaded programs, looking at you πŸ‘€) or when you want to set up a transaction stage callback.
  • TX_BEGIN_CB is literally just a wrapper around TX_BEGIN_PARAM that validates the callback signature for you. (and for the vintage girlies, there's also a TX_BEGIN_LOCK macro which is an alias for TX_BEGIN_PARAM πŸ’–)

Every single one of these macros must be followed by a block of code containing all the operations that are gonna be performed atomically. no skipping, no shortcuts, it's giving committed ✨


TX_ONABORT opens a block of code that only runs if starting the transaction fails because of an error in pmemobj_tx_begin(), or if the transaction gets aborted. it's technically optional but fr fr, in practice you should not omit it. 😭

now, if you actually want your application to crash when a transaction aborts and you have no TX_ONABORT section β€” mood, dramatic β€” you can define the POBJ_TX_CRASH_ON_NO_ONABORT macro before including <libpmemobj.h>. that gives you a default TX_ONABORT section that just calls abort(3). πŸ’€

TX_ONCOMMIT opens a block that only runs if the transaction successfully commits β€” meaning the code inside the TX_BEGIN() block was not interrupted by an error or by a call to pmemobj_tx_abort(). optional, but cute. πŸŽ€

TX_FINALLY opens a block that runs no matter what β€” commit or abort, doesn't matter, she shows up. optional. 🌸

TX_END cleans up and closes the transaction started by the TX_BEGIN() / TX_BEGIN_PARAM() / TX_BEGIN_CB() macros. this one is mandatory, bestie β€” every transaction has to be terminated with this macro, periodt. if the transaction was aborted, errno is set appropriately. πŸ‘‘


πŸ”₯ TRANSACTION LOG TUNING πŸ”₯

so from the libpmemobj implementation's point of view, there are exactly two types of operations in a transaction:

  • snapshots, where the action must be persisted immediately,
  • intents, where the action can be persisted at the transaction commit phase.

pmemobj_tx_add_range(3) and all its lil' variants belong to the snapshots group. 🫢

pmemobj_tx_alloc(3) (with its variants), pmemobj_tx_free(3), pmemobj_tx_realloc(3) (with its variants) and pmemobj_tx_publish(3) belong to the intents group. and ngl, here's the tea:

  • even though pmemobj_tx_alloc() allocates memory immediately, it only modifies the runtime state and postpones persistent memory modifications to the commit phase.
  • pmemobj_tx_free(3) can't free the object immediately because of possible transaction rollback, so it postpones both the action and the persistent memory modifications to the commit phase.
  • pmemobj_tx_realloc(3) is just a combination of those two. combo meal πŸ“
  • pmemobj_tx_publish(3) postpones reservations and deferred frees to the commit phase.

those two types of operations (snapshots and intents) require that libpmemobj builds a persistent log of operations. the intent log (also known as a β€œredo log”) is applied on commit, and the snapshot log (also known as an β€œundo log”) is applied on abort. πŸ“

when a libpmemobj transaction starts, it's not possible to predict how much persistent memory space those logs are gonna need. so libpmemobj has to internally allocate that space whenever it's needed. and this has two downsides:

  • when a transaction snapshots a lot of memory or does a lot of allocations, libpmemobj may need to do many internal allocations, which must be freed when the transaction ends β€” adding time overhead when big transactions are frequent,
  • transactions can start to fail due to not enough space for logs β€” this can be especially problematic for transactions that want to deallocate objects, as those might also fail.

to solve both of these problems, libpmemobj exposes the following functions:

  • pmemobj_tx_log_append_buffer(),
  • pmemobj_tx_xlog_append_buffer(),
  • pmemobj_tx_log_auto_alloc().

pmemobj_tx_log_append_buffer() appends a given range of memory [addr, addr + size) to the log type of the current transaction. type can be one of the two values (with meanings described above):

  • TX_LOG_TYPE_SNAPSHOT,
  • TX_LOG_TYPE_INTENT.

the range of memory must belong to the same pool the transaction is on, and must not be used by more than one thread at the same time. you can verify that second condition with the tx.debug.verify_user_buffers ctl (see pmemobj_ctl_get(3)). βœ…

the pmemobj_tx_xlog_append_buffer() function behaves exactly the same as pmemobj_tx_log_append_buffer() when flags equals zero. flags is a bitmask of the following values:

  • POBJ_XLOG_APPEND_BUFFER_NO_ABORT β€” if the function does not end successfully, do not abort the transaction.

pmemobj_tx_log_snapshots_max_size calculates the maximum size of a buffer that will be able to hold nsizes snapshots, each of size sizes[i]. application should not expect this function to return the same value between restarts. in future versions of libpmemobj this function can return smaller (because of better accuracy or space optimizations) or higher (because of higher alignment required for better performance) values. this function is independent of transaction stage and can be called both inside and outside of a transaction. if the returned value S is greater than PMEMOBJ_MAX_ALLOC_SIZE, the buffer should be split into N chunks of size PMEMOBJ_MAX_ALLOC_SIZE, where N is equal to (S / PMEMOBJ_MAX_ALLOC_SIZE) (rounded down) and the last chunk of size (S - (N * PMEMOBJ_MAX_ALLOC_SIZE)). math girlies rise πŸ™ˆ

pmemobj_tx_log_intents_max_size calculates the maximum size of a buffer that will be able to hold nintents intents. just like with pmemobj_tx_log_snapshots_max_size, application should not expect this function to return the same value between restarts, for the same reasons. this function is independent of transaction stage and can be called both inside and outside of a transaction.

pmemobj_tx_log_auto_alloc() disables (on_off set to 0) or enables (on_off set to 1) automatic allocation of internal logs of the given type. it can be used to verify that the buffer set with pmemobj_tx_log_append_buffer() is big enough to hold the log, without reaching an out-of-space scenario. πŸ’…

the pmemobj_tx_set_user_data() function associates custom volatile state, represented by pointer data, with the current transaction. this state can later be retrieved using the pmemobj_tx_get_user_data() function. if pmemobj_tx_set_user_data() was not called for the current transaction, pmemobj_tx_get_user_data() will return NULL. these functions must be called during TX_STAGE_WORK or TX_STAGE_ONABORT or TX_STAGE_ONCOMMIT or TX_STAGE_FINALLY. πŸ¦‹

pmemobj_tx_set_failure_behavior() specifies what should happen in case of an error within the transaction. it only affects functions which take a NO_ABORT flag. if pmemobj_tx_set_failure_behavior() is called with POBJ_TX_FAILURE_RETURN, a NO_ABORT flag is implicitly passed to all functions which accept this flag. if called with POBJ_TX_FAILURE_ABORT, then all functions abort the transaction (unless a NO_ABORT flag is passed explicitly). this setting is inherited by inner transactions. it does not affect any of the outer transactions. aborting on failure is the default behavior. pmemobj_tx_get_failure_behavior() returns the failure behavior for the current transaction. both pmemobj_tx_set_failure_behavior() and pmemobj_tx_get_failure_behavior() must be called during TX_STAGE_WORK. πŸ‘‘


✨ RETURN VALUE ✨

the pmemobj_tx_stage() function returns the stage of the current transaction stage for a thread.

on success, pmemobj_tx_begin() returns 0. otherwise, an error number is returned.

the pmemobj_tx_begin() and pmemobj_tx_lock() functions return zero if lockp is successfully added to the transaction. otherwise, an error number is returned.

the pmemobj_tx_xlock() function returns zero if lockp is successfully added to the transaction. otherwise, the error number is returned, errno is set, and when flags do not contain POBJ_XLOCK_NO_ABORT, the transaction is aborted. πŸ’€

the pmemobj_tx_abort() and pmemobj_tx_commit() functions return no value.

the pmemobj_tx_end() function returns 0 if the transaction was successful. otherwise it returns the error code set by pmemobj_tx_abort(). note that pmemobj_tx_abort() can be called internally by the library. πŸ‘€

the pmemobj_tx_errno() function returns the error code of the last transaction.

the pmemobj_tx_process() function returns no value.

on success, pmemobj_tx_log_append_buffer() returns 0. otherwise, the stage is changed to TX_STAGE_ONABORT, errno is set appropriately and the transaction is aborted.

on success, pmemobj_tx_xlog_append_buffer() returns 0. otherwise, the error number is returned, errno is set, and when flags do not contain POBJ_XLOG_NO_ABORT, the transaction is aborted.

on success, pmemobj_tx_log_auto_alloc() returns 0. otherwise, the transaction is aborted and an error number is returned.

on success, pmemobj_tx_log_snapshots_max_size() returns the size of the buffer. on failure it returns SIZE_MAX and sets errno appropriately.

on success, pmemobj_tx_log_intents_max_size() returns the size of the buffer. on failure it returns SIZE_MAX and sets errno appropriately. 🌈

πŸ’€ CAVEATS πŸ’€

Okay bestie, listen up, because this part is the important part and I need you to actually read it. 🫢

Transaction flow control is run by the setjmp(3) and longjmp(3) macros, and they show up in both the macro flavor and the function flavor of the API. The transaction will longjmp when a transaction aborts. Sounds cute, right? It has one major drawback, and it's spelled out in ISO C standard subsection 7.13.2.1. Basically that subsection says: values of objects with automatic storage duration, local to the function containing the setjmp invocation, that are not volatile-qualified and that got changed between the setjmp invocation and the longjmp call, are indeterminate. πŸ™ˆ Like, nobody knows what they are anymore. They're in their flop era.

Here's the example that shows exactly what I mean:

int *bad_example_1 = (int *)0xBAADF00D;
int *bad_example_2 = (int *)0xBAADF00D;
int *bad_example_3 = (int *)0xBAADF00D;
int * volatile good_example = (int *)0xBAADF00D;

TX_BEGIN(pop) {
    bad_example_1 = malloc(sizeof(int));
    bad_example_2 = malloc(sizeof(int));
    bad_example_3 = malloc(sizeof(int));
    good_example = malloc(sizeof(int));

    /* manual or library abort called here */
    pmemobj_tx_abort(EINVAL);
} TX_ONCOMMIT {
    /*
     * This section is longjmp-safe
     */
} TX_ONABORT {
    /*
     * This section is not longjmp-safe
     */
    free(good_example); /* OK */
    free(bad_example_1); /* undefined behavior */
} TX_FINALLY {
    /*
     * This section is not longjmp-safe on transaction abort only
     */
    free(bad_example_2); /* undefined behavior */
} TX_END

free(bad_example_3); /* undefined behavior */

Look at good_example β€” she said volatile and now she's thriving ✨. The bad_example_* girlies did not, and now they're giving undefined behavior 😭.

So the rule, fr fr: objects that are not volatile-qualified, are of automatic storage duration, and have been changed between the invocations of setjmp(3) and longjmp(3) β€” and yes bestie, that also means within the work section of the transaction after TX_BEGIN() β€” should not be used after a transaction abort, or should only be used with the utmost care. And "after a transaction abort" includes the code after the TX_END macro too. Not just inside the blocks. Periodt.


libpmemobj(7) is not cancellation-safe. Let me say that again for the people in the back: not cancellation-safe. πŸ’…

The good news: the pool will never be corrupted because of a canceled thread. Iconic. The less good news: other threads may stall waiting on locks that were taken by that thread. 😬 So if your application wants to use pthread_cancel(3), it must disable cancellation before calling any libpmemobj(7) APIs β€” that's pthread_setcancelstate(3) with PTHREAD_CANCEL_DISABLE β€” and then re-enable it afterwards.

And no, deferring cancellation (pthread_setcanceltype(3) with PTHREAD_CANCEL_DEFERRED) is not safe enough, because libpmemobj(7) internally may call functions that are specified as cancellation points in POSIX. So that shortcut? Not it. 🚫

One more thing, and it's lowkey a big deal: libpmemobj(7) relies on the library destructor being called from the main thread. For this reason, all functions that might trigger destruction (e.g. dlclose(3)) should be called in the main thread. Otherwise, some of the resources associated with that thread might not be cleaned up properly. 🧹 Main character energy only, okay?


🌈 SEE ALSO 🌈

Go read these, bestie, they're all part of the lore:


This page is from PMDK, dated 2025-07-24. That's the whole thing, bestie β€” you read the entire manual page and you ate. πŸ‘‘ Now go write some longjmp-safe code. πŸ’…βœ¨

πŸ“¦ where this page comes from