Skip to content

Release v0.6.4 — native C/C++ and Python blocks in .stlib - #225

Merged
thiagoralves merged 4 commits into
mainfrom
development
Aug 26, 2026
Merged

Release v0.6.4 — native C/C++ and Python blocks in .stlib#225
thiagoralves merged 4 commits into
mainfrom
development

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Promotes development to main for the v0.6.4 release.

What's in it

#224 — libraries can now ship function blocks whose bodies are C/C++ or Python. STruC++ does not compile those bodies; it transports them. The archive carries the authored file verbatim and the consuming toolchain lowers it at its own build time, against whatever native bridge that toolchain implements.

That distinction is the point: compiling the bodies here would bake one fixed bridge ABI into every archive, so a bridge change would break every library published before it until each was rebuilt.

Concretely:

  • --compile-lib <dir> discovers .cpp/.c/.cc/.cxx/.py alongside .st/.il, routed into a separate bucket so a Python body never reaches the ST parser.
  • The interface is recovered from the ST header every such file carries, by projecting it to a header-only block and running the ordinary front end — one declaration parser, so a bad declaration in a .py gets the same diagnostics an .st would.
  • Native blocks land in manifest.functionBlocks marked implementation: "cpp" | "python", pointing at their file via sourceFile, and emit no chunk.
  • --no-source strips ST sources only: a native block has no chunk, so its source is the deliverable.
  • A library whose every input is native no longer fails. This is the production bug behind DOPE-585 — it previously reached compile() with an empty translation unit and died on No source files provided.

Plus the two review findings from #224, both of which had the library building success: true with a quietly wrong artifact:

  • A native FUNCTION was accepted then silently dropped from the manifest. Now a compile error explaining that a FUNCTION has no instance state for the bridge to hold. ST functions are unaffected — they keep their manifest entries and chunks.
  • One name claimed by two exports produced two manifest entries with no error. Now rejected across the whole manifest — functions, function blocks, types, globals, case-insensitively. That is wider than the reported case and closes three collisions that predate the feature (function vs function block, type vs function block, global vs function block), all previously silent.

Compatibility

Additive. An archive with no native blocks serializes exactly as before, so the existing library corpus and older editors are unaffected. An archive that does carry native blocks is a newer artifact by definition: an older consumer finds no chunk for them and fails loudly at compile rather than silently linking nothing.

Verification

  • Full suite 2277 passed, 7 skipped; tsc --noEmit clean.
  • Verified end-to-end on hardware: built a library with one C++ and one Python block via strucpp --compile-lib, installed it through the editor's real install path, compiled and uploaded a consumer project to a device (slm-rp4, runtime v4.1.10) and read the values back — 42 and 142 as expected, plus a negative control on changed inputs giving 42 and 51.
  • Also driven from the openplc-web UI, including the avr-gcc verification stage.

After merge

Tag v0.6.4 on main to trigger the release workflow. openplc-web and openplc-editor pin strucpp through binary-versions.json and need that release before openplc-web#699 and openplc-editor#1042 can consume the feature.

🤖 Generated with Claude Code

https://claude.ai/code/session_018qaJbNUTA9kMLL9Jr5FjUU

thiagoralves and others added 4 commits August 26, 2026 08:47
A library may ship function blocks whose bodies are C/C++ or Python. STruC++
does not compile those bodies — it transports them. The archive carries the
authored file verbatim and the consuming toolchain lowers it at its own build
time, against whatever native bridge that toolchain implements.

That distinction is the point. Compiling the bodies here would bake one fixed
bridge ABI into every archive, so a bridge change would break every library
published before it until each was rebuilt. Transporting the source keeps a
published library working across bridge revisions.

What is read is the ST header every such file already carries — the
FUNCTION_BLOCK line and its VAR_* blocks. That is ordinary ST, so the
interface is recovered by projecting the file down to a header-only block and
running it through the normal front end: one declaration parser, no second
implementation to drift, and a bad declaration in a .py file gets the same
diagnostics an .st file would. The body between the last END_VAR and the
closing END_FUNCTION_BLOCK never reaches the parser.

Native blocks land in `manifest.functionBlocks` looking like any other,
distinguished by `implementation: "cpp" | "python"` and pointing at their file
via `sourceFile`. They emit no chunk. Consumers that see `implementation` must
lower the source themselves; consumers that don't recognise the field find no
chunk either, so they fail loudly rather than silently linking nothing.

