Version 1.0.1
MapperHeap gives an MSX-DOS2 program a general-purpose heap (allocate a block, use it, give it back) with the blocks living in memory-mapper RAM instead of in the Z80's 64 KB address space. All of the mapper RAM in the machine is available: the internal mapper, mappers in expanded slots, and cartridge mappers, all of them, up to the 4 MB per mapper that MSX-DOS2 itself can address.
MapperHeap is one relocatable module, alloc.as, with six routines and one
variable in its public interface.
A Z80 sees 64 KB at a time, and a program does not get all of it. The bottom 256 bytes hold the system vectors, among them the BDOS entry point at 0005h; page 3 holds the stack and MSX-DOS's work area. MSX-DOS loads the program at 0100h and it occupies memory upward from there, through the rest of page 0 and into page 1 and beyond, as far as its size requires. A program that needs to hold a lot of data in memory therefore runs out of address space long before it runs out of RAM. The machine may well have 512 KB, 1 MB or 4 MB of mapper RAM that the program is not using.
There are three conventional approaches, each with drawbacks: hold the data on disk and accept the loss of speed; build the program around a fixed set of hand-managed buffers; or program the mapper hardware directly, tracking which 16 KB segment is visible at any moment, throughout the program.
MapperHeap manages the mapper hardware on the program's behalf. It uses
page 2 (8000h–BFFFh) as a window, requests 16 KB segments from MSX-DOS2 as
they are needed, and divides them into blocks of the size the program
requests. A
block is named by a 4-byte far pointer that stays valid for the block's
whole life, no matter what the window currently shows. To touch the block,
the program hands the far pointer to deref (in alloc.as) and gets back an
ordinary 16-bit address inside page 2.
- Capacity is limited by installed RAM, not by address space. The amount of data a program can hold is set by the mapper RAM in the machine, not by the 64 KB the Z80 can address.
- Ordinary allocate and free. Variable-sized blocks, any mix of sizes, freed in any order. Freed blocks that happen to be next to each other in the same segment merge back into one larger block automatically, so the heap does not fragment over a long run.
- Every mapper, no configuration. Segments are requested from MSX-DOS2, which supplies whatever free segments it has, from whichever mapper. Nothing needs to be told how much RAM the machine has or where it is.
- Compatible with MSX-DOS2 and other resident software. Segments come from MSX-DOS2's own allocator, so the heap coexists with the RAM disk, with resident utilities and with MSX-DOS2 itself, and MSX-DOS2 reclaims every segment when the program ends.
- Small. One module, six routines, no configuration, no initialisation table to fill in.
What it is not: it is not a virtual memory system. A block never straddles two segments, so the largest single block is 16372 bytes, and a block is only addressable while its segment is in the window.
| file | what it is | needed to build your program? |
|---|---|---|
alloc.as |
MapperHeap itself | yes — assemble and link it |
alloc.inc |
external declarations for the library's routines | yes — include it |
farptr.inc |
far-pointer equates and macros | yes — alloc.as includes it |
dos2func.inc |
MSX-DOS function number equates | only if you call BDOS |
dos2chec.as |
MSX-DOS2 detector | only if you want the check |
examples/ |
five short example programs | no |
tests/ |
the stress test program and its helpers | no |
doc/ |
the rest of the documentation | no |
In more detail:
alloc.as — MapperHeap itself, one file, in two layers. The lower layer
talks to the mapper: it finds MSX-DOS2's mapper support routines at start-up,
claims 16 KB segments, and switches the window. The upper layer is the heap
proper: it divides segments into blocks, keeps a list of the free ones, and
merges neighbours when a block comes back. Six routines and one variable are
public — heapinit, halloc, hfree, deref, p2restore, maptot and
hblocks. Everything else (mapinit, allocseg, newseg, flremove,
fladd, and the copy of MSX-DOS2's mapper jump table) is private to the file.
alloc.inc — one external declaration per public name, each with a
comment giving its inputs, outputs and error signal. Include it in any module
that calls the library. It declares nothing else and generates no code.
farptr.inc — the equate NULLOFF (the offset value FFFFh, meaning "no
block") and six macros that name the things every program does with far
pointers: copy one (fpcopy), copy one out of the window (fpsave), test one
for null (fpnull), map one (derefp), allocate a block (fpalloc) and free
one (fpfree). alloc.as includes this file and uses the first four
throughout; your program may use all six. They expand inline
— see doc/api.md for what each
one clobbers.
dos2func.inc — the MSX-DOS function numbers as named equates
(_STROUT, _TERM0, _DOSVER and the rest, 92 of them). Equates only: it
does not define the BDOS entry address or a system macro, so a program
that uses it defines those itself. The quick start below shows the three
lines involved.
dos2chec.as — one small relocatable module exporting one routine,
dos2check, which asks MSX-DOS for its version number and returns with carry
set if this is not MSX-DOS2. It includes nothing and depends on nothing, so it
drops into any program. The library needs MSX-DOS2; this is how a program
finds out politely instead of crashing.
- MSX-DOS2. MapperHeap is built on MSX-DOS2's mapper support routines,
which MSX-DOS1 does not have. Check with
dos2check(indos2chec.as) and exit with a message if the check fails. - A memory mapper.
heapinit(inalloc.as) returns with carry set if the machine has no mapper support. - An M80-compatible assembler and linker producing
.relmodules. Macros with parameters, andifndef/endif, are used — both are standard M80. MapperHeap was developed with SOLIDasandld.
Assembling and linking:
as myprog.as
as alloc.as
ld myprog=myprog,alloc
If you use the MSX-DOS2 check as well:
as myprog.as
as alloc.as
as dos2chec.as
ld myprog=myprog,alloc,dos2chec
Every block in the heap is named by a 4-byte far pointer:
The slot byte is the standard MSX slot address (the same byte the BIOS slot routines use) so one byte covers any slot, expanded or not:
For example 00000010b is slot 2, not expanded; 10000011b is slot 3-0. MapperHeap stores this byte exactly as MSX-DOS2 gave it, so you normally never take it apart.
Two rules follow from the layout, and they are the only two worth memorising:
- Far pointers are passed by address, never by value. Four bytes do not
fit in a register pair, so every routine takes the address of the four
bytes in
HL. - Far pointers are permanent, page-2 addresses are not. Store far
pointers anywhere in ordinary RAM and copy them freely; they stay valid
until the block is freed. The address that
derefreturns is only good until the next thing that moves the window.
The program below is a complete MSX-DOS2 .com program. It allocates one
1000-byte block in mapper RAM, writes a single byte into it, prints a message,
frees the block and returns to MSX-DOS. It is the smallest program that
exercises the whole sequence a real program follows, and every step in it is
one a real program also has to take.
What it does, in order:
- Checks that this is MSX-DOS2.
dos2check(indos2chec.as) returns with carry set under MSX-DOS1, where the mapper support routines this library needs do not exist. The program prints a message and exits. - Starts the heap.
heapinit(inalloc.as) locates MSX-DOS2's mapper support routines and records what MSX-DOS has in page 2, so thatp2restorecan put it back later. Carry set means the machine has no memory mapper. No mapper RAM is claimed yet. - Allocates a block.
halloc(inalloc.as) is given the size inBCand the address of a 4-byte buffer inHL. Since the heap is empty at this point,hallocrequests a fresh 16 KB segment from MSX-DOS2, divides it, and writes a far pointer to the new block intomyptr. Carry set means no mapper memory was available. - Writes to the block. The far pointer in
myptrnames a byte in mapper RAM, which the Z80 cannot address directly.derefp(the macro fromfarptr.inc, which callsderefinalloc.as) brings the block's segment into page 2 and returns its address there inHL, sold (hl),42writes the block's first byte. - Restores page 2, then prints. The block's segment is still in page 2 at
this point, and MSX-DOS must not be called while it is.
p2restore(inalloc.as) puts back the slot and segment MSX-DOS had, and only then is the BDOS call that prints the message safe. This is the rule described in the next section, and it is the one thing in the listing that is easy to forget. - Frees the block.
hfree(inalloc.as) takes the address of the same far pointer thathallocfilled in. After this callmyptrnames memory the heap may give to someone else. - Restores page 2 again and terminates. Ending the program is also an
MSX-DOS call, so it too has to be preceded by
p2restore. Note that the three error exits at the end of the listing callp2restoreas well — includingnodos2, which is reached beforeheapinithas run. That is safe:p2restoredoes nothing at all untilheapinithas completed.
Two details of the listing are worth pointing out before you read it.
dos2func.inc supplies the MSX-DOS function numbers only, so the program
defines the BDOS entry address and the system macro itself. And myptr is
an ordinary 4-byte variable in the program's own data segment: far pointers
live in normal RAM, never in the heap they point into.
; File: example.as
.z80
include dos2func.inc ; MSX-DOS function numbers
include alloc.inc ; the heap routines
include farptr.inc ; far-pointer macros + NULLOFF
external dos2check ; from dos2chec.as
BDOS equ 00005h ; dos2func.inc gives the function
; numbers, not this entry address
system macro func ; the usual shorthand for a BDOS call
ld c,func
call BDOS
endm
cseg
start: call dos2check ; MSX-DOS2?
jp c,nodos2 ; CY = no, this is MSX-DOS1
call heapinit ; bring up the mapper heap
jp c,nomapper ; CY = no mapper support
fpalloc myptr,1000 ; want a 1000-byte block in myptr
jp c,outofmem ; CY = no memory left
derefp myptr ; make the block addressable:
ld (hl),42 ; HL = live address in page 2
call p2restore ; hand page 2 back to DOS...
ld de,message
system _STROUT ; ...before ANY BDOS call
fpfree myptr ; done with the block
call p2restore ; and before returning to DOS
system _TERM0
nodos2: ld de,msg_dos1
jr abort
nomapper: ld de,msg_nomap
jr abort
outofmem: ld de,msg_oom
abort: push de ; p2restore modifies DE, so the message
call p2restore ; address has to be saved across it
pop de
system _STROUT
system _TERM0
dseg
message: defb "Block allocated and written.",13,10,"$"
msg_dos1: defb "This program needs MSX-DOS2.",13,10,"$"
msg_nomap: defb "No memory mapper found.",13,10,"$"
msg_oom: defb "Out of mapper memory.",13,10,"$"
myptr: defs 4 ; one far pointer
Build it with:
as example.as
as alloc.as
as dos2chec.as
ld example=example,alloc,dos2chec
Page 2 belongs to MSX-DOS whenever MSX-DOS runs.
Call
p2restore(inalloc.as) before every BDOS call and before your program terminates.
MapperHeap banks foreign memory into page 2. MSX-DOS2 keeps its own record of what page 2 holds and reasserts that record around system calls. If you call BDOS while the library's mapping is live, DOS and the library disagree about what is in page 2, and memory is quietly corrupted — in practice the machine freezes, redirected output comes out empty, or Ctrl-C prints garbage.
p2restore is cheap and calling it more often than strictly necessary is
always safe. It is also a no-op before heapinit has run, so it can go in an
error path that might be reached either way.
Two consequences follow directly:
- Addresses from
derefare temporary. They are valid only until the next call toderef,halloc,hfree,p2restore, or BDOS. When you need the block again, callderefagain — repeat calls for the same segment are nearly free, because the library remembers what the window is showing. - Never store a page-2 address in a data structure. Store the far pointer instead.
- Largest block: 16372 bytes. A block never straddles two segments, so one 16 KB segment minus its bookkeeping is the ceiling. Larger data has to be split into chunks by the program; the library will not do it.
- Smallest block: 8 bytes. A request for fewer than 8 bytes occupies 8. Real overhead is 4 bytes per block on top of that.
- Blocks are found first-fit, and a block is taken from the end of the free space it comes from, so a partly-used segment fills from the top down.
- Merging happens inside one segment only. Two free blocks in different segments are not next to each other in real memory and never merge.
- Segments are kept for the run. Once claimed, a segment is never returned to MSX-DOS2 while the program is running; MSX-DOS2 reclaims all of them when the program terminates.
- One segment per 4 MB mapper is unavailable. MSX-DOS2 holds each
mapper's segment counts in single bytes, so a 4 MB mapper — 256 segments —
is reported as 255, and MSX-DOS2 manages only segments 0 to 254.
ALL_SEGnever allocates segment number 255, so MapperHeap never receives it. This is not an addressing limit: segment numbers are bytes, and 0–255 names all 256 segments. Segment 255 is real RAM and can be selected directly; MSX-DOS2 just does not know it is there. Measured on a machine with seven 4 MB mappers: 112 KB unavailable, exactly one segment per mapper. - Not reentrant. MapperHeap keeps its working variables in fixed storage, so it must not be called from an interrupt handler, and a routine it calls must not call back into it.
tests/heaptst2.as is a stress test that allocates about 8 MB in mixed block
sizes, frees a quarter of what it allocated, and then allocates another 8 MB in
small blocks that have to fit into the gaps that freeing left. It prints one
line per operation, and those lines are checked afterwards for overlapping
blocks. Full description, including how the checking is done and what it
proved, is in doc/tests.md.
- doc/api.md — the routines one by one: inputs, outputs, registers modified, error signals, and the far-pointer macros.
- doc/internals.md — how it works inside: the memory model, the block format, the free list, and the allocate and free algorithms, with a worked example traced step by step.
- doc/tests.md — the test program and what it verifies.
- examples/README.md — five short example programs, each demonstrating a different part of the library, with a description of what each one does and why it is there.


