blob: bcfe0777a30a4330831ba8a4d88f3c68e6c36681 [file] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# plc4net-code-gen
The pure-.NET `.mspec` → C# generator. Parses `.mspec` with the checked-in
ANTLR-for-C# lexer/parser, walks the tree into a type-model IR, and emits the
model class, `StaticParse`, `Serialize` and `GetLengthInBits` per type — the
replacement for the Java freemarker `model-template` / `io-template` (whose
C# `io-template` was never migrated, so it only ever produced data classes).
## Run
```bash
dotnet run --project plc4net/tools/code-gen -c Release -- \
<protocol> <mspec-source[;mspec-source...]> <output-dir> [namespace] [--manual-static-calls]
```
Each source can be one `.mspec` file or a directory containing `.mspec`
files. Separate multiple sources with semicolons.
Generation is staged and then replaces `<output-dir>/model`, so a failed
write leaves the previous generated model intact. The files follow the
generated-model `.editorconfig` override: LF line endings and a final newline.
The CI model check also rejects generated C# files without a final LF, even
when the checked-in files match the generator output.
Regenerating these protocol model files is an explicit
driver-maintenance step; the driver slices that consume them commit the
result. This is separate from the checked-in ANTLR parser artifacts below,
whose reproducibility is enforced by CI.
### Regenerating protocol models
From the repository root, use the Maven wrapper profiles to regenerate the
checked-in Modbus and S7 protocol models or to verify that they have not
drifted from their `.mspec` sources:
```bash
./mvnw -f plc4net/pom.xml -Pupdate-generated-code generate-sources
./mvnw -f plc4net/pom.xml -Pcheck-generated-code generate-sources
```
The first command updates the checked-in model directories. The second writes
only to a temporary directory and fails when generated output differs from the
checked-in files; CI runs that check.
The profiles use `python` by default. On a system where Python 3 is exposed
only as `python3`, add `-Dpython.executable=python3` to either command.
## Supported subset and fail-fast behavior
The generator only emits a protocol when every field has a known C# wire
mapping. It deliberately fails before writing output for unsupported field
keywords, terminated arrays, unsupported data-IO shapes, unsupported temporal
primitives, non-big-endian byte order, and unimplemented `STATIC_CALL`
targets. This prevents a successful generation from silently changing a wire
layout or deferring a missing implementation to production.
Counted and byte-length arrays are supported; byte arrays with a byte-length
use the buffer bulk-read path. Modbus `rtuCrcCheck` and `asciiLrcCheck` are
generated as concrete helpers. Any other `STATIC_CALL` fails the generation,
unless `--manual-static-calls` is given.
With `--manual-static-calls`, every `STATIC_CALL` the generator has no
implementation for is left to the hand-written part of the protocol's partial
`<Protocol>StaticHelper` class, the way plc4j and plc4go keep their `StaticHelper`
by hand. The generated code calls `<Protocol>StaticHelper.<Function>(...)` as the
mspec writes it, and the generated half of the class does not declare the
function, so a function that nobody wrote fails the compile of the driver
instead of the generation. S7 is generated this way: its string, time and date
codings live in `S7StaticHelper.Manual.cs` next to the generated model.
A case of a discriminated type that does not pin a discriminator read from the
wire keeps the value it was parsed with, as plc4j does: Modbus'
`ModbusPDUError` pins `errorFlag` but not `functionFlag`, so it takes the
function code as an extra trailing constructor and `StaticParse` parameter and
returns it from the discriminator accessor. The message then serializes back to
the bytes it was parsed from, which a checksum over the serialized form relies
on. A discriminator that is a parser argument rather than a wire field is not
kept; it takes the type's default.
## Grammars and the checked-in parsers
`src/generated/` holds the ANTLR 4.13.2 output (lexer, parser, listener and
`.tokens` side-cars) for the two mspec grammars in
`code-generation/protocol-base-mspec/src/main/antlr4/org/apache/plc4x/plugins/codegenerator/language/mspec/`:
`MSpec.g4` and `expression/Expression.g4`. plc4net keeps no copy of them; they
are used exactly as they are upstream, the same files the Java toolchain
compiles. The output is **checked in**, not generated at build time —
day-to-day work needs no JDK and no ANTLR tool.
ANTLR pastes the text of a semantic predicate into the generated code
unchanged, and the lexer's `EmptyLine` rule spells its predicate the way the
Java runtime does, `{getCharPositionInLine() == 0}?`. The generated
`MSpecLexer` is a `partial` class, so `src/MSpecLexer.Predicates.cs` supplies
that method (it returns the C# runtime's `Column`) and the grammar needs no
C# port; without that file the generated lexer does not compile.
`MSpecLexerTests` pins the behaviour.
### Regenerating the parsers
This requires Python 3 and a Java runtime. The script downloads the pinned
ANTLR archive; it does not require a manually installed ANTLR command.
```bash
python plc4net/tools/code-gen/generate_parsers.py
python plc4net/tools/code-gen/generate_parsers.py --check
```
The script downloads the pinned ANTLR archive into the user cache, verifies
its SHA-256 checksum, applies the ASF header and repository line-ending rules,
and omits ANTLR's debug-only `.interp` files. The `--check` mode regenerates in
a temporary directory and fails when the checked-in artifacts drift. The .NET
workflow runs that check whenever PLC4NET or either shared grammar changes.
## Layout
| path | role |
|------------------------------|--------------------------------------------------|
| `MspecReader.cs` | text → ANTLR parse tree |
| `MspecModelBuilder.cs` | parse tree → `model/` IR |
| `MspecExpressionParser.cs` | a quoted mspec expression → `model/terms/` tree |
| `model/` | the type-model IR |
| `output/CSharpGenerator.cs` | IR → C# (class + parse / serialize / length) |
| `output/CSharpExpressionRenderer.cs` | `Term` → a C# expression |
| `Program.cs` | the CLI |