- Introduction
- Building the Library
- Compiling your CP/M program
- Running the Tests
- Platform-dependent Functions
- Source Tree Layout
- What is implemented?
- Standard C headers with working implementations
- Standard C interfaces still missing from the provided partial headers
- Standard C headers not currently provided
- POSIX / Unix-style extensions
- POSIX pieces still missing
- Non-standard libcpm3-z80 extensions
- To Do
libcpm3-z80 is a portable, readable, and minimal ISO/IEC 9899:TC2 Standard C Library for Digital Research's CP/M 3.
Implementing a standard library shouldn't be a daunting task. Unless the library immoderately aspires to target all architectures and all compilers by using sophisticated preprocessor directives and tricks, only known to god and a few earthlings.
libcpm3-z80 is an attempt to provide a library, written in purest C, with a clear separation of platform independent and platform dependent code. To port it to your architecture you need to provide a handful of well documented platform specific functions that the standard requires and are not available in CP/M's BDOS.
The library is now organized directly by public subsystem under src/:
stdlib/, stdio/, string/, sys/, platform/, setjmp/, and so on.
There is no longer a separate src/_impl/ tree.
Clone the repository first:
git clone https://github.com/tstih/libcpm3-z80.git
cd libcpm3-z80| Command | Description |
|---|---|
make |
Build the library |
make test |
Build library + tests and run them in the CP/M emulator |
make clean |
Remove build/ and bin/ |
| Parameter | Values | Default | Description |
|---|---|---|---|
DOCKER |
on, off |
on |
on builds inside the wischner/sdcc-z80 Docker image, no local SDCC needed. off builds natively and requires SDCC on PATH. |
PLATFORM |
name | none |
Defines PLATFORM_<NAME> for conditional platform code. The default platform provides implementations of msleep() and _libinit(). Any other value omits them — link your own. |
BUILD_DIR |
path | build/ |
Intermediate build products (.rel, archive). |
BIN_DIR |
path | bin/ |
Final outputs: library, CRT0, headers. |
Examples:
make # build inside Docker (default)
make DOCKER=off # build natively with local SDCC
make PLATFORM=myboard # custom platform, no no-op stubs
make DOCKER=off BUILD_DIR=out/build BIN_DIR=out/binWarning:
make testrequiresDOCKER=on. WithDOCKER=offthe test binaries will be built but cannot be run —make testwill error out.
All outputs are placed in bin/ (or BIN_DIR if overridden):
| File | Description |
|---|---|
crt0cpm3-z80.rel |
C runtime start-up object (must be linked first) |
libcpm3-z80.lib |
CP/M 3 standard C library archive |
include/ |
Public header files |
libsdcc-z80.lib (SDCC integer/float stubs) is downloaded automatically to
lib/ on the first make test and must also be linked with your program.
Urgent:
libcpm3-z80.libalone is not enough. Any program using this library must also linklib/libsdcc-z80.lib, or the final link will be incomplete. Repository: https://github.com/retro-vault/libsdcc-z80
The library targets SDCC Z80 __sdcccall(1). You need SDCC version >= 4.2.0.
You need SDCC installed locally, or you can adapt the Docker invocation from
the test/Makefile as a template. The link order is fixed: CRT0 first,
your object files, then the library, then the SDCC stubs last.
Urgent: Always link
lib/libsdcc-z80.libtogether withbin/libcpm3-z80.lib. The SDCC support library is required and lives here: https://github.com/retro-vault/libsdcc-z80
If you use upstream sdcc directly, add -mz80 on both the compile and
link commands. Also set the code and data locations explicitly when linking:
CP/M .COM programs must start at 0x100, and plain upstream SDCC otherwise
tries to place the data segment at 0x8000. The custom Docker toolchain used
in this project already defaults to Z80, so the examples there do not need
-mz80 explicitly.
# Compile
sdcc -mz80 --std-c11 --no-std-crt0 --nostdinc --nostdlib \
-I bin/include -c -o myprog.rel myprog.c
# Link
sdcc -mz80 --std-c11 --no-std-crt0 --nostdinc --nostdlib \
--code-loc 0x100 --data-loc 0 \
-o myprog.ihx \
bin/crt0cpm3-z80.rel myprog.rel \
bin/libcpm3-z80.lib lib/libsdcc-z80.lib
# Convert IHX to CP/M .COM (binary starts at 0x0100)
sdobjcopy -I ihex -O binary myprog.ihx myprog.comWith the project's Docker SDCC wrapper, the target selection is already baked
in, but --code-loc 0x100 --data-loc 0 still applies if you invoke upstream
sdcc yourself.
See test/Makefile for a complete working example.
make testThis builds the library and all test .com binaries inside Docker, then runs
them automatically inside a RunCPM CP/M 3 emulator container. Results are
written to bin/<name>.txt.
Note:
make testrequires Docker (DOCKER=on, the default). WithDOCKER=offonly the test binaries are compiled — they cannot be run without the CP/M emulator.
The test suite comprises:
| Binary | Tests |
|---|---|
tctype.com |
ctype.h character classification |
tstring.com |
string.h string functions |
tstdlib.com |
stdlib.h general utilities |
tstdio.com |
stdio.h file I/O |
ttime.com |
time.h time functions |
tmath.com |
math.h floating-point math |
tmem.com |
allocator internals and utility lists |
tfile.com |
low-level file API (open/read/write/lseek/stat) |
tsdcc.com |
SDCC runtime integration |
tsetjmp.com |
setjmp/longjmp |
Each program prints PASS or FAIL per test case and a summary line at the
end. You can also copy any .com file to a real CP/M disk and run it on
hardware or an emulator such as z80pack.
Implementation files are grouped by the public library area they support:
| Folder | Contents |
|---|---|
src/sys/ |
CP/M system bindings such as bdos.s and crt0cpm3-z80.s |
src/stdlib/ |
stdlib implementation plus allocator, list, and startup internals |
src/stdio/ |
stdio implementation plus internal formatting helpers |
src/platform/ |
platform-specific hooks and timing helpers |
src/setjmp/ |
setjmp / longjmp |
src/string/, src/time/, src/math/, src/file/, ... |
subsystem implementations |
The old src/_impl/ layout has been retired.
The library is designed to be portable. Everything that depends on specific
hardware is isolated behind a single PLATFORM make parameter. The value is
just a short build-time name for your target-specific support code:
| Example | Meaning |
|---|---|
none |
Built-in default platform (the default) |
partner |
Iskra Delta Partner with your own platform.c |
myboard |
Any custom target with your own platform support |
The name is not parsed or validated by the build system. Choose something
short and descriptive so that libplatform is meaningful at runtime.
make PLATFORM=none # built-in default
make PLATFORM=partner # your own Partner platform.c
make PLATFORM=myboard # any custom platformAt build time this defines two preprocessor macros:
| Macro | Example value | Purpose |
|---|---|---|
PLATFORM_NONE |
(defined/not defined) | Guards the built-in default platform code |
PLATFORM_NAME |
none |
Bare token of the platform name |
PLATFORM_NAME_STR |
"none" |
String literal, derived via # stringification in platform.h |
All platform-specific symbols are declared in platform.h. Include it in any
file that uses nltype, libplatform, progname, or msleep():
#include <platform.h>| Variable | Type | Default | Description |
|---|---|---|---|
libplatform |
const char * |
NULL |
Platform name string; set to PLATFORM_NAME_STR by _libinit() |
nltype |
char |
NL_LF |
Newline translation mode for all console output |
progname |
const char * |
NULL |
Program name; CP/M does not pass argv[0], so set this explicitly in your own startup code if you need it |
nltype controls how \n is expanded by all output functions (putchar,
printf, fwrite, etc.):
| Value | Constant | Console output |
|---|---|---|
0 |
NL_LF |
\n only (Unix style) |
1 |
NL_CRLF |
\r\n (CP/M / DOS style, default for the built-in platform) |
2 |
NL_LFCR |
\n\r |
Two functions have no CP/M 3 BDOS equivalent and must be provided per platform:
| Function | Signature | Description |
|---|---|---|
msleep() |
void msleep(int millisec) |
Busy-wait delay in milliseconds |
_libinit() |
void _libinit(void) |
Platform initializer, called once at startup |
Both are declared in platform.h.
_libinit() is called at the very end of C runtime initialization — after the
heap, file descriptors, and command-line arguments are ready. Use it to set
platform variables and do any hardware-specific startup.
The default none platform implements both:
void _libinit(void) {
libplatform = PLATFORM_NAME_STR; /* "none" */
nltype = NL_CRLF; /* CP/M needs CR+LF */
}void msleep(int millisec) {
/* Calls _delay_1ms() once per millisecond.
_delay_1ms() is a Z80 assembly routine that burns
exactly 4000 T-states (= 1 ms) on a 4 MHz CPU.
T-state budget:
Static: CALL=17 RET=10 LD B,n=7 final DJNZ=8 => 42
Dynamic: 209 × DEC HL(6) + 208 × DJNZ(13) => 3958
Total: 4000 T-states = 1 ms at 4 MHz
Note: the C loop itself adds ~30-50 uncounted T-states per
iteration. Long delays accumulate a small positive error
(~0.1-0.2 ms per 100 ms). For tight timing call _delay_1ms()
directly from assembly. */
while (millisec-- > 0)
_delay_1ms();
}If you pass a PLATFORM other than none the library does not
include msleep() or _libinit(). You get unresolved externals at link time
until you supply your own implementations. Link your object file before the
library:
sdcc ... myprog.rel myplatform.rel bin/libcpm3-z80.lib ...Inside your _libinit() set libplatform and nltype as appropriate for
your hardware. Set progname only if your startup environment provides a
program name:
#include <platform.h>
void _libinit(void) {
libplatform = PLATFORM_NAME_STR; /* set by the build system */
nltype = NL_CRLF; /* or NL_LF, NL_LFCR */
}This library currently provides a small, test-backed subset of the C library, plus a CP/M-oriented POSIX-like layer and a few CP/M-specific extensions.
These headers are present and backed by code in src/:
| Header | Status | Implemented surface |
|---|---|---|
ctype.h |
partial | isalnum, isalpha, iscntrl, isdigit, isgraph, islower, isprint, ispunct, isspace, isupper, isxdigit, tolower, toupper |
assert.h |
implemented | assert() macro with NDEBUG support |
errno.h |
partial | errno plus a small CP/M-oriented error set |
float.h |
partial | single-precision constants only |
inttypes.h |
partial | intmax_t, uintmax_t, PRI* macros, strtoimax, strtoumax |
iso646.h |
implemented | alternative operator spellings |
limits.h |
partial | integer and size limits for this target |
math.h |
partial | ceil, cos, cot, exp, fabs, floor, frexp, ldexp, log, log10, modf, pow, sin, sqrt, tan |
setjmp.h |
implemented | setjmp, longjmp |
stdarg.h |
implemented | va_list, va_start, va_arg, va_end |
stdbool.h |
implemented | bool, true, false macros |
stddef.h |
partial | size_t, ptrdiff_t |
stdint.h |
partial | fixed-width 8/16/32-bit integer types |
stdio.h |
partial | FILE, stdin, stdout, stderr, fopen, fclose, fflush, fgetc, fgets, fputc, fputs, fread, fseek, ftell, fwrite, feof, ferror, clearerr, getc, getc_unlocked, getchar, gets, getw, fprintf, perror, printf, putc, putchar, puts, remove, rewind, setbuf, setvbuf, sprintf, tmpfile, tmpnam, ungetc, vfprintf, vprintf, vsprintf |
stdnoreturn.h |
implemented | noreturn macro |
stdlib.h |
partial | abort, atexit, exit, abs, atof, atoi, atol, bsearch, div, itoa, labs, ldiv, rand, srand, strtol, strtoul, malloc, calloc, free, qsort |
string.h |
partial | memchr, memcmp, memcpy, memset, strchr, strcmp, strcpy, strcspn, strlen, strncmp, strncpy, strrchr, strsep, strerror, strrev, strtok, stolower, stoupper |
time.h |
partial | asctime, clock, ctime, difftime, gmtime, mktime, time |
All currently declared public interfaces are backed by code, but many headers are intentionally only a subset of the full ISO C surface. The main gaps are:
| Header | Not yet provided |
|---|---|
math.h |
most of the wider C math surface beyond the current single-precision core |
stdio.h |
fscanf, rename, scanf, sscanf |
stdlib.h |
getenv, system |
time.h |
strftime and timezone/DST extensions beyond UTC-style operation |
This is not yet a full hosted C library. In particular, these standard headers are not shipped here:
complex.hfenv.hlocale.hsignal.hstdalign.hstdatomic.htgmath.hthreads.huchar.hwchar.hwctype.h
These are not part of ISO C. They are grouped separately because they form the library's Unix-like I/O layer on top of CP/M 3:
| Header | Status | Implemented surface |
|---|---|---|
fcntl.h |
partial | open, creat, O_*, SEEK_* |
sys/types.h |
partial | ssize_t, off_t |
sys/stat.h |
partial | struct stat, stat |
unistd.h |
partial | read, write, close, fsync, lseek, unlink |
dirent.h |
partial | DIR, struct dirent, opendir, readdir, closedir using CP/M wildcard search (A:*.COM, *.TXT, etc.) |
The POSIX layer is intentionally small. Common Unix interfaces not currently provided include:
accessdup,dup2isattymkdir,rmdirpiperenameas a POSIX filesystem callstatvariants such asfstat- process APIs such as
exec*,fork,wait
These are project-specific or CP/M-specific APIs rather than ISO C or POSIX:
| Header | Extension |
|---|---|
platform.h |
nltype, NL_LF/NL_CRLF/NL_LFCR, libplatform, progname, msleep(), _libinit() |
sys/bdos.h |
bdos(), bdosret(), CP/M 3 BDOS constants, bdos_ret_t |
time.h |
gettimeofday(), settimeofday(), struct timeval |
stdlib.h |
_splitpath() |
string.h |
stoupper(), stolower(), strrev() |
- Expand standard-header coverage if the project goal moves beyond the current minimal subset.