Skip to content

docs: native function blocks — globals, instances, and the full IEC type surface - #14

Open
thiagoralves wants to merge 5 commits into
mainfrom
docs/native-block-globals-and-fb-instances
Open

docs: native function blocks — globals, instances, and the full IEC type surface#14
thiagoralves wants to merge 5 commits into
mainfrom
docs/native-block-globals-and-fb-instances

Conversation

@thiagoralves

@thiagoralves thiagoralves commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Documents what DOPE-584 actually delivered for native function blocks: shared globals, function block instances, and the full IEC type surface — structures, enumerations, multi-dimensional arrays and arrays of structures.

Pages

New

  • Python Data Types — structures, enumerations, arrays (including the lower-bound trap), arrays of structures, strings, temporal types
  • C++ Data Types — the same ground on the C++ side, where almost every spelling differs

Updated

  • cpp-structure.md — function block instances, VAR_EXTERNAL, the member-underscore mangling, temporal units, plus a correction (see below)
  • python-restrictions.md — shared globals, function block instances, EN/ENO, and the missing-import failure mode
  • python-structure.md — the supported-types table now reflects reality

Everything was run on hardware

Not written from reading the source. Each example was compiled and uploaded to an SLM-RP4 (Runtime v4) with openplc-cli, and the outputs read back through the debugger — 35 C++ patterns and 26 Python checks. Several findings contradict what the source alone would suggest:

Finding Why it matters
Member spelling is opposite between the languages. Python keeps the Variables Table spelling (mot.speed); C++ sees the compiler's uppercase (mot.SPEED). Getting it wrong in Python is an AttributeError at runtime, not a build error. I hit this writing the examples.
Python lists use IEC indices. ARRAY [1..2] is a list of length 3 whose index 0 is None. for row in grid: hands you a None first.
grid(i, j), not grid[i][j] for multi-dimensional arrays in C++. [i][j] does not compile.
A scalar enum pin needs MODE m = mode; first. mode.get() returns a wrapper that will not cast to int. An array element is already the raw enum and skips the step — the two differ.
A Python function block instance must be declared VAR. As an Input, all four pins arrive and reads work, but writes never reach the PLC — a TON sits at zero with nothing to explain why. Verified both ways.
The C++ ton0() call is what makes a timer advance. Without it: compiles, runs, Q = FALSE and ET = 0 forever. Confirmed by running both versions.
DATE is days; TIME/TOD/DT are nanoseconds. On device: D#1970-01-1110, T#2s2000000000.

Also documents a failure mode that lies about itself: drop one of the four required imports and the block exits reporting "PLC runtime has stopped." while the PLC is plainly still running. The wrapper's liveness check calls os.kill, os was never imported, and the NameError surfaces as a stopped runtime. Easy to hit — the imports look unused.

Correction to an existing claim

cpp-structure.md said ordering comparisons on STRING need .get() on both sides. They work directly on the variables; verified by compiling both forms.

Not covered

  • STRING[n] (a declared length) is still the transport's 126-character budget, so the docs state the cap rather than the declared length. Deferred to strucpp by DOPE-584.
  • Negative array bounds (ARRAY [-10..10]) are legal IEC 61131-3 but rejected project-wide by both the parser and the variables table, so they are not documented as usable.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ

thiagoralves and others added 3 commits August 26, 2026 09:10
…h languages

Two things a user can now do in a native block that the docs did not cover, one
of which they can get wrong silently.

**Shared globals from Python.** A Python block can read and write a
VAR_EXTERNAL, but because it runs in a separate process the read-modify-write is
not atomic the way it is in ST or C++. The PLC hands the global's value in with
the inputs and stores the block's copy back at the end of the cycle; each of
those takes the global's lock, but the whole cycle sits between them, so anything
another task writes in that window is overwritten. `g = g + 1` is therefore not a
safe increment if anything else also writes g.

That deserved a warning rather than a footnote, because nothing fails loudly —
the value is never corrupt, just occasionally stale, and a counter simply runs
slow. Measured on hardware and quoted: a Python block and a C++ block each adding
2 to one global left it holding consistently two thirds of their combined total.
The guidance is one writer per global, read freely, and accumulate in ST.

**Function block instances.** A Python block cannot declare one, and the page
now says why: an instance only advances when something calls it, and a Python
block has no way to call into the scan, so its pins would never update. Rather
than hand over pins that silently stay put, the compiler refuses. The
alternatives are documented — EN/ENO for controlling the Python block itself, and
instantiating the FB in an ST block and passing its outputs in.

A C++ block has no such limit, so the C++ page gains the worked example: declare
the instance under VAR, set its pins, call it, read it back. Including the two
details that are easy to get wrong — the instance keeps the name you declared
while its pins are upper-cased, and a member named after its own type carries a
trailing underscore.

The C++ page also claimed inputs and outputs were the only classes available to a
C++ block. All six are, so that is now a table, with a note that VAR_EXTERNAL in
C++ *is* atomic within a scan — which is the contrast that makes the Python
warning meaningful.

