Skip to main content

struct2ui

Render C structs / JSON schemas as an editable PySide6 UI, and convert freely between C / JSON / bin. Built for algorithm parameter tuning: describe a C interface in JSON, auto-generate a Qt form, edit it, then export back to C source, JSON, or binary.

Features

  • JSON → UI: describe C structs / enums / arrays with minimal JSON and auto-render Qt widgets (int→QSpinBox, float→QDoubleSpinBox, enum→QComboBox, etc.).
  • Embeddable: StructEditor is a plain QWidget that drops into any PySide6 / PyQt UI.
  • Multi-format export: edited results export to C source, JSON, or binary; C source can also be parsed back into a schema.
  • Read bin back: import_bin loads a raw .bin (e.g. read back from the chip) into the UI — the byte-for-byte inverse of the binary export.
  • Validation: keyword spell-checking (with “Did you mean X?” hints), semantic audits (min<=max, step>0), and pipeline cross-validation.
  • ELF layout check: optionally verify each module — and the whole pipeline against its SDK container struct — against a compiled ELF's DWARF layout, catching offset/size drift and wrong stage order.
  • Qt-binding agnostic: built on Qt.py — works with PySide6 / PyQt6 / PySide2 / PyQt5.

Installation

pip install struct2ui

# Pick a Qt binding (choose one)
pip install "struct2ui[pyside6]"
pip install "struct2ui[pyqt6]"

# Enable ELF layout verification (optional)
pip install "struct2ui[elf]"

The library itself only depends on Qt.py; you must install a Qt binding (PySide6 / PyQt6 / etc.) yourself. ELF verification depends on pyelftools, installed via the [elf] extra.

Quick Start

from Qt import QtWidgets
from struct2ui import StructEditor

app = QtWidgets.QApplication([])

editor = StructEditor()
editor.set_sources(
    cfg_dir="cfg_t",        # modules dir: *.json holding struct/enum/typedef
    flow_file="abc.json",   # pipeline definition file
)
editor.resize(480, 600)
editor.show()

app.exec_()

JSON Schema Reference

This is the core of the library: you describe your C types in JSON and the UI is generated from it. Files live in the modules directory (cfg_dir), one *.json per logical group. Each file is a flat object whose keys are either type names (your structs / enums) or the reserved key typedefs.

File layout

{
  // optional free-form metadata (ignored by the loader, allowed anywhere)
  "version": "1.0",
  "description": "audio EQ parameters",

  // type aliases: map a custom C type to a primitive
  "typedefs": { "gain_t": "int32_t", "freq_t": "float" },

  // a struct definition
  "eq_cfg_t": {
    "type": "struct",
    "items": [
      { "name": "enabled", "type": "uint8_t", "value": 1 },
      { "name": "gain",    "type": "gain_t",  "value": 0,
        "min": -12, "max": 12, "step": 1, "unit": "dB",
        "tip": "output gain", "when": { "enabled": 1 } }
    ]
  },

  // an enum definition
  "mode_t": {
    "type": "enum",
    "items": { "OFF": 0, "LOW": 1, "HIGH": 2 }
  }
}

Top-level blocks

Block type Keys Notes
struct type, items (list of field specs) items is required
enum type, items (object {NAME: value}) reads back the enumerator name
union type, items (list of branch structs), branch (optional) tagged union; each branch is an inline struct selected by its when (see below)
typedefs object {alias: real_type} resolved before field building

Free-form metadata keys are allowed and ignored at any level: version, description, author, comment, note.

