Skip to content

Commit 8711919

Browse files
dgburrJason Mobarak
and
Jason Mobarak
authored
Generate Kaitai Struct Format Descriptions and run Kaitai Struct Compiler to generate language bindings (#1336)
Kaitai Struct is a declarative language used to describe binary data structures. KSY files containing format specifications for all supported SBP messages can be generated using the kaitai target. The resulting files can be found in the kaitai/ksy directory. This target also runs the Kaitai Struct compiler to generate bindings for python and perl. These bindings (along with test cases) can be found in the kaitai/python and kaitai/perl subdirectories respectively. Due to the nature of Kaitai Struct, the new bindings do not currently allow for serialisation (i.e. construction of SBP objects). Serialisation for java and python targets is supported as an experimental feature in the development version of Kaitai Struct (>0.10) so this restriction should hopefully be removed in the future. It should also be noted that the field names used by the KSY bindings are not 1:1 identical to the native bindings due constraints which Kaitai Struct places on various token types, e.g. it is forbidden to use upper case characters in field names. --------- Co-authored-by: Jason Mobarak <[email protected]>
1 parent cca58c9 commit 8711919

File tree

342 files changed

+70480
-62
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

342 files changed

+70480
-62
lines changed

.github/workflows/benchmark.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ on:
1212
- "python/**"
1313
- "haskell/**"
1414
- "rust/**"
15+
- "kaitai/python/**"
16+
- "kaitai/perl/**"
1517
- scripts/ci_benchmark.bash
1618
- scripts/ci_benchmark_install.bash
19+
- test_data/benchmark_main.py
1720
- .github/workflows/benchmark.yaml
1821
jobs:
1922
benchmark:

Dockerfile

+22
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ RUN \
8585
python3.10 python3.10-dev python3.10-distutils \
8686
dpkg-dev \
8787
cmake \
88+
libjson-perl \
89+
libdigest-crc-perl \
8890
&& curl -sSL https://get.haskellstack.org/ | sh \
8991
&& rm -rf /var/lib/apt/lists/* /tmp/* \
9092
&& curl -s "https://get.sdkman.io" | bash \
@@ -115,6 +117,26 @@ ENV PATH=$NVM_DIR/versions/node/$NODE_VERSION/bin:${PATH}
115117

116118
RUN npm install npm@latest mocha quicktype -g && sudo rm -rf /tmp/*
117119

120+
# install kaitai struct compiler
121+
RUN \
122+
cd /tmp \
123+
&& curl -LO https://github.com/kaitai-io/kaitai_struct_compiler/releases/download/0.10/kaitai-struct-compiler-0.10.zip \
124+
&& unzip kaitai-struct-compiler-0.10.zip \
125+
&& mv kaitai-struct-compiler-0.10 /opt \
126+
&& ln -sf /opt/kaitai-struct-compiler-0.10/bin/kaitai-struct-compiler /usr/local/bin \
127+
&& kaitai-struct-compiler --version \
128+
&& rm -rf /tmp/*
129+
130+
# install perl runtime for kaitai struct
131+
RUN \
132+
cd /tmp \
133+
&& curl -LO https://github.com/kaitai-io/kaitai_struct_perl_runtime/archive/refs/tags/0.10.zip \
134+
&& unzip 0.10.zip \
135+
&& cd kaitai_struct_perl_runtime-0.10 \
136+
&& perl Makefile.PL \
137+
&& make install \
138+
&& rm -rf /tmp/*
139+
118140
ARG UID=1000
119141

120142
# Add a "dockerdev" user with sudo capabilities

Makefile

+45-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ SBP_STAGING := $(shell git describe --match 'libsbp-staging*' --always --tags |
1919

2020
CHANGELOG_MAX_ISSUES := 100
2121

22-
.PHONY: help test release dist clean all docs pdf html c deps-c gen-c test-c python deps-python gen-python test-python javascript deps-javascript gen-javascript test-javascript java deps-java gen-java test-java haskell deps-haskell gen-haskell test-haskell haskell deps-protobuf gen-protobuf test-protobuf verify-prereq-generator verify-prereq-c verify-prereq-javascript verify-prereq-python verify-prereq-java verify-prereq-haskell verify-prereq-protobuf mapping rust deps-rust gen-rust test-rust deps-jsonschema gen-jsonschema test-jsonschema verify-prereq-jsonschema deps-quicktype-typescript gen-quicktype-typescript test-quicktype-typescript verify-prereq-quicktype-typescript deps-quicktype-javascript gen-quicktype-javascript test-quicktype-javascript verify-prereq-quicktype-javascript deps-quicktype-elm gen-quicktype-elm test-quicktype-elm verify-prereq-quicktype-elm
22+
.PHONY: help test release dist clean all docs pdf html c deps-c gen-c test-c python deps-python gen-python test-python javascript deps-javascript gen-javascript test-javascript java deps-java gen-java test-java haskell deps-haskell gen-haskell test-haskell haskell deps-protobuf gen-protobuf test-protobuf deps-kaitai gen-kaitai test-kaitai verify-prereq-generator verify-prereq-c verify-prereq-javascript verify-prereq-python verify-prereq-java verify-prereq-haskell verify-prereq-protobuf verify-prereq-kaitai mapping rust deps-rust gen-rust test-rust deps-jsonschema gen-jsonschema test-jsonschema verify-prereq-jsonschema deps-quicktype-typescript gen-quicktype-typescript test-quicktype-typescript verify-prereq-quicktype-typescript deps-quicktype-javascript gen-quicktype-javascript test-quicktype-javascript verify-prereq-quicktype-javascript deps-quicktype-elm gen-quicktype-elm test-quicktype-elm verify-prereq-quicktype-elm
2323

2424
# Functions
2525
define announce-begin
@@ -55,6 +55,7 @@ help:
5555
@echo " java to make Java bindings"
5656
@echo " rust to make Rust bindings"
5757
@echo " protobuf to make Protocol Buffer bindings"
58+
@echo " kaitai to make Kaitai Struct format descriptions"
5859
@echo " jsonschema to make JSON Schema definitions"
5960
@echo " release to handle some release tasks"
6061
@echo " test to run all tests"
@@ -67,7 +68,7 @@ help:
6768

6869
packaged-languages: c python haskell rust javascript
6970

70-
non-packaged-languages: java protobuf jsonschema quicktype
71+
non-packaged-languages: java protobuf kaitai jsonschema quicktype
7172

7273
all: packaged-languages docs non-packaged-languages
7374

@@ -87,6 +88,7 @@ java: deps-java gen-java test-java java-examples
8788
haskell: deps-haskell gen-haskell test-haskell
8889
rust: deps-rust gen-rust test-rust
8990
protobuf: deps-protobuf gen-protobuf test-protobuf
91+
kaitai : deps-kaitai gen-kaitai test-kaitai
9092
jsonschema: deps-jsonschema gen-jsonschema test-jsonschema
9193

9294
quicktype-typescript: deps-quicktype-typescript gen-quicktype-typescript test-quicktype-typescript
@@ -136,6 +138,9 @@ verify-prereq-rust:
136138

137139
verify-prereq-protobuf: ;
138140

141+
verify-prereq-kaitai:
142+
@command -v kaitai-struct-compiler 1>/dev/null 2>/dev/null || { echo >&2 -e "I require \`kaitai-struct-compiler\` but it's not installed. Aborting.\n"; exit 1; }
143+
139144
verify-prereq-jsonschema: ;
140145

141146
verify-prereq-quicktype:
@@ -169,6 +174,8 @@ deps-rust: verify-prereq-rust
169174

170175
deps-protobuf: verify-prereq-protobuf
171176

177+
deps-kaitai: verify-prereq-kaitai
178+
172179
deps-jsonschema: verify-prereq-jsonschema
173180

174181
deps-quicktype-typescript: verify-prereq-quicktype
@@ -179,7 +186,7 @@ deps-quicktype-elm: verify-prereq-quicktype
179186

180187
# Generators
181188

182-
gen: gen-c gen-python gen-javascript gen-java gen-haskell gen-rust gen-protobuf gen-jsonschema gen-quicktype
189+
gen: gen-c gen-python gen-javascript gen-java gen-haskell gen-rust gen-protobuf gen-kaitai gen-jsonschema gen-quicktype
183190
gen-quicktype: gen-quicktype-typescript gen-quicktype-elm
184191

185192
gen-c_args = -i $(SBP_SPEC_DIR) \
@@ -298,8 +305,6 @@ gen-rust:
298305

299306
$(call announce-end,"Finished formatting Rust code")
300307

301-
$(call announce-end,"Finished generating Rust bindings")
302-
303308
gen-protobuf:
304309
$(call announce-begin,"Generating Protocol Buffers bindings")
305310
cd $(SWIFTNAV_ROOT)/generator; \
@@ -309,6 +314,31 @@ gen-protobuf:
309314
--protobuf
310315
$(call announce-begin,"Finished generating Protocol Buffers bindings")
311316

317+
gen-kaitai:
318+
$(call announce-begin,"Generating Kaitai Struct Format Description")
319+
cd $(SWIFTNAV_ROOT)/generator; \
320+
$(SBP_GEN_BIN) -i $(SBP_SPEC_DIR) \
321+
-o $(SWIFTNAV_ROOT)/kaitai/ \
322+
-r $(SBP_VERSION) \
323+
--kaitai
324+
$(call announce-begin,"Finished generating Kaitai Struct Format Description")
325+
326+
$(call announce-begin,"Generating Kaitai Struct Python tests")
327+
cd $(SWIFTNAV_ROOT)/generator; \
328+
$(SBP_GEN_BIN) -i $(SBP_TESTS_SPEC_DIR) \
329+
-o $(SWIFTNAV_ROOT)/kaitai/python/kaitai_sbp/tests/ \
330+
-r $(SBP_VERSION) \
331+
--test-kaitai-python
332+
$(call announce-end,"Finished generating Kaitai Struct Python tests")
333+
334+
$(call announce-begin,"Generating Kaitai Struct Perl tests")
335+
cd $(SWIFTNAV_ROOT)/generator; \
336+
$(SBP_GEN_BIN) -i $(SBP_TESTS_SPEC_DIR) \
337+
-o $(SWIFTNAV_ROOT)/kaitai/perl/KaitaiSbp/t/ \
338+
-r $(SBP_VERSION) \
339+
--test-kaitai-perl
340+
$(call announce-end,"Finished generating Kaitai Struct Perl tests")
341+
312342
gen-jsonschema:
313343
$(call announce-begin,"Generating JSON Schema definitions")
314344
cd $(SWIFTNAV_ROOT)/generator; \
@@ -335,7 +365,7 @@ gen-quicktype-elm:
335365

336366
# Testers
337367

338-
test: test-all-begin test-c test-c-v4 test-java test-python test-haskell test-javascript test-rust test-all-end
368+
test: test-all-begin test-c test-c-v4 test-java test-python test-haskell test-javascript test-rust test-kaitai test-all-end
339369

340370
test-all-begin:
341371
$(call announce-begin,"Running all tests")
@@ -395,6 +425,15 @@ test-protobuf:
395425
$(call announce-begin,"Running Protocol Buffer tests")
396426
$(call announce-end,"Finished running Protocol Buffer tests")
397427

428+
test-kaitai:
429+
$(call announce-begin,"Running Kaitai Struct Python tests")
430+
cd $(SWIFTNAV_ROOT) && tox -c kaitai/python/kaitai_sbp/tests/tox.ini kaitai/python/kaitai_sbp/tests/test_*.py
431+
$(call announce-end,"Finished running Kaitai Struct Python tests")
432+
433+
$(call announce-begin,"Running Kaitai Struct Perl tests")
434+
cd $(SWIFTNAV_ROOT) && perl -MExtUtils::Command::MM -MTest::Harness -e test_harness kaitai/perl/KaitaiSbp/t/*.t
435+
$(call announce-end,"Finished running Kaitai Struct Perl tests")
436+
398437
test-jsonschema:
399438
$(call announce-begin,"Running JSON Schema tests")
400439
$(call announce-end,"Finished running JSON Schema tests")

README.md

+49-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [SBP Development Procedures](#sbp-development-procedures)
1616
- [SBP Protocol Specification](#sbp-protocol-specification)
1717
- [JSON Schema Definitions](#json-schema-definitions)
18+
- [Kaitai Struct Format Descriptions](#kaitai-struct-format-descriptions)
1819
- [LICENSE](#license)
1920

2021
<!-- tocstop -->
@@ -43,6 +44,7 @@ following directory structure:
4344
* [`javascript`](./javascript): JavaScript client library and examples.
4445
* [`rust`](./rust): Rust client library and examples.
4546
* [`sbpjson`](./sbpjson): Tools for parsing SBP-JSON.
47+
* [`kaitai`](./kaitai): Kaitai Struct Format Description and generated code.
4648

4749
Except for the `generator`, all of the above are generated and should not be modified directly.
4850

@@ -263,8 +265,54 @@ provided. Libraries for JavaScript, TypeScript, and Elm generated by the
263265
[QuickType](https://github.com/quicktype/quicktype) tool are provided. See the
264266
HOWTO for instructions on updating these schemas.
265267

268+
## Kaitai Struct Format Descriptions
269+
270+
[Kaitai Struct](https://kaitai.io/) is a declarative language used to describe
271+
binary data structures. KSY files containing format specifications for all
272+
supported SBP messages can be generated using the `kaitai` target. The
273+
resulting files can be found in the [`kaitai/ksy`](./kaitai/ksy) directory.
274+
This target also runs the Kaitai Struct compiler to generate bindings for
275+
perl and python. These bindings (along with test cases) can be found in the
276+
[`kaitai/perl`](./kaitai/perl) and [`kaitai/python`](./kaitai/python) subdirectories
277+
respectively.
278+
279+
The Kaitai Struct format description files can also potentially be used to
280+
generate bindings for the following targets:
281+
282+
* graphviz
283+
* csharp
284+
* rust
285+
* java
286+
* go
287+
* cpp_stl
288+
* php
289+
* lua
290+
* nim
291+
* html
292+
* ruby
293+
* construct
294+
* javascript
295+
296+
### Notes on python bindings
297+
298+
The python bindings generated by the Kaitai Struct compiler allow for
299+
significantly faster parsing in comparison to the construct-based bindings
300+
included in libsbp. However, due to the nature of Kaitai Struct, these
301+
bindings do not allow for serialisation (i.e. construction of SBP objects).
302+
Serialisation for java and python targets is supported as an experimental
303+
feature in the development version of Kaitai Struct (> 0.10) so this
304+
restriction should hopefully be removed in the future.
305+
306+
It should also be noted that the names used by the KSY bindings are not 1:1
307+
identical to the construct bindings due constraints which Kaitai Struct places
308+
on various token types (e.g. it is forbidden to use upper case characters in
309+
field names). The test cases in the [`kaitai/ksy/tests`](./kaitai/ksy/tests) directory
310+
(particularly those in `test_parsers.py`) can be used as a starting point for
311+
working with these bindings, as well as demonstrating the minor differences
312+
between the output generated by each set of bindings.
313+
266314
## LICENSE
267315

268-
Copyright © 2015-2022 Swift Navigation
316+
Copyright © 2015-2023 Swift Navigation
269317

270318
Distributed under the [MIT open source license](LICENSE).

generator/sbpg/generator.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ def get_args():
6969
parser.add_argument('--protobuf',
7070
action="store_true",
7171
help='Target language: Protocol Buffers.')
72+
parser.add_argument('--kaitai',
73+
action="store_true",
74+
help='Target language: Kaitai Struct Format Description.')
75+
parser.add_argument('--test-kaitai-python',
76+
action="store_true",
77+
help='Target language: Kaitai Struct Python tests.')
78+
parser.add_argument('--test-kaitai-perl',
79+
action="store_true",
80+
help='Target language: Kaitai Struct Perl tests.')
7281
parser.add_argument('--jsonschema',
7382
action="store_true",
7483
help='Target language: JSON Schema.')
@@ -112,7 +121,7 @@ def main():
112121
# Parse and validate arguments.
113122
args = get_args().parse_args()
114123
verbose = args.verbose
115-
assert args.jsonschema or args.python or args.javascript or args.c or args.test_c or args.c_sources or args.haskell or args.latex or args.protobuf or args.java or args.test_java or args.rust or args.test_rust, \
124+
assert args.jsonschema or args.python or args.javascript or args.c or args.test_c or args.c_sources or args.haskell or args.latex or args.protobuf or args.kaitai or args.test_kaitai_python or args.test_kaitai_perl or args.java or args.test_java or args.rust or args.test_rust, \
116125
"Please specify a target language."
117126
input_file = os.path.abspath(args.input_file[0])
118127
assert len(args.input_file) == 1
@@ -123,7 +132,7 @@ def main():
123132
assert os.path.exists(output_dir), \
124133
"Invalid output directory: %s. Exiting!" % output_dir
125134
# Ingest, parse, and validate.
126-
test_mode = args.test_c or args.test_rust or args.test_java
135+
test_mode = args.test_c or args.test_kaitai_python or args.test_kaitai_perl or args.test_java or args.test_rust
127136

128137
if test_mode:
129138
file_index = yaml.resolve_test_deps(*yaml.get_files(input_file))
@@ -184,6 +193,15 @@ def main():
184193
elif args.protobuf:
185194
import sbpg.targets.protobuf as pb
186195
pb.render_source(output_dir, parsed)
196+
elif args.kaitai:
197+
import sbpg.targets.kaitai as kaitai
198+
kaitai.render_source(output_dir, parsed)
199+
elif args.test_kaitai_python:
200+
import sbpg.targets.test_kaitai_python as test_kaitai_python
201+
test_kaitai_python.render_source(output_dir, parsed)
202+
elif args.test_kaitai_perl:
203+
import sbpg.targets.test_kaitai_perl as test_kaitai_perl
204+
test_kaitai_perl.render_source(output_dir, parsed)
187205
elif args.jsonschema:
188206
import sbpg.targets.jsonschema as jsonschema
189207
jsonschema.render_source(output_dir, parsed)
@@ -210,9 +228,11 @@ def main():
210228
elif args.test_c:
211229
test_c.render_check_suites(output_dir, all_specs)
212230
test_c.render_check_main(output_dir, all_specs)
213-
if args.test_rust:
231+
elif args.test_rust:
214232
import sbpg.targets.test_rust as test_rs
215233
test_rs.render_main(output_dir, all_specs)
234+
elif args.kaitai:
235+
kaitai.render_main(output_dir, all_specs, release)
216236

217237

218238
except KeyboardInterrupt:

generator/sbpg/targets/common.py

+37
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
1111

1212
import base64
13+
import re
14+
import json
1315

1416
def string_type(value):
1517
return type(value) == str
@@ -31,3 +33,38 @@ def to_str(value):
3133

3234
def b64_decode(field):
3335
return base64.standard_b64decode(field)
36+
37+
# convert CamelCase to snake_case
38+
def snake_case(s):
39+
if "_" in s:
40+
return "_".join(snake_case(p) for p in s.split("_"))
41+
if len(s) == 1:
42+
return s.lower()
43+
s = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", s)
44+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s).lower()
45+
46+
# convert snake_case to CamelCase
47+
def camel_case(s):
48+
s = re.sub("([a-z])([A-Z])", r"\1_\2", s)
49+
return "".join(w.title() for w in s.split("_"))
50+
51+
# convert all keys in a dict to snake_case
52+
def snake_case_keys(x):
53+
if isinstance(x, list):
54+
return [snake_case_keys(v) for v in x]
55+
elif isinstance(x, dict):
56+
return dict((snake_case(k), snake_case_keys(v)) for k, v in x.items())
57+
else:
58+
return x
59+
60+
# convert JSON to dict
61+
def decode_json(x):
62+
return json.loads(x)
63+
64+
# Convert dict to canonical JSON representation
65+
def encode_json(x):
66+
return json.dumps(x, allow_nan=False, sort_keys=True, separators=(',', ':'))
67+
68+
# escape non-printable characters
69+
def str_escape(x):
70+
return repr(x)[1:-1]

generator/sbpg/targets/haskell.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from jinja2.utils import pass_environment
2020

2121
from sbpg.targets.templating import JENV, indented_wordwrap
22+
from sbpg.targets.common import camel_case
2223
from sbpg import ReleaseVersion
2324

2425
MESSAGES_TEMPLATE_NAME = "SbpMessagesTemplate.hs"
@@ -68,12 +69,6 @@
6869
'string': 'putByteString $ encodeUtf8',
6970
}
7071

71-
def camel_case(s):
72-
"""
73-
Convert snake_case to camel_case.
74-
"""
75-
return "".join([i.capitalize() for i in s.split("_")])
76-
7772
def to_global(s):
7873
"""
7974
Format a global variable name.

0 commit comments

Comments
 (0)