Verified against hardware before writing: the C++ snippet is the code that ran,
a standard-library TON reaching Q with ET at its preset and a user block's
accumulator holding exactly twice the call count over 5116 calls.

Depends on DOPE-584 (openplc-editor #1044 / openplc-web #701) — the variable
classes and instance handling described here land with it.
The page I wrote an hour ago said a Python block cannot declare one, and
explained why the compiler refuses it. Accurate at the time and now wrong: the
feature it described as impossible was in the plan, had been skipped, and has
since been built.

So the section says what actually happens. You declare the instance the way you
always would; you cannot call it, because your block runs in another process and
the instance lives in the PLC's; and you do not need to, because the PLC calls
every instance your block declares once per scan and your code just uses the
pins.

What needed spelling out is the part that looks synchronous and is not. Setting
an input and reading an output on adjacent lines reads like a call, but the
sequence per scan is: apply what the block wrote, call the instance, publish what
it produced — so the block sees that on its next cycle. It is the same one-cycle
lag every Python input already has, and easier to miss here for exactly that
reason.

Also documented: pins are upper-cased (the compiler upper-cases members, and
`ton0.in` would be a Python keyword anyway); outputs are read-only and internal
state is not exposed at all; the instance runs on the PLC's scan rather than the
block's ~100 ms loop, so a TON keeps accurate time however rarely Python looks at
it; several instances are called in Variables Table order; and the two shapes
still refused, an array of instances and a generic pin.

The C++ page's cross-reference is corrected to match — it had said Python has no
such capability, which is no longer the difference. The difference is the lag.
DOPE-584 gave native blocks full IEC type parity, and the docs still described
the old world: base types and flat arrays only. Two new pages cover what a
structure, an enumeration and an array actually look like on each side, because
the two languages do not agree and the differences are silent when you get them
wrong.

Every example here was compiled and run on an SLM-RP4 (Runtime v4) through
openplc-cli, with the values read back over the debugger. The findings that
would not have survived guesswork:

- Member spelling is opposite between the two languages. Python keeps the
  spelling from the Variables Table (`mot.speed`); C++ sees the compiler's
  uppercase (`mot.SPEED`). Getting this wrong in Python raises
  AttributeError at runtime, not at build time.
- A Python list is indexed by the IEC index, so `ARRAY [1..2]` is a list of
  length 3 whose element 0 is None. Iterating it naively hands you a None.
- A multi-dimensional array is `grid(i, j)` in C++, not `grid[i][j]`.
- A scalar enumeration pin in C++ needs assigning to the raw enum first;
  `mode.get()` returns a wrapper that will not cast to int. An array element
  is already the raw enum and needs no such step.
- A function block instance must be declared under VAR to be driven from
  Python. Declared as an Input its pins still arrive, but writes never reach
  the PLC, so a TON sits at zero with nothing to explain why.
- DATE is a count of days while TIME, TOD and DT are nanoseconds.

Also documents the missing-import failure mode, which reports itself as
"PLC runtime has stopped." while the PLC is plainly still running: the
wrapper's liveness check calls os.kill, `os` was never imported, and the
NameError is reported as a stopped runtime.

Corrects one claim on the existing C++ page: ordering comparisons work
directly on STRING variables, without going through .get().

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ
Brings the structures/enumerations/arrays pages onto this branch so the
native-block documentation lands as one change.

Resolved the overlap rather than keeping both copies:
- `cpp-structure.md` keeps this branch's Function Block Instances section and
  gains a pointer to the new C++ Data Types page.
- The new pages' own Function Block Instances sections are trimmed to a link,
  since this branch already covers the topic in full — including that the
  C++ `ton0()` call is what makes a timer advance, which a hardware run
  independently confirmed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ
@thiagoralves thiagoralves changed the title docs: shared globals from Python, and function block instances in both languages docs: native function blocks — globals, instances, and the full IEC type surface Aug 28, 2026
The class and the location answer different questions, and the examples
conflated them. `Input` and `Output` define the pins of a Function Block —
how a value moves in and out of that POU. A location says which physical
address a variable is wired to. A Program's I/O is `Local` with a `%IX` or
`%QX` location; the class never changes to match the direction of the signal.

`local-variables.md` already stated this correctly ("To read or write a
physical I/O pin from a Program | **Local** | ... The class stays Local"),
so the examples were contradicting the reference page.

Fixed:
- `getting-started/quick-start.md` — `output_state` was `Class: Output` with
  `%QX0.0`. Now Local, with a note on why, since this is the first located
  variable most readers ever declare.
- `examples/start-stop-seal-in.md` — told the reader that wiring the demo to
  real hardware meant switching the three variables to `Input` / `Output`
  class. It means adding locations and leaving them Local.

Checked and left alone: Function Block variables tables legitimately use
Input/Output (those are pins, and carry no location); the Resource's global
variables use the `Global` class with locations; `modbus-slave-outputs.md`
already had `Local` + `%QX0.0`; and "Digital Input" / "Analog Output" in the
addressing tables name address kinds, not variable classes.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ
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