Core Concepts¶
Four ideas explain the shape of the API: byte buffers, builders, the split between one-shot and incremental entry points, and the optional logger.
Byte vectors are the currency¶
Every emitter takes and returns (simple-array (unsigned-byte 8) (*)). There
is no stream abstraction at the boundary and no file handle: compile-to-elf64
returns a vector, and write-elf64-file is a separate two-line function that
puts a vector on disk. The consequence worth knowing is that images are built
entirely in memory, so a large program's object file exists twice at peak — once
as *code* and once as the assembled image.
Internally two buffer types accumulate output. binary-buffer is a bare
adjustable byte vector with a fill pointer, and byte-buffer is a CLOS wrapper
around one. Neither is exported; they appear here only because the serializer
functions in the API Reference take a byte-buffer as their
second argument.
The with-output-to-vector macro is the exported way to collect bytes without
touching either type:
Builders hold the parts that are not code¶
A Mach-O or PE image is code plus a set of tables: segments, sections, symbols, relocations, imports, exports. A builder is the mutable accumulator for those tables. You create one, add to it, and then finalize:
(let ((builder (cl-cc/binary:make-mach-o-builder :x86-64)))
(cl-cc/binary:add-text-segment builder code)
(cl-cc/binary:add-symbol builder "_main")
(cl-cc/binary:add-entry-point builder 0)
(cl-cc/binary:build-mach-o builder code))
The three builders — mach-o-builder, the ELF64 builder from
make-elf64-executable, and pe-builder from make-pe32+-builder — are
independent types with independent operations. They do not share a protocol.
Note that build-mach-o takes the code bytes a second time, after
add-text-segment already received them. The builder records the segment
layout; the assembly pass re-reads the payload. Pass the same vector to both.
Two shapes of API¶
Each format offers a one-shot function, an incremental builder, or both.
| Format | One-shot | Builder |
|---|---|---|
| ELF relocatable | compile-to-elf64 |
— |
| ELF executable | compile-to-elf64-exec |
make-elf64-executable |
| Mach-O | — | make-mach-o-builder |
| Mach-O universal | build-mach-o-fat-binary |
— |
| PE32+ | compile-to-pe |
make-pe32+-builder |
Reach for the one-shot function when the whole program is one code vector and a
list of relocations, which is the common case coming out of a code generator.
Reach for the builder when you need to place things the one-shot signature does
not express: extra PT_LOAD segments, .rodata constants, PE imports and
exports, precise export RVAs.
The asymmetry is not principled. It reflects which format needed which control
first, and the one-shot functions grew keyword arguments (:arch, :bss-size,
:dll-p, :subsystem, :exports) as those needs appeared.
Diagnostics¶
write-mach-o-file shells out to codesign before replacing the target. A
timeout or non-zero exit signals macho-codesign-error, leaving an existing
target unchanged. Successful signing can still be observed through the
optional logger:
The default is nil, and with nil the library emits nothing at all. This
mirrors cl-process-kit's
*process-logger* convention — fittingly, since cl-process-kit:run is what
guards the codesign invocation itself with a timeout, and
cl-log-kit is what
*binary-logger* is bound to. Both are hard dependencies: the binding has to
exist even when nobody uses it.
Optimization passes¶
Two passes are exposed rather than applied automatically.
icf-merge-identical-functions folds byte-identical functions, returning the
kept functions, a name-to-canonical-name table, and a count. Its :enabled
argument defaults to *icf-enabled*, which is nil, so folding is off until
you ask for it. It rechecks byte equality after hashing, so a hash collision
cannot fold code that merely hashes alike.
elf64-verify-wx signals an error if any PT_LOAD segment is both writable and
executable. It is a check, not a fix; call it before writing if W^X matters to
you.
System name and package name¶
The ASDF system is cl-cc-binary; the Lisp package it defines is
cl-cc/binary. The two names differ because the package predates the split
of cl-cc into separate repositories and callers already qualify symbols as
cl-cc/binary:.