Cross-cutting conventions for emp-tool public APIs. Read this before adding or changing an exported function that takes a byte/block/bool count, before choosing a PRG buffer-fill API, or before writing a failure path.
Every failure in emp-tool — hostile/corrupt input (.empbc loaders,
validate_program), API misuse (shape/width mismatches), I/O setup and
mid-protocol faults, malicious-abort checks — reports through error()
(runtime/core/error.h): print the message with the call site, then
std::_Exit(1). emp-tool does not throw, anywhere, and is
-fno-exceptions-compatible: test_no_exceptions compiles the public
umbrella with -fno-exceptions, so a reachable throw fails the build.
Standard-library allocation failure follows the same fail-stop model
(under -fno-exceptions the runtime aborts instead of raising
std::bad_alloc).
Why one fatal path:
- Unwinding past half-settled protocol state (a partly garbled circuit,
a half-consumed OT batch) is never safe to resume from, and
exit()'s destructor/atexit cleanup races still-live ThreadPool workers (seeutils.hppfor the_Exitrationale). - Exceptions buy recoverability only if some caller can meaningfully recover; in a two-party protocol binary there is no such caller — the process is the session.
- One discipline keeps every failure observable the same way: nonzero
exit + one stderr line with
file:line.
Consequences for code and tests:
- Don't write cleanup that only runs on a failure path (close-on-error,
free-on-error):
error()ends the process, so the path is dead. Plain straight-line code witherror()calls is the idiom. - A "rejects bad input" test asserts child-process death, not a caught
exception — fork, silence stderr, expect nonzero exit (see
test/ir/test_boolean_program.cpp'sdies()helper).
Use expecting(condition, message) for a cheap invariant or API precondition
that is overwhelmingly true during correct execution:
expecting(len >= 0, "negative length");expecting evaluates its condition exactly once and calls error(message) with
the caller's file and line if it is false. On GCC/Clang it is force-inlined and
tells the optimizer that true is the expected result, which can improve code
layout and initial branch prediction without adding a helper call. The CPU's
dynamic branch predictor still controls steady-state prediction. Compile-time
conditions can still be folded away.
Do not use expecting for ordinary data-dependent control flow where both
outcomes are normal. It is the shared emp-tool/downstream spelling for contract
checks that lead to error().
Operational results use the same form: store the result when needed, then state
its successful condition with expecting. For a message that requires dynamic
formatting, pass a callable; it is invoked only on failure:
FILE *f = std::fopen(path, "rb");
expecting(f != nullptr,
[&] { return std::string("cannot open ") + path; });Use expecting(false, message) for an unconditional fatal/default case. Keep
direct error() calls inside the failure primitive itself only; it remains a
public compatibility sink for downstream code while new call sites consistently
express the expected condition.
Replacing assert(condition) with expecting(condition, message) makes the
check survive NDEBUG. This is appropriate when violating the condition would
permit undefined behavior or silent state corruption and the check is cheap. It
is not a zero-cost substitution for a Release-build assertion, because a
Release assertion is removed entirely. Compile-time contracts remain
static_assert.
EMP uses two bit-buffer shapes.
For typed values with compile-time width, codecs return:
std::array<bool, V::width()>This is the session-I/O contract for WireValue: width is part of the
type, storage is real bool, and .data() may be passed to APIs that
take const bool*.
For runtime-sized bit sequences, use byte-bools:
std::vector<uint8_t> // owning; the byte-bool codec returns this
const uint8_t* // + length; the byte-bool codec reads thisEach byte represents one bit and must be normalized to 0 or 1.
Do not use std::vector<bool> in emp-tool library/protocol code. It is
bit-packed, has proxy references, has no real bool*, and forces
hidden copies. Do not reinterpret byte-bool storage as bool*; convert
explicitly if an API requires real bool storage.
Buffer-length and count parameters on emp-tool's public API use
int64_t, not int or size_t.
Why:
intoverflows at 2^31 elements.- unsigned
size_tunderflows silently inlen -= batchstyle loops that decrement to zero. int64_tavoids both and matches the existing IO / crypto APIs.
New emp-tool APIs that take a "number of bytes / blocks / bools / points" parameter follow this convention. Internal counters and indices inside those bodies should match:
for (int64_t i = 0; i < len; ++i) {
// ...
}A negative length or count is always an API error, never an alternate spelling
of zero. Reject it with error() at the public boundary, before updating an
unsigned counter, multiplying by an element width, doing pointer arithmetic, or
passing the value to an API whose size parameter is unsigned. Only after that
check may a signed length be converted to size_t. Zero remains a valid no-op
for buffer-fill, hash-absorb, and transport operations.
When converting an element count to bytes, check multiplication against
INT64_MAX before multiplying. This preserves the signed-length invariant all
the way down to the byte-oriented API instead of overflowing first and trying
to validate the result afterward.
Template non-type parameters stay as int / size_t when they are
compile-time bounded sizes (int N, int K, size_t Width, etc.).
PRG::random_data requires a 16-byte-aligned destination and asserts
in debug builds. Use PRG::random_data_unaligned for stack integers,
small structs, byte buffers with arbitrary offset, and any other
destination that is not naturally block-aligned.
Use the aligned fast path only when the destination is known to be
16-byte aligned, such as a block* or a suitably aligned block buffer.