Binding a C enum whose values are not 0..n-1¶
A real C API rarely numbers its enum 0, 1, 2, …. A sentinel sits at -1,
levels are spaced by ten, a first value starts at 1. This example binds one:
typedef enum {
LVL_AUTO = -1, ///< Let the gate choose (it picks `info`).
LVL_DEBUG = 10, ///< Diagnostic detail.
LVL_INFO = 20, ///< Normal operation.
LVL_WARN = 30, ///< Something needs attention.
} gate_level_t;
Python sees the strings "auto", "debug", "info" and "warn", and C
receives the constants. That holds on every path a choice crosses: a
constructor argument, a property, and a method argument.
1. Scaffold¶
20 is LVL_INFO's value. jm writes a state default into the C and into
the scaffolded Python test, and a C constant's name means nothing in the
Python.
2. Declare the enum, and use it¶
In just-makeit.toml, name the choices and, in the same order, the C
constant each one means:
[[enum]]
name = "level"
values = ["auto", "debug", "info", "warn"]
enumerators = ["LVL_AUTO", "LVL_DEBUG", "LVL_INFO", "LVL_WARN"]
Then use it from objects/gate.toml, three ways:
[[gate.init_params]] # Gate(level="auto")
name = "level"
type = "enum:level"
default = "auto"
[[gate.properties]] # g.threshold, read and written as a string
name = "threshold"
type = "int"
field = true
writable = true
enum = "level"
[[gate.methods]] # g.passes("warn")
name = "passes"
return_type = "int"
params = [{ name = "level", type = "int", enum = "level" }]
The constructor now takes level. The Python test jm new scaffolded
still carries its # jm:generated line, so it is jm's, and apply renders
it again for the new constructor:
Every table jm generates spells the constants by name:
So the C compiler checks each one. If a constant is renamed or deleted, the build fails. It can't quietly start meaning a different choice.
Without enumerators, a choice's position is its C value, as it always
was. That's right for an enum numbered 0..n-1 in declaration order, and
wrong for this one. In this example, "debug" would reach C as 1.
3. Write the C, and re-apply¶
The enum goes in the sacred header, above the state struct. The kernels go in
native/src/gate/gate_core.c:
levels_gate_state_t *
levels_gate_create(int level)
{
...
obj->threshold = level == LVL_AUTO ? LVL_INFO : level;
...
}
int
levels_gate_passes(levels_gate_state_t *state, int level)
{
return level >= state->threshold;
}
The same enumerators list tells jm where each choice's documentation
lives. After the second apply, help(Gate) lists every level with its
description:
level : Literal["auto", "debug", "info", "warn"], default "auto"
- ``"auto"`` — Let the gate choose (it picks `info`).
- ``"debug"`` — Diagnostic detail.
...
4. Build, and cross every path¶
cmake -B build -S . && cmake --build build && ctest --test-dir build
pip install -e . # points Python at src/, where the build put the extension
from levels import Gate
g = Gate() # "auto" -> LVL_AUTO (-1); C resolves it to info
assert g.threshold == "info"
g = Gate(level="debug") # LVL_DEBUG: 10, not index 1
g.threshold = "warn" # the setter stores LVL_WARN, 30
assert g.passes("info") == 0 and g.passes("warn") == 1
g.threshold = "auto" # -1 is legal, in both directions
assert g.threshold == "auto"
g.passes("verbose") # ValueError: invalid level 'verbose'
# (choices: auto, debug, info, warn)
-1 works in both directions. A string is looked up as its index first,
and only that index can signal "not found". The constant is read from the
table after that, so no C value is ever mistaken for a miss. Going the other
way, a value that is none of the constants raises ValueError rather than
reading past the table.