Libundo

Introduction

Multilevel undo is unquestionably a very valuable tool which allows users to fearlessly explore changes to their data without worry about the details of not being able to recover from mistakes. It is no wonder that many of today's users expect their applications to seamlessly support multilevel undo/redo.

Unfortunately for users, many application developers implement undo/redo poorly or not at all. When it is implemented, it is often done so in an ad hoc fasion, with each possible program operation having associated code to perform undo and redo actions. This is additional code which must be written, debugged and supported. Some application writers simplify the problem of managing undo information by only implementing single level undo, or by not implementing any sort of undo! Because of the complexity of properly managing undo/redo, the users suffer.

Libundo is a simple, easy-to-use library which manages recording and playback of undo/redo information for application developers. It is designed to be simple to plug in to existing applications and require only a minimal amount of support code be written to support multi-level undo/redo. Libundo handles all the details of determining what has changed after an undoable action is performed, recording that information and saving it for use when an undo is performed. Libundo makes it easy for application writers to give end users what they want.

Libundo is available under the GNU GPL and is not tied to any GUI libraries or application frameworks. Libundo has a C language interface. Libundo does require the ability to do Unix-style memory mapping.

Libundo Basics

Libundo works by managing an alternate memory allocation heap. Through the functions undo_malloc() and undo_free() you are able to allocate and free memory, just like the standard functions malloc() and free(). The difference is that through the undo_snapshot() function call, the current state of the memory heap can be recorded and saved for later restoration.

It is expected that you will use undo_malloc() to allocate all memory used to store state of the document your application operates on. Immediately after you initialize a new document, you will call undo_snapshot() to record the original document state. Similarly, as the last step of each operation which modifies your document data, you will call undo_snapshot() to record the changes you have made while performing that action.

At a later time, when the application user requests that the last action be undone, you will call the function undo_undo() and Libundo will use the recorded snapshots to undo the one action. When the user requests a redo, you will call undo_redo(), and one undone action will be redone. Note that when you call undo_undo() or undo_redo(), undo memory will be allocated and freed as recorded by your previous snapshots.

Session Management

Before you can allocate any undoable memory, you must first create an undo session and set a memory limit. The undo_new() function allows you to create an undo session and undo_destroy() will destroy the active undo session.

UNDO *undo_new(void);
int undo_destroy(UNDO *undo);

If your application has multiple documents open, you will likely have a distinct undo session for each document.

int undo_set_history_logical(UNDO *undo, int onoff);
This function sets history type. If the onoff parameter is 1, a new snapshot performed after a series of undo operations will erase all redoable memory past the current point. Otherwise, the new snapshot will be appended to the list of existing snapshots. The latter case corresponds to the libundo-0.7.0 behavior and is the default as of now, but this may change in the future, so please don't rely upon the default and set the history type explicitly.

Finally, each undo session has an associated memory limit. Use undo_set_memory_limit() to set this limit.

int undo_set_memory_limit(UNDO *undo, size_t max_memory);

This limit is used to determine how many recorded snapshots are kept by the undo session. Libundo will discard old snapshots as necessary to keep its snapshot history under the specified limit. You must specify a memory limit before you make any calls to undo_snapshot().

Memory Management

The interface and operation of undo_malloc(), undo_realloc() and undo_free() should be familiar to any C language programmer. They are designed to be drop-in replacements for malloc(), realloc() and free() when operating on undoable data.

void *undo_malloc(UNDO *undo, size_t size);
void *undo_realloc(UNDO *undo, void *mem, size_t size);
void undo_free(UNDO *undo, void *mem);

There shouldn't be any surprises here. Each of these functions behaves the same as its C library counterpart, except it uses the memory heap of its undo session, passed by the first argument.

The only memory management function without a C library counterpart is undo_snapshot().

int undo_snapshot(UNDO *undo);

undo_snapshot() simply records state of the given undo session's memory heap, including information about what blocks are currently allocated. It will honor the memory limit set by undo_set_memory_limit(). This recorded snapshot can be played back later by undo_undo().

Undo/Redo

After more than one snapshot has been recorded by undo_snapshot(), undo_undo() and undo_redo() can be used.

int undo_undo(UNDO *undo);
int undo_redo(UNDO *undo);

Both functions simply restore the state of the given session's memory heap to that which is represented by the appropriate snapshot. Memory allocated by undo_malloc() may be freed or reallocated as a result of calling undo_undo() or undo_redo(). Be careful that you don't have references to undoable memory which might be freed in non-undoable memory, or vise versa.

The functions undo_get_undo_count() and undo_get_redo_count() are also provided.

unsigned undo_get_undo_count(const UNDO *undo);
unsigned undo_get_redo_count(const UNDO *undo);

These functions simply return the numbers of times your application can expect to undo or redo undoable actions from the current point in the undo stack of the given session. These can be useful to set undo/redo menu items and buttons in your GUI to insensitive when there are no undos or redos possible.

Error Recovery

The above Libundo functions which return the type int all return standard Libundo error codes.

	#define UNDO_NOERROR        0   /* No error */
	#define UNDO_BADPARAM       1   /* Bad parameter passed to function */
	#define UNDO_NOMEM          2   /* Not enough memory */
	#define UNDO_NOSESSION      3   /* No undo session */
	#define UNDO_NODO           4   /* Nothing to undo/redo */
	#define UNDO_NOLIMIT        5   /* No memory limit specified */

The Libundo functions which return pointers will return NULL when Libundo experiences an error. In the case of either type of function, undo_get_errcode() can be used to retrieve the error code for the last error.

int undo_get_errcode(UNDO *undo);

Note that calling undo_get_errcode() will reset the state of Libundo such that undo_get_errcode() will return UNDO_NOERROR the next time it is called if there hasn't been another error.

As a convenience to you, Libundo provides a function for converting from a numeric undo error code to an English string describing the error.

char *undo_strerror(int errcode);

undo_strerror() will return a null terminated string with a newline at the end suitable for printing in the case of an error.

The History of Libundo

Libundo was written by Matt Kimball. The first and the only public release, 0.7.0, was made by him on October, 1999. In 2005, Evgeny Stambulchik revived the project.

The Future of Libundo

See the Roadmap document for an approximate development plan. If you've got an opinion about which extensions to Libundo would be useful and which are unnecessary, make your voice heard.