Field properties (inside a struct's items)

Each entry in items is a field spec. Only the following keys are recognized — any other key triggers an "unknown keyword" error (with a "Did you mean …?" hint):

Property Applies to Description
name all Required. C field name.
type all Required. C type or a typedefs alias (e.g. int32_t, float, char, an enum/struct name).
value all Default value. For arrays, a list (per element) or a scalar (applied to every element).
count arrays Makes the field an array. Integer, a #define name, an expression (N - 1), or a list for multi-dim ([3, 4], stored flat as 12).
min / max int, float Numeric bounds. Validated min <= max. Drive spin-box / slider / dial ranges. Like count, each may be an integer, a #define name, or an integer expression of those (e.g. MAX_GAIN - 1); it is resolved to a number at load time.
step int, float Increment, must be > 0.
decimals float Number of fractional digits shown/stored. Non-negative integer (capped at 18). If omitted, it is derived from step (e.g. step: 0.5 → 1 digit); if neither is set, defaults to 3. For double fields needing more precision, set decimals explicitly (e.g. 15-16).
unit all Unit suffix shown in the label, e.g. gain (dB).
tip all Tooltip text.
choices int, float, char[N] Discrete value set rendered as a combo (see below).
when all Conditional enable/disable (see below).
widget all Override the auto-picked editor (see table below).

widget values

When omitted, the widget is inferred from type (and from min/max, choices). Override it explicitly with widget:

widget Valid for Renders as
checkbox int Check box (also auto-picked when min:0, max:1).
toggle bool/int Toggle push-button.
combo int, float, char[N] Combo box; pair with choices.
slider int Slider with min/max labels (requires min & max).
dial int Rotary dial popup (requires min & max).
file array Path label + Browse button; loads array values from a text file.
multiline array Single-line shell that pops up a multi-line editor.
table struct array Force grid layout (one column per struct member; a union member pinned via branch expands its branch members into columns).

Numeric fields without an explicit widget become a QSpinBox / QDoubleSpinBox; scalar arrays become a comma-separated line edit.

choices (discrete values)

A non-enum integer / float / char[N] field can be constrained to a fixed set. Two forms are accepted:

"choices": [0, 1, 2, 3]                              // label = str(value)
"choices": [{ "Off": 0 }, { "Low": 1 }, { "High": 2 }]  // explicit labels

The combo reads back the underlying value (not the label), so C / JSON / bin export still emits the number (or string for char[N]). Audits enforce: each value within [min, max] when declared, and value (the default) must be one of the choices.

when (conditional enable/disable)

A field can be greyed out unless other fields hold specific values. when is an object mapping a sibling field name to its required value; all entries must match (logical AND):

{ "name": "cutoff", "type": "float", "when": { "enabled": 1, "mode": "HIGH" } }

The dependency name is matched against the dotted leaf name in scope. When a dependency is not found, the field stays enabled.

union (tagged union) & branch

A union block declares overlapping branches, each an inline struct in items. At runtime the branch whose when matches is the one shown / exported (first match wins); the union's .bin / .c size is always the largest branch and unmatched exports are zero-padded.

{ "name": "FilterCoeffs", "type": "union", "items": [
  { "name": "raw",    "type": "struct", "when": { "type": "IIR_BIQUARD_RAW" },
    "items": [ /* 5 floats */ ] },
  { "name": "design", "type": "struct", "when": { "type": { "!=": "IIR_BIQUARD_RAW" } },
    "items": [ { "name": "f0", "type": "float" }, /* ... */ ] }
] }

Set branch on the union to pin it to a single branch: only that branch is a candidate (the others are never shown and their when is ignored); the pinned branch is still gated by its own when. Pinning to a flat (all-leaf) branch lets a struct array carrying the union render as a table — the branch members become columns, and a row whose discriminator makes the branch when false has those cells disabled (value kept, still exported zero-padded).

{ "name": "coeffs", "type": "FilterCoeffs", "branch": "design" }

count expressions & multi-dimensional arrays

count may be an integer, a #define constant name, or an integer expression of those using + - * / % () (e.g. MAX_BAND_NUM - 1). A list makes a multi-dimensional array that is stored flat to match the C ABI:

{ "name": "matrix", "type": "float", "count": [3, 4] }  // 12 contiguous floats, shown as [3][4]

char[N] is treated as a C string by default (use widget to override).

Validation

Loading runs three layers of checks, surfaced in the load report panel:

  • Keyword spelling — unknown keys report "Did you mean tip?" style hints.
  • Semantic audits — min <= max, step > 0, count > 0, choices within bounds, default is one of the choices.
  • Pipeline cross-validation — values in the pipeline file are checked against the schema (range, choices, enum membership).

Pipeline File

While the modules directory (cfg_dir) defines your types, the pipeline file (flow_file) describes one concrete instance: which module stages are present, in what order, and their edited values. It is a single JSON object with one top-level group key (any name you like, e.g. the product / tuning name) whose value is the group body:

{
  "Speech Tuning": {
    "flow": ["RX NS", "RX DRC", "RX EQ"],  // stage order, optional
    "container": "SpeechConfig",           // optional, enables layout check

    "RX NS":  { "type": "SpeechNs2FloatConfig", "items": { /* values */ } },
    "RX DRC": { "type": "MultiCompexpConfig" },   // 'items' may be omitted
    "RX EQ":  { "type": "EqConfig", "items": { /* values */ } }
  }
}

Group-level keys

Key Required Notes
flow no Ordered list of stage names. The flow buttons and the export order follow it. Omit it and the stages run in the order they appear in the JSON. When present, every listed name must have a stage block; defined stages missing from flow are a warning.
container no Name of the SDK struct that holds all modules (see Container layout check). Omit to disable that check.
stage name — Any other key is a stage block; its name is the label shown on the flow button.

All keys are lowercase and case-sensitive — write flow, not Flow. A mistyped key (Flow, flwo, …) is reported with a "Did you mean flow?" hint. Free-form metadata keys (version, description, author, comment, note) are allowed and ignored.

Stage block

Key Required Notes
type yes A struct / union type name defined in the modules directory.
items no The edited values, keyed by field name. Omitting items means "all defaults" — the stage renders from the type's declared defaults.
member no For the container layout check: the member name inside container this stage maps to (defaults to the stage label).

Container layout check

When container names an SDK struct and an ELF is loaded, the whole pipeline is laid out in flow order — as if each stage were a member of that container — and compared member-by-member against the struct's DWARF layout (offset, size, total size). This catches stages in the wrong order: if the modules are all correct but reordered, you get a single "module order is wrong" error naming the expected order, instead of a pile of misleading offset mismatches.

Embedding into an Existing UI

StructEditor is a regular QWidget; just put it into a layout:

from Qt import QtWidgets
from struct2ui import StructEditor

class MyWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        editor = StructEditor()
        editor.set_sources(cfg_dir="cfg_t", flow_file="abc.json")
        self.setCentralWidget(editor)

StructEditor takes no source paths at construction time and keeps no persistent state of its own. Point it at its sources after creating it with set_sources() (or let the user pick them at runtime via Settings). If you want to remember the last-used paths across runs, store them in the host app and call set_sources() again at startup.

Constructor Parameters

Parameter Description
parent Qt parent object, defaults to None
appearance Button appearance overrides, {key: {'mode': ..., 'icon': ..., 'text': ...}}
custom_button_position Where add_custom_button() drops buttons relative to the built-ins: 'end' (after, default) or 'start' (before / far left)

Setting Sources

Source paths are configured after construction, not passed to the constructor. Omitting an argument leaves that slot untouched.

editor.set_sources(
    cfg_dir="cfg_t",      # modules dir: *.json with struct/enum/typedef defs
    flow_file="abc.json", # pipeline definition file
    elf_file="fw.elf",    # optional ELF for .bin layout verification
)
set_sources argument Description
cfg_dir Modules directory holding *.json (struct / enum / typedef definitions)
flow_file Path to the pipeline JSON file (pipeline definition)
elf_file Path to an ELF for .bin layout verification; setting it enables verification (toggle later via the Settings checkbox, which keeps the path)
elf_enabled Override whether ELF verification runs, independent of whether a path is set
reload Rebuild schema / flow / UI from the new sources, defaults to True
async_load Run that reload on a background thread (defaults to False). See Async Loading

Toolbar orientation is set with set_flow_orientation('horizontal' | 'vertical').

Async Loading

Parsing the modules, validating the pipeline and (optionally) reading the ELF all happen during the reload. For large module sets or ELFs this can take a while, and by default it runs synchronously — set_sources() blocks until the editor is fully built, which is what programmatic callers and tests want.

A GUI host can avoid the startup freeze by passing async_load=True after show(): the window appears immediately and shows a Loading... placeholder while the heavy parsing runs on a worker thread, then swaps in the real content when it is ready. Reloads triggered while one is already running supersede the older one.

editor.show()
editor.set_sources(
    cfg_dir="cfg_t",
    flow_file="abc.json",
    elf_file="fw.elf",
    async_load=True,        # parse off the GUI thread; window stays responsive
)

Custom Toolbar Buttons

Host applications can add their own buttons to the top path bar via add_custom_button(). All custom buttons form a single group, separated from the built-in buttons by a divider that is added automatically — you only choose which side they sit on (once, via the custom_button_position constructor arg).

editor = StructEditor(custom_button_position="end")
editor.set_sources(cfg_dir="cfg_t", flow_file="abc.json")

def on_send():
    print(editor.current_values())

btn = editor.add_custom_button(
    "Send",                 # tooltip / accessible label (shown when no icon)
    on_send,                # callable invoked on click (no arguments)
    icon="icons/send.png",  # optional; falls back to text when missing
    checkable=False,        # optional; make the button toggleable
)
# `btn` is the created QPushButton, returned so you can tweak it further.
Parameter Description
text Tooltip / accessible label, also shown when no icon is given. Required.
on_click Callable invoked on click, takes no arguments. Required.
icon Path to an icon file; falls back to text when the path is missing/empty.
checkable Make the button toggleable, defaults to False.

Returns the created QPushButton.

Programmatic API

Beyond the UI, StructEditor exposes methods to read state and drive exports from code (e.g. wired to a custom button):

Method Description
set_sources(cfg_dir=None, flow_file=None, elf_file=None, elf_enabled=None, reload=True, async_load=False) Point the editor at its source files after construction. See Setting Sources.
set_flow_orientation(orientation) Switch the flow-button strip between 'horizontal' (top, default) and 'vertical' (left). Rebuilds the strip and keeps the active stage selected.
show_section(label) -> None Open the stage/section named label in the content area (the same as clicking its flow button). Paints the error panel instead when that stage has pipeline errors; clears the content when the label is unknown.
current_values() -> dict Live editor values of the active section, keyed by field name (empty dict when no parameter section is shown).
current_paths() -> dict Current {'modules', 'pipeline', 'elf'} file paths; an empty string means that slot is unset.
add_custom_button(text, on_click, icon=None, checkable=False) -> QPushButton Add a host button to the top path bar. See Custom Toolbar Buttons.
export_bin(path, show_dialogs=False) -> bool Write the current pipeline to a .bin at path — the programmatic equivalent of clicking Export → Binary. Returns True when there are no ELF layout errors (or ELF verification is off), False otherwise.
import_bin(source, show_dialogs=False) -> bool Load a raw .bin image (e.g. read back from the chip) into the UI — the inverse of export_bin. source is a path or bytes. The image length must match the pipeline layout exactly; on any mismatch the image is rejected and the UI is left unchanged. On success it overwrites every stage's values and re-renders.

export_bin verifies the ELF layout first when an ELF is selected and enabled (the file is still written, matching the UI flow). With show_dialogs=True it pops the same success / ELF-mismatch / failure dialogs the UI shows; with the default show_dialogs=False no dialogs appear and render/write failures are raised as exceptions for the caller to handle.

paths = editor.current_paths()           # {'modules': ..., 'pipeline': ..., 'elf': ...}
name = paths['pipeline']                  # derive an output name from the pipeline
ok = editor.export_bin("out/speech.bin")  # True when ELF layout is clean

import_bin is the reverse trip: feed it a .bin the chip sent back and the UI is repopulated from it. Because the image is a byte-for-byte C-ABI layout, its length must match the pipeline layout exactly — a short or long image is rejected wholesale so a stale/mismatched dump can never partially corrupt the form.

ok = editor.import_bin("out/speech.bin")  # True when the image was applied

Export API

The low-level export functions can be used standalone, without any UI:

from struct2ui.schema import SchemaRegistry
from struct2ui.exporters import (
    emit_c,          # sections + registry -> C source string
    dumps_json,      # -> JSON string
    emit_bin,        # -> binary bytes
    read_bin,        # binary bytes -> values dict (inverse of emit_bin)
    merge_abi,       # merge ABI info
    verify_sections, # verify .bin layout against an ELF
    parse_c_source,  # C source -> parse result
    build_schema_dict,
)

Architecture

Layer Module Responsibility
Schema (pure data, no Qt) struct2ui.schema Parse *.json into a Field tree; spell-checking, semantic audits, pipeline cross-validation
UI rendering struct2ui.ui WidgetFactory, FormRenderer/TreeRenderer, array tables, when conditional binding
Export struct2ui.exporters C / JSON / bin export, C source reverse parsing, ELF verification
Top-level widget struct2ui.StructEditor Action toolbar / content area; Settings panel for source paths; load report panel

Development

pip install -e ".[dev]" --no-build-isolation
python -m pytest

Tests live under tests/ and drive real Qt widgets headlessly on the offscreen platform.

License

MIT © Jay

Metadata

Release files for struct2ui 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for struct2ui 0.5.1
File Size Uploaded
struct2ui-0.5.1.tar.gz 128.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for struct2ui 0.5.1
File Interpreter ABI Platform
struct2ui-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 233.8 kB

Release files / struct2ui-0.5.1.tar.gz

Download URL struct2ui-0.5.1.tar.gz
Size 128.4 kB
Tags Source
SHA-256 checksum
How to use checksums
899b655ed189879909861d84781b3a4b6c7e180c743cdc30f6c970d83932aa7b
BLAKE2b-256 checksum
How to use checksums
34594b6fe05df3dbf8b4d48c2f8bf0fcff8922eb755ea6ee2eb15f74475c13ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Release files / struct2ui-0.5.1-py3-none-any.whl

Download URL struct2ui-0.5.1-py3-none-any.whl
Size 105.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79f206816a44fb332ff3c46d4e797677a62e25b5152c711f9f935cfa3af5b998
BLAKE2b-256 checksum
How to use checksums
75e52f949d012ce6d293a972fd42a811aa6b41129d0a0716c73153b030b284d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Release history Release notifications | RSS feed

0.7.0

2 release files

0.6.0

2 release files

This release

0.5.1 This release

2 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page