Also:

  - A library whose every input is native no longer fails. There is nothing
    for the compiler to do, so it is not called — previously this reached
    `compile()` with an empty translation unit and died on "No source files
    provided", which made an all-native library unbuildable. A library with no
    inputs at all still fails, as before.

  - `--compile-lib <dir>` discovers .cpp/.c/.cc/.cxx/.py alongside .st/.il, so
    an author drops Block.py in the folder next to Other.st and both end up in
    the archive.

  - `--no-source` strips ST sources only. Native bodies are exempt and must
    be: they have no chunk, so the source IS the deliverable and an archive
    without it is unbuildable by anyone. A native block cannot be shipped
    closed-source in this format.

Archives that contain no native blocks are unchanged, so the existing library
corpus and older editors are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018qaJbNUTA9kMLL9Jr5FjUU
Two review findings on the native-blocks change, both reproduced before fixing
and both of the same shape: the library built with `success: true` and handed
back an artifact that was quietly wrong. A published `.stlib` is immutable in
the field, so silence there costs more than a failure does.

B1 — a native FUNCTION was accepted, then dropped
-------------------------------------------------
`projectNativeHeaderToSt` rejected PROGRAM but accepted FUNCTION, projected its
header and closed it with END_FUNCTION. The result landed in the AST's function
list, which `compileNativeEntries` does not read, so:

    success: true   errors: []
    manifest.functions: []   manifest.functionBlocks: []

A "successful" library with the block present nowhere, surfacing later as an
undefined type in someone else's project.

There is no such thing as a C/C++ or Python FUNCTION here: the bridge hands the
body a pointer to per-instance storage and calls `setup` once before `loop` on
every scan, which is function-block shape. A FUNCTION has no instance to hold
that state in. So this is now a compile error naming the file and saying why.

ST functions are untouched — they compile through the normal path and keep
their manifest entries and chunks, including alongside a native block. This is
only about a `.cpp`/`.py` file declaring FUNCTION, which the editor's own UI
already forbids; it takes a hand-authored project to reach.

`compileNativeEntries` no longer returns a `functions` list, since native
entries are always function blocks now, and the `LibraryFunctionEntry` import
goes with it.

B2 — one name, two exports, no error
------------------------------------
STruC++'s duplicate detection fires only within a single kind and a single
translation unit. Two `FUNCTION_BLOCK Foo` in one compile is caught; `FUNCTION
Foo` beside `FUNCTION_BLOCK Foo` is not. And native headers compile in a
separate unit, so nothing there was ever compared against the ST — an author
moving a block from ST to C++ and forgetting to delete the `.st` got two `Foo`
entries with different interfaces, one with a chunk and one without.

`findDuplicateExports` now runs over the assembled manifest — functions,
function blocks, types and globals together, case-insensitively — and fails
naming the symbol and both kinds. That is deliberately wider than the reported
case: a library exports one thing per name, whatever those things are. It also
closes three collisions that predate this feature and were equally silent
(function vs function block, type vs function block, global vs function block).

Nothing downstream validates against these entries today, so the old behaviour
went unnoticed; the point is that the next reader that does validate would
inherit the ambiguity from an archive nobody can change.

Editor-side half of B1: the orchestrator read native sources from a hardcoded
`pous/function-blocks`, so a native FUNCTION (which lives in `pous/functions/`)
died on a path guessed wrong rather than reaching strucpp's explanation. The
directory now follows the POU type.

C1 was referenced in the review but never filed; not addressed here.

Tests: 12 new in `native-blocks.test.ts` covering both rejections, the wider
duplicate matrix, case-insensitive matching, and that a library with distinct
names still builds. Full suite 2265 passed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018qaJbNUTA9kMLL9Jr5FjUU
…locks-in-stlib

Enables DOPE-585. Consumer PRs: Autonomy-Logic/openplc-web#699 and Autonomy-Logic/openplc-editor#1042, both of which need binary-versions.json pointing at a release that carries this.

Archives with no native blocks serialize unchanged, so the existing library corpus and older editors are unaffected.
Patch bump for the native-block support merged in #224: libraries can now ship
C/C++ and Python function blocks, whose source the archive transports verbatim
for the consumer to lower at its own build time.

Additive and backward compatible — an archive with no native blocks serializes
exactly as before, so the existing library corpus and older editors are
unaffected.

openplc-web and openplc-editor pin strucpp through `binary-versions.json` and
need this version to consume the feature.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018qaJbNUTA9kMLL9Jr5FjUU
@thiagoralves
thiagoralves merged commit 09a9388 into main Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant