Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
352 changes: 6 additions & 346 deletions docs/docs/en/02-build-and-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,354 +51,14 @@ For fixed-address, overlay, or relocating system programs,
nonstandard origins and generates the required CP/M entry jump/bootstrap. Its
utility reference covers the CLI options, origin layouts, and limitations.

### Composite projects

A parent `dccmake.txt` can build an ordered collection of child projects:

```ini
# LIBDEMO/dccmake.txt
dcc-projects=lib,host
```

Run `dccmake` in the parent directory. Each listed directory must contain
its own `dccmake.txt`; paths resolve relative to the containing parent.
The library builds first and generates the typed host wrappers before the
host compiles. Each child retains its own inputs, output format, tools,
and build directory. Nested composite projects are supported, with cycle
detection and a maximum of 16 composite levels. A failed child stops the
remaining builds and makes the root invocation fail.

Parent configurations contain only `dcc-projects` and an optional
`dcc-library-linker`. Keep compilation settings in children; common
command-line overrides are forwarded to each child. `dcc-input` and
`dcc-output` cannot be overridden on a composite invocation.

```sh
dccmake dcc-library-linker=/path/to/dcc/l80c
```

The library-specific linker override applies only to LIB children, leaving
COM children on their ordinary linker. It can also be used for a standalone
library build. A composite builds outputs in their child build directories;
it does not install files or make disk updates transactional.

### Fixed-address bank-1 libraries

`dcc-output-format=lib` builds a fixed-address CP/M bank-1 library, not a
LINK-80 object archive. DCC's native `l80c` implements this format through
its `--lib` mode; emulated `L80.COM` does not.

Keep the host and library in separate directories, each with a
`dccmake.txt` and `build/` directory. For example, the library configuration
can contain:

```ini
dcc-input=LIB.C
dcc-output=SPREADS
dcc-output-format=lib
dcc-exports=auto
dcc-library-api=auto
dcc-library-host-dir=../host/generated
dcc-stack-bytes=512
dcc-runtime=${DCC_DIR}/DCCRTL.MAC
dcc-include-directory=${DCC_DIR}
l80c-tool=${DCC_DIR}/l80c
```

Run `dccmake` in that directory to produce `build/SPREADS.LIB` and
`build/SPREADS.SYM`. Configure compiler/assembler tools as for ordinary
application builds. The host's own configuration keeps the default `com`
format and uses the same native linker without `--lib`.

