Skip to content
FreeDOSDeep Dive Published Updated 6 min readViews unavailable

DPMI Real-Mode Callbacks: Register, Bridge, and Release Safely

Use DPMI callback functions 0303h and 0304h with stable register storage, bounded interrupt-context work, and explicit lifetime management.

A protected-mode DOS application sometimes has to register a callback with software that executes in real mode. A mouse driver, TSR, or device interface may accept a far address and invoke it later from its own context. A protected-mode selector:offset is not a usable real-mode pointer, so DPMI provides a bridge: function INT 31h/AX=0303h allocates a real-mode callback address that switches into a protected-mode procedure, and 0304h releases it.

This bridge is a scarce, asynchronous resource. The callback can run after registration returns and may run from an interrupt-related context. The procedure, register-state buffer, and any data it touches must remain valid for the entire registration lifetime. The callback should do bounded work, preserve the interface contract, and defer complex DOS or application operations to the main event loop.

Allocate with the exact DPMI contract

Function 0303h receives a protected-mode procedure selector:offset in DS:(E)SI and a selector:offset to a 32-byte real-mode register data structure in ES:(E)DI. On success, carry is clear and CX:DX returns the segment:offset of a real-mode callback stub. On failure, carry is set and AX contains a DPMI error. The caller must not swap these segment/offset halves or treat the callback address as an ordinary pointer into protected-mode code.

The register structure uses the format defined by DPMI for real-mode calls. Initialize its fields and reserved bytes according to the specification, and use storage that remains accessible to the callback for as long as the callback exists. The contents of that structure are meaningful during callback processing; do not cache a pointer to temporary call-frame memory or assume the buffer remains a permanent log of each invocation.

allocate persistent 32-byte real-mode register structure
initialize it according to the DPMI specification
AX = 0303h
DS:SI = protected callback selector:offset
ES:DI = register-structure selector:offset
INT 31h
if carry set: release local storage and report AX
else save CX:DX as callback segment:offset
register that real-mode address with the real-mode client

The pseudocode deliberately leaves out assembler-specific register setup and callback ABI details. The callback target is a protected-mode entry point, while the returned address is what the real-mode caller receives. Keep the types and variable names distinct so a 32-bit linear address cannot accidentally be passed where the real-mode far pointer is expected.

Establish ownership before registering

Registration with the external driver creates a lifetime relationship. If the driver retains the callback address, the client must not free the DPMI callback or the memory it references until it has first disabled or unregistered the callback through that driver’s own API. Shutdown order matters: stop event delivery, wait for or exclude an in-flight callback as the interface allows, unregister the real-mode address, call DPMI function 0304h to free the bridge, then release application storage.

If the driver does not provide a safe unregister operation, the callback may need to remain allocated until process termination or until the driver is reset using a documented operation. Do not guess that closing a handle automatically discards a callback registration. A stale callback can jump into a freed protected-mode selector or access reclaimed memory, causing a crash that appears far removed from the registration code.

The DPMI host has a finite pool of callback addresses. The DPMI 1.0 specification describes callback availability and requires hosts to support a minimum number per client in its compatibility model, but clients should still handle allocation failure. Applications that repeatedly install and remove callbacks need to release each callback exactly once. A leaked callback can make a later registration fail only after several otherwise successful runs, so include repeated start/stop cycles in testing.

Keep callback work interrupt-safe

The source real-mode client may invoke the bridge because an interrupt or device event occurred. A callback should copy the small set of required registers or event fields into a bounded queue, acknowledge only what its source API requires, and return promptly. Avoid calling non-reentrant DOS services, allocating memory, waiting for disk or keyboard input, printing through an unverified console path, or taking a lock that the interrupted code already holds.

If the callback must schedule work, use a ring buffer with a clear producer/consumer contract. Define what happens when the queue is full: drop and count an event, coalesce state changes, or request a later resynchronization. Do not overwrite unread entries silently. Keep the enqueue path short and protect shared state according to the host’s interrupt and task model. An event callback that does not tolerate reentrancy can corrupt its own queue when two sources arrive before the first event is drained.

The callback’s register-save area is not a general application event queue. It is a DPMI transition structure with specified format and lifetime. Copy data you need into owned storage before returning, and do not let a pointer into the register structure escape to another thread or deferred job.

Mouse example and protected-mode boundaries

A protected-mode program can allocate a real-mode callback and pass its segment:offset to a mouse driver that accepts a user callback. The mouse driver continues to run in real mode; when it calls the bridge, the host saves the real-mode registers, switches modes, and enters the protected-mode procedure. The application can capture coordinates and button state, enqueue a compact event, then let its main loop update the UI.

The mouse driver’s own callback calling convention remains authoritative. DPMI only performs the mode transition; it does not decide which registers are meaningful, whether the driver calls with a far call or interrupt return, whether events can nest, or whether the driver expects a particular acknowledgement. Read the exact driver documentation and test the call path under the named DPMI host.

Do not place the callback inside movable or unloadable code. If the runtime can relocate code or data, pin or lock the relevant region using the host’s documented API for as long as the callback can occur. Preserve selectors and make sure the target procedure’s code segment and data segment are still valid. A callback address is not proof that every host supports every protected-mode feature the program uses inside it.

Failure analysis and acceptance checks

When allocation fails, record the DPMI error code and host identity. When an event crashes, capture whether the real-mode client is still registered, whether the selector remains allocated, the saved register values, and the callback’s nesting state. Reproduce with one callback and no unrelated TSRs, then add other resident software. A failure only after repeated installation commonly suggests a resource leak; a failure only during rapid input can indicate reentrancy or queue overrun.

Verify the following lifecycle in a test host: allocation success, callback invocation, data copy, unregister, 0304h release, and successful allocation again. Also test allocation failure and shutdown while event delivery is active if the external API permits that case. Check canaries around the event queue and register structure. Ensure the callback does not call DOS file or console APIs directly unless both the DPMI and source-driver contracts explicitly permit that context.

DPMI callbacks are powerful because they bridge two execution modes without forcing the real-mode driver to know protected-mode selectors. They remain safe only when the client owns registration ordering, stable memory, callback resource release, and the restrictions of interrupt context. Treat the bridge as an asynchronous ABI, not as a convenient far pointer.

Related:

Sources:

Comments