|
ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
|
This is the naming law for ProtoCore. It has two jobs: it states the rules precisely enough that a tool can check them, and it explains why each rule exists so you can apply it to a case the table does not cover.
If you only read one section, read The contract.
Audience. Anyone reading or extending the library, including people who have never worked on embedded code. Where a rule depends on a C or C++ detail, that detail is explained rather than assumed. If a term is new, docs/learn has the primers.
ProtoCore presents a C API. The implementation is C++.
That sentence decides almost every rule below, so it is worth being precise about what each half means and why they differ.
The API is C-shaped: flat names, one global namespace, no overloading, no templates in the signatures a caller depends on. This is not nostalgia. The target list is xtensa, riscv, arm, and c2000, and control-law code on those parts is written and reviewed as C. An API that requires a C++ compiler to call is an API that cannot be used where the library most needs to be usable.
The implementation is C++, and that is deliberately not a contradiction. What a control engineer needs to guarantee is the behavior of the emitted binary at the points where timing and memory matter, not the source language that produced it. ProtoCore guarantees those points directly, by verifying the generated instructions (see section 6). Once behavior is established at the binary, the source language above it is an implementation detail.
So: one flat naming law, applied everywhere. There is no second, namespaced convention for internals, because a reader moving between a header and its implementation should not have to change mental models halfway.
| Thing | Form | Example |
|---|---|---|
| Function | pc_snake_case, flat | pc_sha256_init |
| Type | pc_snake_case, flat | pc_sha256_ctx |
| Macro / compile-time flag | PC_UPPER_SNAKE, flat | PC_ENABLE_SSH, PC_SHA256_DIGEST_LEN |
| Enum type | enum class, pc_snake_case | enum class pc_ip_family : uint8_t |
| Enum member | keeps its descriptive prefix | pc_ip_family::PC_IP_V4 |
| Include guard | PROTOCORE_<FILE>_H, max 31 | PROTOCORE_SHA256_H |
| File and directory | snake_case | src/crypto/mac/hmac_sha256.cpp |
| Test env / suite | native_<topic> / test_<topic> | native_ip, test_ip |
No namespace. No using namespace. Everything below is the reasoning; the table is the rule.
The rule: every symbol the library exports is prefixed pc_ or PC_, at global scope.
C has no namespaces. A C caller disambiguates by name alone, so every exported name must be globally unique on its own. The prefix is what buys that uniqueness, and it has to be on every symbol, because a prefix with holes in it is not a guarantee.
This is why ProtoCore does not wrap its C++ internals in namespace protocore even though it could. Two conventions in one library means every reader has to know which half of the codebase they are in before they can predict a name, and every symbol that crosses the boundary needs a translation. One flat law is duller and better.
Pick names that stay under the linker's and the preprocessor's limits. See section 2 for the 31-character macro rule, which is the tightest of them.
The rule: macros are PC_UPPER_SNAKE, at global scope, always.
The preprocessor runs before the compiler and knows nothing about scope. A macro is textual replacement across the whole translation unit. There is no construct that can contain one, so "flat" is not a choice here, it is the only option.
That has a consequence worth internalizing, because it produces genuinely confusing errors: a macro will rewrite a token that is already scoped.
The member is scoped and it does not help, because substitution happens before the compiler sees the scope. This is exactly why enum members keep descriptive prefixes (section 3), and why PC_ on every macro matters: it keeps our macros out of everyone else's token space and theirs out of ours.
Keep macro names under 31 characters. C89 guarantees only the first 31 characters of a macro name are significant, and ProtoCore targets toolchains where that limit is real, c2000 included. Two macros agreeing in their first 31 characters are the same macro there, silently.
When a name does not fit, abbreviate whole words. Never cut a word short. The two produce names that look similar and read completely differently:
| Chopped | last word cut off partway - reads as a typo; the reader stops to work out the real name |
| Abbreviated | PC_SSH_MSG_CH_WIN_ADJ - reads as deliberate; every token maps back one-to-one |
Abbreviation keeps the word boundaries, so the correspondence to the spec survives: CH=CHANNEL, WIN=WINDOW, ADJ=ADJUST still recovers SSH_MSG_CHANNEL_WINDOW_ADJUST from RFC 4254, which is what a reviewer checks the constant against. That matters most for the names that are least arbitrary: protocol message names, IANA cipher suites, and datasheet register names are quoted from a document, and mangling one breaks the only link back to it.
Prefer abbreviations the library already uses, so a reader meets a small vocabulary rather than a new guess each time: MAX (369 uses), LEN (136), BUF (85), REG (72), CMD (43), MSG (42), HDR (29), AUTH (25), REQ (18), ERR.
How to shorten a word: keep its consonant skeleton. Dropping vowels leaves a frame the eye still resolves to one word, which is why the abbreviations already in the tree read as words rather than noise - HDR, MSG, CFG, PKT are all built this way. It extends to words with no conventional short form: ENOUGH to ENGH, INTERACTIVE to IACTV, KEYBOARD to KBD.
An abbreviation that reads two ways is no better than a chop. INVALID has an obvious five-letter short form that fits the limit, and it reads just as easily as "in-value" as it does "invalid". The test is not "can this be decoded", it is "does every reader decode it the same way, without stopping". When a word has no unambiguous short form, use a synonym that does: ..._MSG_TYPE_ERR says what BadTcpMessageTypeInvalid means and cannot be read any other way.
Nine macros currently exceed the limit, longest 37 (PC_OPCUA_BAD_TCP_NOT_ENOUGH_RESOURCES), tracked in ROADMAP.md. One of them, PC_ENABLE_SSH_KEYBOARD_INTERACTIVE, is a user-facing feature flag that also appears in every example's build_opt.h and in the configurator, so renaming it means regenerating those too. It is renamed anyway, before 1.0.0: this is the last moment a flag can change without it costing anyone anything, and a name that violates the law on the day the law ships is a name nobody will ever fix.
The rule: every enum is an enum class. Members keep a descriptive prefix. Dropping a prefix is a per-enum decision, never a blanket rewrite.
enum class is required because a plain enum leaks its members into the surrounding scope and converts to int implicitly, silencing exactly the type check that makes the enum worth having.
Keeping member prefixes is the part people argue with, since pc_ip_family::PC_IP_V4 looks redundant beside pc_ip_family::V4. Three reasons it stays:
PC_IP_V4 is self-describing when it appears alone. V4 is not.OUTPUT, ERROR, DELETE, NONE, READ, TIMEOUT) and be rewritten out from under us on some target.| Member | Bare form | Legal? |
|---|---|---|
HTTP_11 | 11 | no |
SMB2_DIALECT_0311 | 0311 | no |
DEVICENET_GROUP_1 | 1 | no |
A rule that cannot be applied uniformly is not a rule. The prefix stays by default; any individual enum that genuinely reads better without one is changed deliberately, after checking that its bare members are neither macro-prone nor digit-leading.
Include guards are PROTOCORE_<FILE>_H, built from the file's own name: src/crypto/hash/sha256.h guards with PROTOCORE_SHA256_H.
Guards take the full library name rather than the PC_ prefix, because a guard is the one macro that has to be unique across someone else's build. Every header a user compiles shares a single macro namespace, so PC_HTTP_PARSER_H is a plausible name for another library's guard while PROTOCORE_HTTP_PARSER_H is not. A guard is also not part of the API surface that PC_ exists to keep short and typeable.
Every header file name under src/ is unique, and check_symbols.py enforces it. That uniqueness is what makes a filename-derived guard collision-proof, so it is checked rather than assumed: adding a second parser.h anywhere in the tree fails CI.
This replaces an earlier PC_<PATH>_H rule that derived the guard from the full path. That rule was unsatisfiable in this tree: it also demanded the 31-character limit from section 2, and 283 of 373 headers (75%) blew it - median 38, worst 62 (PC_SERVICES_SYSTEM_PROVISIONING_SERVICE_PROVISIONING_SERVICE_H). The path form was reasoned about against a three-level tree; the tree is now five and six levels deep. A rule that cannot be satisfied is not a strict rule, it is a rule that gets ignored, and an ignored rule stops protecting the case it was written for. PROTOCORE_<FILE>_H lands at median 20.
The filename form is authoritative when it fits. When it does not, the guard must still convey intent. Two headers overflow (PROTOCORE_NTRIP_CASTER_LISTENER_H at 33, PROTOCORE_PROVISIONING_SERVICE_H at 32). Neither is chopped mid-word: a guard whose last word has been cut off partway names nothing and teaches nothing. A whole word is elided instead, leaving a name that still says what the header is:
| Header | Guard | Length |
|---|---|---|
services/gnss/ntrip_caster_listener.h | PROTOCORE_NTRIP_LISTENER_H | 26 |
services/system/provisioning_service/…h | PROTOCORE_PROVISIONING_H | 25 |
Which word carries the meaning is a judgment call, which is why the exceptions are recorded here and in check_symbols.py rather than computed: caster is implied by ntrip, and a _service header is a service. There are exactly as many exceptions as this table has rows, and the checker rejects any guard that is neither the filename form nor a listed exception - it raises rather than inventing a shortening, because inventing one is exactly the mistake this paragraph exists to prevent.
This is section 3's rule applied to guards: names are for human recognition. A mechanically shortened name can be provably unique and still be a bad name, because uniqueness is a property of the string and recognition is a property of the reader. A word cut off partway does not read as a new identifier; it reads as the original with a typo, and the reader stops to work out which. Any rule here that produces names by truncation is optimizing for the checker over the person, and the person is who the name is for.
The limit itself is not negotiable. 31 is the C89 limit on significant characters in an external identifier; a longer name is not an error, it is unspecified - a conforming toolchain may consider only the first 31 and silently merge two guards, which manifests as a header that mysteriously does not get included. Every compiler currently in the target list keeps the full name, but "the toolchains we happen to have tried do not truncate" is a measurement, not a guarantee, and this library's whole argument is that its guarantees hold on the target rather than on the ones already tested. Staying inside the standard's floor is what makes it a guarantee. Truncation can itself create a collision, so check_symbols.py checks uniqueness of the final, truncated guard, not of the filename it came from.
#pragma once is not used: it is not standard, and the target list includes toolchains where its behavior across duplicated or symlinked headers is not guaranteed.
Source files and directories are snake_case, matching the API they declare and avoiding the case-sensitivity trap, where a repository developed on case-insensitive Windows breaks on case-sensitive Linux over a capitalization-only difference.
Markdown is the documented exception: docs use UPPER_SNAKE (README.md, FEATURES.md, SRCBANNED.md, and this file), including the per-die register references under board_drivers/hal/esp/ such as P4_MIPI_HELPERS.md. The case-sensitivity argument is about #include paths resolving differently per platform, which does not apply to prose.
Test environments and suites carry no house prefix. They are native_<topic> and test_<topic>, not native_pc_<topic>. A prefix exists to prevent collisions in a shared global namespace; a test environment name lives only in platformio.ini and has no such namespace to protect. native_ip says everything native_pc_ip would.
Recording the roads not taken, because "why not the obvious thing" is usually the more useful half.
namespace protocore around the C++ internals.** Rejected: it splits the library into two naming conventions with a translation layer between them, and buys scoping the flat prefix already provides. It would also put a C++-only construct in the path of a C-shaped API.#pragma once.** Rejected: not standard, and the target list is deliberately wide.The naming law is the visible half of a larger rule: where ProtoCore promises a behavior, the promise is checked against the emitted instructions, not argued from the source.
This is what makes a C++ implementation acceptable under a C API for control-law use. The claims that get this treatment are the ones a caller actually depends on:
begin(): no allocator call reachable in the relevant .text.Each is documented as claim, then disassembly, then why the disassembly establishes the claim. A guarantee without that chain is a comment, and comments do not survive a compiler upgrade.
The mechanical rules are checked, not trusted to review:
check_symbols.py decides only what is decidable: prefix and casing, macro scope and length, include-guard form, file naming, and the absence of namespace / using namespace. Judgment calls (is this name descriptive, should this particular enum de-prefix) are review, and this document is what the review argues from.
CI runs all three. A rule that is not enforced is a suggestion, and suggestions drift.
src/