The library pipeline uses DCC's linked BSS/heap symbols, omits transient
startup, and leaves heap initialization to the matching bank-1 loader.
Library output requires `dcc-exports=auto`: the 1–8 exports are the public C
definitions discovered by the compiler, and each one is linked with its
canonical signature (see [Automatic typed libraries](#automatic-typed-libraries)).
Export names are at most eight characters. All application functions are
retained so externally called exports cannot be stripped as unreachable.
Runtime stripping and optional peephole optimization still apply.

Library stacks must be 256–2,048 bytes in 256-byte units. If DCC raises the
first translation unit's stack requirement, `dccmake` rejects the mismatch
instead of publishing a smaller stack in the header; increase the configured
reserve to an aligned value. Choose sufficient stack space for helper
modules and the complete library call chain as well.

The linked image, BSS and at least one heap byte must end below the reserved
stack, which ends at `C7FFh`. With the default 2 KiB stack an image without
BSS may therefore extend through `BFFEh`. Native `l80c` enforces the same
image, heap and stack limits as the resident bank-1 loader.

The linker records the heap base after the linked image/BSS, and the loader
bounds the heap below the reserved stack. Library `main`, process
exit/chaining, and re-entry into the bank-switch gateway are not supported.

The `.LIB` file (library format version 1) is a header, the export table,
then the image bytes, padded to a 128-byte record multiple. All multi-byte
fields are little-endian:

| Offset | Size | Field |
| --- | --- | --- |
| 0 | 4 | Magic `B1LB` |
| 4 | 1 | Library format version, `1` |
| 5 | 1 | ABI major, `1` |
| 6 | 1 | ABI minor, `0` |
| 7 | 1 | Call revision, `1` |
| 8 | 2 | Origin, `0100h` |
| 10 | 2 | Image length |
| 12 | 2 | Export count, 1–8 |
| 14 | 2 | Reserved, zero |
| 16 | 2 | Stack bytes |
| 18 | 2 | Header length, 26 + 12 × exports |
| 20 | 2 | `__brk` image offset, or `FFFFh` |
| 22 | 2 | `__hlimit` image offset, or `FFFFh` |
| 24 | 2 | Absolute heap base |

Each 12-byte export entry holds a zero-padded 8-byte name, the adapter's
image-relative offset, and the image-relative offset of its NUL-terminated
canonical signature string. Every export has a signature.

#### Automatic typed libraries

Library interfaces are generated from compiler type information; there is
no export list or separate interface manifest to maintain:

```ini
# lib/dccmake.txt (run dccmake here before building the host)
dcc-input=DATA.C
dcc-output=DATA
dcc-output-format=lib
dcc-exports=auto
dcc-library-api=auto
dcc-library-host-dir=../host/generated
dcc-stack-bytes=512
dcc-runtime=${DCC_DIR}/DCCRTL.MAC
dcc-include-directory=${DCC_DIR}
l80c-tool=${DCC_DIR}/l80c
```

With `dcc-library-api=auto`, library source is the single source of truth.
DCC compiles every configured library source with its normal include paths,
defines and compiler settings, emitting resolved type metadata with `-bank-api`.
It generates `host/generated/API.H`, retaining parameter names and resolving
typedefs to primitive C types, plus the client wrappers. Do not include this
generated header from library source: generation must work without prior
outputs. No source-text C parser or Python generator is used.
Every non-static definition must have a supported signature; public helpers
must either join the API or become `static`. Unsupported definitions fail
instead of disappearing.

Alternatively, `dcc-library-api=../API.H` selects a handwritten header: the
library and host share that header, and every public definition must match a
declaration there. The header is validated, never overwritten. Exports are
still discovered from definitions and linked with generated signatures.
Assembly definitions, unresolved prototypes, public data, static helpers,
and functions owned by the separately linked runtime are not auto exports.
There must be 1–8 discovered functions, and names are uppercased for lookup
and limited to eight characters. M80 external-name collisions and reserved
generated symbol names are rejected.

The build produces:

- `lib/build/B1ADAPT.C`: bank-1 dispatch adapters and canonical signature strings.
- `host/generated/API.H`: public prototypes and buffer annotations in auto mode.
- `host/generated/B1HOST.C`: client implementations of the public C API.
- `lib/build/B1M01.API` and later: resolved source metadata, including definition
parameter names. Handwritten-header mode also produces `B1M00.API` for the
selected header. Metadata-pass `.MAC` files are build intermediates.
- `lib/build/DATA.LIB`: the library image with its typed export table; no
hand-maintained manifest.

**Generate the contract before client integration**, without an assembler,
linker or runtime file:

```sh
# In the library directory, or the composite root containing lib and host:
dccmake dcc-contract-only=true
```

This still compiles the library sources to validate types and obtain metadata,
but does not assemble, link or install anything. Composite COM children are
skipped. A previously linked LIB is left untouched. Normal builds refresh the
contract automatically. Contract-only mode requires LIB output.
Failed generation invalidates generated interfaces instead of leaving stale
headers for clients. Existing handwritten headers in the generated location are
protected against accidental overwriting.

The host includes `API.H` and retains an ordinary COM configuration, adding
`generated/B1HOST.C` to `dcc-input`, both `generated` and the directory containing
the common `BANKLIB.H` to `dcc-include-directory`, and compiling BANKLIB/BIOSBANK runtime
sources. Auto-generated client wrappers include their adjacent API header;
bank adapters and handwritten-header wrappers use a build-time absolute path,
so regenerate them after moving the source tree. Missing directories beneath
`dcc-library-host-dir` are created automatically. Full automatic rebuilds also
invalidate the previous LIB before compiling.

**Transfer types and annotations.** The supported set is `void`
results, signed/unsigned 8-, 16-, and 32-bit integers, opaque `unsigned int`
handles, explicitly counted byte buffers, and annotated pointers to single
named flat records. Typedefs resolve through the
compiler: a handle typedef remains canonical `u16`, not a raw pointer.
Plain `char` is signed on this target. Pointer results, unannotated pointers,
pointer graphs, function pointers, varargs, old-style/unprototyped functions,
floats, and by-value structure/union arguments/results are rejected explicitly.
Records have a checked target-layout contract, not implicit pointer or
deep-object serialization.

For example:

```c
typedef unsigned int handle;
unsigned int onload(void);
unsigned int onunload(void);
unsigned int add16(unsigned int left, unsigned int right);
unsigned int upper(unsigned char *data, unsigned int count);
unsigned int filerd(unsigned char *data, unsigned int count);
struct BlockSpec { unsigned int seed; unsigned char label[8]; };
struct BlockInfo {
unsigned int handle, size;
unsigned long byte_sum;
signed char state;
unsigned char label[8];
};
handle blknew(const struct BlockSpec *spec, struct BlockInfo *info);
unsigned int blkread(handle block, unsigned int offset,
unsigned char *data, unsigned int count);
unsigned int blkfree(struct BlockInfo *info);
#pragma dcc bank_buffer upper 1 inout 2 128
#pragma dcc bank_buffer filerd 1 out 2 128
#pragma dcc bank_buffer blkread 3 out 4 128
#pragma dcc bank_record blknew 1 in
#pragma dcc bank_record blknew 2 out
#pragma dcc bank_record blkfree 1 inout
```

The maintained
[LIBDEMO example](https://github.com/gloveboxes/Z80ROMlessSBC/tree/main/examples/LIBDEMO)
builds this data service from `lib/DATA.C` and its eight-step client/test
program from `host/DATATEST.C`. Its outputs are `DATA.LIB` and `DATATEST.COM`.
The shared BANKLIB/BIOSBANK client support lives in
[`examples/BANKSDK`](https://github.com/gloveboxes/Z80ROMlessSBC/tree/main/examples/BANKSDK),
not inside another runnable example.
Its public C API uses descriptive `bank_library_*`, `bank_memory_*` and
`bank_bios_*` names, aliased to the short linker symbols, so they add no
runtime wrapper overhead.
The generated wrappers make ordinary C calls possible; only the deliberate
ABI-rejection checks use low-level call frames.

The pragma syntax is
`#pragma dcc bank_buffer FUNCTION BUFFER_ARG in|out|inout COUNT_ARG MAX_BYTES`.
Argument positions are **1-based**; the count must resolve to `unsigned int`
(`u16`); the maximum is a literal decimal integer from 1 to 256. Annotate
each byte pointer once (identical repeated annotations are harmless).
In auto-header mode place pragmas in library sources or headers they include;
annotations are collected across configured modules. In handwritten-header mode
they must occur in the selected API header.
`const` byte pointers support `in` only. Zero count permits NULL; nonzero
count with NULL is invalid. No pointers or pointer graphs cross banks;
buffers are independent flat snapshots, without pointer-alias preservation.
The sum of result, scalar arguments and maximum buffer sizes must fit 256
bytes. These are byte lengths, never implicit NUL-terminated strings.

For a single record use
`#pragma dcc bank_record FUNCTION RECORD_ARG in|out|inout`.
The 1-based argument must point to a complete **named struct** with
unqualified signed/unsigned 8-, 16- or 32-bit integer fields or fixed
one-dimensional arrays of those types. Pointer members, nested/anonymous
aggregates, unions, bitfields, const/volatile members, and flexible arrays
are not supported. Record pointers cannot be NULL; `const` record pointers
permit only IN. Records passed by value or returned by value remain
unsupported.

The compiler emits version-2 bank metadata with resolved tags, member
names/types, sizes, offsets and array lengths. This is obtained from the
type model, never by parsing source text. Automatic contracts use
`#pragma once` instead of macro include guards and contain
complete `struct TAG` definitions, annotations and compile-time target-layout
checks; original typedef aliases are resolved, not reproduced. Handwritten
contracts are checked against the same schemas. Conflicting definitions
across source modules are rejected, including same-size field changes.
Use matching updated `dcc` and `dccmake` executables.

VS Code's C/C++ IntelliSense uses native host widths and alignment, not
DCC's 16-bit packed target model. Generated layout assertions are skipped
only while `__INTELLISENSE__` is defined by the editor; struct definitions
and prototypes remain visible. Real compilation, including DCC and native
compiler validation, retains all assertions. Do not define this editor
macro in build settings to bypass incompatible-layout failures.

Record copies use the verified DCC packed little-endian representation;
these headers are not a portable native-host ABI. A compiler with different
widths/padding fails the generated static assertions. Wrappers copy complete
records with bounded `memcpy`; no heap allocation, per-field serialization
or runtime layout discovery is needed. A fixed-size call uses constant frame
offsets, an exact-size host array, and one adapter body-length check.
Only return/OUT regions are zeroed, rather than clearing inputs before
overwriting them. Record and byte-buffer arguments may be mixed.

OUT records start zero-filled. IN/OUT/INOUT direction and successful-dispatch
copy-back rules are the same as for byte buffers; a zero C result with a
successful transport still publishes outputs. Copied arguments are independent
snapshots; overlapping arguments involving a record are rejected if either
is writable. The callee must not retain its temporary record pointer. Large
record arrays should remain bank-owned, with bounded slices exposed separately.

**Canonical signatures and framing.** Type names are `void`, `i8`, `u8`,
`i16`, `u16`, `i32`, and `u32`. Functions use `RESULT(ARG,...)`, with no
whitespace: `u16()`, `void()`, `u16(u16)`, and `void(u16)`. Byte buffers
replace the pointer type with `DIRECTION:u8[COUNT_ARG<=MAX_BYTES]`, regardless
of signedness of the byte pointer. The example `objread` has exactly
`u16(u16,u16,out:u8[4<=128],u16)`. Full NUL-terminated ASCII strings
(maximum 255 characters excluding NUL) are linked at `b1s00`, `b1s01`, etc.;
adapters use `b1e00`, `b1e01`, etc. The linker receives both
`--export OBJREAD=_b1e05` and `--signature OBJREAD=_b1s05`, avoiding M80's
six-significant-character limit and signature-hash collisions. Export
entries are 12 bytes: name[8], image-relative adapter offset, image-relative
signature-string offset (both offsets little-endian uint16).

The packed **body** is result bytes first, then scalar arguments in
declaration order (pointer arguments consume no pointer slot), then copied
buffers and records in declaration order. Integers use the target's little-endian representation.
An OUT buffer begins zero-filled; IN copies only into bank 1; OUT/INOUT copy
back only after successful dispatch. Results and count scalars occupy their
normal 1/2/4-byte widths, without alignment padding. For `objread`, the body
is result2, handle2, offset2, count2, then `count` bytes: 8–136 bytes.
Records occupy one fixed-size region each among the copied arguments, in
declaration order, and do not consume pointer slots. `blknew` uses 29 body
bytes and `blkfree` 19. Record signatures use
`DIRECTION:rec1le:TAG[SIZE]{FIELD:TYPE@OFFSET;ARRAY:TYPE[COUNT]@OFFSET}`.
The grammar version and `le` specify packed little-endian layout. Full
field names/order, widths/signedness, array bounds, offsets and size are
included, so equal names or sizes alone never establish compatibility.
The 255-character signature and 256-byte body limits apply;
overflow is a generation error, not a truncated signature or short hash.

Host wrappers keep one four-byte static `bank_library_binding_t` and call
`bank_library_call_bound(&binding, name, signature, body, body_size)` from
`BANKLIB.H`. BANKSDK compares the full signature once per module generation,
then reuses the bound export index. Open reserves a 512-byte bank-1 frame
extent; steady-state calls do not allocate/free. Module generation and pool
epoch checks reject stale bindings/ownership before frame writes and again at
dispatch. Raw `blcall()` is also available, with full signature validation on
every call. Clients and libraries use BIOS ABI 1.0, call revision 1.
On the bank wire,
the runtime prepends a little-endian uint16 body-size prefix and passes a
pointer to that prefix to `unsigned int b1e00(unsigned char *wire)`.
The adapter checks the prefix against the minimum fixed size **before**
reading scalars, checks each count/bound and the 256-byte cap, then requires
an exact body-size match before rebasing pointers or invoking C. It returns
1 for dispatch or 0 for invalid framing. The common runtime must ensure the
allocation really contains the claimed body bytes; the prefix itself is
outside the 256-byte body limit.

On local NULL/count/bounds validation failures wrappers call
`blfail(BL_ARGUMENT)` or `blfail(BL_BOUNDS)` before any copying or transport.
On transport failure they preserve the runtime's `blerr()`, do not copy out,
and return deterministic zero (or return from a void function). Callers
**must check `blerr()`**, since a legitimate result can also be zero.
Optional `onload(void)`/`onunload(void)` lifecycle functions are ordinary
`u16()` exports; the common runtime owns their automatic invocation.
Banked-memory library support is intended for the
[Z80 ROM-less SBC](https://gloveboxes.github.io/Z80ROMlessSBC/), with its
[banked CP/M memory and runtime](https://gloveboxes.github.io/Z80ROMlessSBC/system/cpm-execution/).
It is not required for ordinary CP/M `.COM` programs. See the
[Banked memory support appendix](appendix/04-banked-memory.md) for memory-layout
diagrams, fixed-address libraries, generated typed APIs and composite builds.

<!-- markdownlint-disable MD046 -->
??? note "The manual pipeline (click to expand)"
Expand Down
Loading
Loading