Brink
Brink is a domain specific language for linking and composing binary files.
Install / Use
/learn @steveking-gh/BrinkREADME
Brink
Brink is a domain specific language for linking and composing of an output file. Brink simplifies construction of complex files by managing sizes, offsets and ordering in a readable declarative style. Brink tries to be especially useful when creating FLASH, ROM or other non-volatile memory images.
Quick Start
Build From Source
Step 1: Install Rust
Brink is written in rust, which works on all major operating systems. Installing rust is simple and documented in the Rust Getting Started guide.
Step 2: Clone Brink
From a command prompt, clone Brink and change directory to your clone. For example:
$ git clone https://github.com/steveking-gh/brink.git
$ cd brink
Step 3: Build and Run Self-Tests
$ cargo test --release --all
All tests should pass, 0 tests should fail.
Step 4: Install Brink
The previous build step created the Brink binary as ./target/release/brink. You can install the Brink binary anywhere on your system. As a convenience, cargo provides a per-user installation as $HOME/.cargo/bin/brink.
$ cargo install --path ./
Command Line Options Reference
brink [OPTIONS] <input>
The required input file contains the brink source code to compile and build the output file. Brink source files typically have a .brink file extension.
| Option | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| -D<name>[=value] | Defines a const value from the command line.<br>See Command-Line Const Defines below. |
| --list-extensions | List all available extensions compiled into brink as controlled by Cargo feature flags. |
| --map-csv | Writes a CSV format map file <stem>.map.csv to the current directory.<br>For example: firmware.brink → firmware.map.csv. |
| --map-csv=<file> | Writes a CSV map file to the specified file. |
| --map-csv=- | Writes a CSV map file to stdout. |
| --map-c99 | Writes a C99 header file <stem>.map.h to the current directory.<br>For example: firmware.brink → firmware.map.h. |
| --map-c99=<file> | Writes a C99 header to the specified file. |
| --map-c99=- | Writes a C99 header to stdout. |
| --map-json | Writes a JSON format map file <stem>.map.json to the current directory.<br>For example: firmware.brink → firmware.map.json. |
| --map-json=<file> | Writes a JSON map to the specified file. |
| --map-json=- | Writes a JSON map to stdout. |
| --noprint | Suppress print statement output from the source program. |
| -o <file> | Output file name. Defaults to output.bin. |
| -q, --quiet | Suppress all console output, including errors. Overrides -v. Useful for fuzz testing. |
| -v | Increase verbosity. Repeat up to four times (-v -v -v -v). |
When the user does not specify a path, Brink writes map file(s) and the output to the current working directory.
Command-Line Const Defines
The -D option injects a const definition into the program from the command line.
This option is modelled after the GCC -D preprocessor syntax. You can specify -D multiple times, once per each definition. For example:
brink -DBASE=0x8000 -DCOUNT=16 firmware.brink
The name must be a valid Brink identifier. The value is optional; without a value, Brink sets the const to 1, with type Integer, following the GCC boolean-flag convention.
-D overrides any same-named const definition in the source.
Map output lists all const definitions including -D consts.
Value Type Inference
Brink knows or infers the type from the value string using the same rules as source code for type inference.
| Example | Value | Type | Description |
| ---------------- | ------ | --------- | ------------------------------------------ |
| -DFLAG | 1 | Integer | Defaults to true (1). |
| -DCOUNT=16 | 16 | Integer | Plain decimal → Integer |
| -DBASE=0x1000 | 0x1000 | U64 | Hex/binary without suffix → implicit U64 |
| -DBASE=0x1000u | 0x1000 | U64 | u suffix → explicit U64 |
| -DOFFSET=0x40i | 0x40 | I64 | i suffix → explicit I64 |
| -DDELTA=-4 | -4 | I64 | Negative decimal → implicit I64 |
Example
Define a base address and section count at the command line:
brink -DBASE=0x0800_0000 firmware.brink -o firmware.bin
The source can reference BASE as an ordinary const:
section entry { wr8 0x01; }
section top { wr entry; }
output top BASE;
What Can Brink Do?
Brink can assemble any number of input files into a unified output.
<img src="./images/unified_binary.svg" width="400">Brink can calculate relative or absolute offsets, allowing the output to contain pointer tables, cross-references and so on.
<img src="./images/offsets.svg" width="400">Brink can add pad bytes to force parts of the file to be a certain size.
<img src="./images/pad.svg" width="400">Brink can add pad bytes to force parts of the file to start at an aligned boundary or at an absolute location.
<img src="./images/align.svg" width="400">Brink can write your own strings and data defined within your Brink source file.
<img src="./images/adhoc.svg" width="400">Brink provides full featured assert and print statement support to help with debugging complex output files.
<img src="./images/debug.svg" width="400">Hello World
For a source file called hello.brink:
/*
* A section defines part of an output.
*/
section foo {
// Print a quoted string to the console
print "Hello World!\n";
}
// An output statement outputs the section to a file
output foo;
Running Brink on the file produces the expected message:
$ brink hello.brink
Hello World!
$
Brink also produced an empty file called output.bin. This file is the default output when you don't specify some other name on the command line with the -o option. Why is the file empty? Because nothing in our program produced output file content -- we just printed the console message.
Let's fix that. We can replace the print command with the wrs command, which is shorthand for 'write string':
/*
* A section defines part of an output.
*/
section foo {
// Write a quoted string to the output
wrs "Hello World!\n";
}
// An output statement outputs the section to a file
output foo;
Now, running the command again:
$ brink hello.brink
$
Produces output.bin containing the string Hello World!\n.
Basic Structure of a Brink Program
A Brink source file consists of one or more section definitions and exactly one output statement. Each section has a unique name. The output statement specifies the name of the top level section. Starting from the top section, Brink recursively evaluates each section and produces the output file. For example, we can define a section with a write-string (wrs) expression:
section foo { // Start a new section named 'foo'
wrs "I'm foo"; // wrs writes a string into the section.
}
output foo; // Final output
Produces a default output named output.bin.
$ cat output.bin
I'm foo
Using a write (wr) statement, sections can write other sections:
section foo {
wrs "I'm foo\n";
}
section bar {
wrs "I'm bar\n";
wr foo; // nested section
}
output bar;
Produces output.bin:
$ cat output.bin
I'm bar
I'm foo
Users can extend Brink with custom data processing using Brink Extension. Users write the output of their extension call into a section with a wr.
section foo {
wrs "I'm foo\n";
}
section bar {
wrs "I'm bar\n";
wr foo; // nested section
}
section final {
wr bar;
wr my_stuff::crc(bar); // Write a 4 byte CRC hash for section 'bar'.
}
assert(sizeof(final) == 20);
output final;
Assert and Print
To aid in debug, Brink supports assert and print statements in your programs.
Assert expressions automate error checking. This example verifies our expectation that section 'bar' is 13 bytes long.
section bar {
wrs "Hello World!\n";
assert sizeof(bar) == 13;
}
output bar;
You can print this length information to the console during gene
