diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index 571b97045..671c11564 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -52,9 +52,9 @@ jobs: matrix: include: - message: cam - script: ./utils/codegen/asn1ToRosMsg.py asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn asn1/raw/cam_en302637_2/cdd/ITS-Container.asn -o etsi_its_msgs/etsi_its_cam_msgs/msg + script: ./utils/codegen/codegen-py/asn1ToRosMsg.py asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn asn1/raw/cam_en302637_2/cdd/ITS-Container.asn -o etsi_its_msgs/etsi_its_cam_msgs/msg - message: denm - script: ./utils/codegen/asn1ToRosMsg.py asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn asn1/raw/denm_en302637_3/cdd/ITS-Container.asn -o etsi_its_msgs/etsi_its_denm_msgs/msg + script: ./utils/codegen/codegen-py/asn1ToRosMsg.py asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn asn1/raw/denm_en302637_3/cdd/ITS-Container.asn -o etsi_its_msgs/etsi_its_denm_msgs/msg steps: - name: Checkout code @@ -84,9 +84,9 @@ jobs: matrix: include: - message: cam - script: ./utils/codegen/asn1ToConversionHeader.py asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn asn1/raw/cam_en302637_2/cdd/ITS-Container.asn -t cam -o etsi_its_conversion/etsi_its_cam_conversion/include/etsi_its_cam_conversion + script: ./utils/codegen/codegen-py/asn1ToConversionHeader.py asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn asn1/raw/cam_en302637_2/cdd/ITS-Container.asn -t cam -o etsi_its_conversion/etsi_its_cam_conversion/include/etsi_its_cam_conversion - message: denm - script: ./utils/codegen/asn1ToConversionHeader.py asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn asn1/raw/denm_en302637_3/cdd/ITS-Container.asn -t denm -o etsi_its_conversion/etsi_its_denm_conversion/include/etsi_its_denm_conversion + script: ./utils/codegen/codegen-py/asn1ToConversionHeader.py asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn asn1/raw/denm_en302637_3/cdd/ITS-Container.asn -t denm -o etsi_its_conversion/etsi_its_denm_conversion/include/etsi_its_denm_conversion steps: - name: Checkout code @@ -107,4 +107,4 @@ jobs: echo "Code generation script resulted in changes to the repository" git diff exit 1 - fi \ No newline at end of file + fi diff --git a/.gitlab-ci.codegen.yml b/.gitlab-ci.codegen.yml index 53de74e3d..a4f95d299 100644 --- a/.gitlab-ci.codegen.yml +++ b/.gitlab-ci.codegen.yml @@ -62,7 +62,7 @@ etsi_its_cam_msgs: needs: [] script: - > - ./utils/codegen/asn1ToRosMsg.py + ./utils/codegen/codegen-py/asn1ToRosMsg.py asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn asn1/raw/cam_en302637_2/cdd/ITS-Container.asn -o etsi_its_msgs/etsi_its_cam_msgs/msg @@ -73,7 +73,7 @@ etsi_its_denm_msgs: needs: [] script: - > - ./utils/codegen/asn1ToRosMsg.py + ./utils/codegen/codegen-py/asn1ToRosMsg.py asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn asn1/raw/denm_en302637_3/cdd/ITS-Container.asn -o etsi_its_msgs/etsi_its_denm_msgs/msg @@ -85,7 +85,7 @@ etsi_its_cam_conversion: needs: [] script: - > - ./utils/codegen/asn1ToConversionHeader.py + ./utils/codegen/codegen-py/asn1ToConversionHeader.py asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn asn1/raw/cam_en302637_2/cdd/ITS-Container.asn -t cam @@ -97,7 +97,7 @@ etsi_its_denm_conversion: needs: [] script: - > - ./utils/codegen/asn1ToConversionHeader.py + ./utils/codegen/codegen-py/asn1ToConversionHeader.py asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn asn1/raw/denm_en302637_3/cdd/ITS-Container.asn -t denm diff --git a/.vscode/launch.json b/.vscode/launch.json index e6090e935..7def9956e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,7 +34,7 @@ "name": "CAM asn1ToRosMsg", "type": "python", "request": "launch", - "program": "utils/codegen/asn1ToRosMsg.py", + "program": "utils/codegen/codegen-py/asn1ToRosMsg.py", "args": [ "asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn", "asn1/raw/cam_en302637_2/cdd/ITS-Container.asn", @@ -48,7 +48,7 @@ "name": "DENM asn1ToRosMsg", "type": "python", "request": "launch", - "program": "utils/codegen/asn1ToRosMsg.py", + "program": "utils/codegen/codegen-py/asn1ToRosMsg.py", "args": [ "asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn", "asn1/raw/denm_en302637_3/cdd/ITS-Container.asn", @@ -62,7 +62,7 @@ "name": "CAM asn1ToConversionHeader", "type": "python", "request": "launch", - "program": "utils/codegen/asn1ToConversionHeader.py", + "program": "utils/codegen/codegen-py/asn1ToConversionHeader.py", "args": [ "asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn", "asn1/raw/cam_en302637_2/cdd/ITS-Container.asn", @@ -77,7 +77,65 @@ "name": "DENM asn1ToConversionHeader", "type": "python", "request": "launch", - "program": "utils/codegen/asn1ToConversionHeader.py", + "program": "utils/codegen/codegen-py/asn1ToConversionHeader.py", + "args": [ + "asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn", + "asn1/raw/denm_en302637_3/cdd/ITS-Container.asn", + "-t", "denm", + "-o", "etsi_its_conversion/etsi_its_denm_conversion/include/etsi_its_denm_conversion" + ], + "console": "integratedTerminal", + "justMyCode": true + }, + + { + "name": "CAM asn1ToRosMsg (rgen)", + "type": "python", + "request": "launch", + "program": "utils/codegen/codegen-rust/asn1ToRosMsg.py", + "args": [ + "asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn", + "asn1/raw/cam_en302637_2/cdd/ITS-Container.asn", + "-o", "etsi_its_msgs/etsi_its_cam_msgs/msg" + ], + "console": "integratedTerminal", + "justMyCode": true + }, + + { + "name": "DENM asn1ToRosMsg (rgen)", + "type": "python", + "request": "launch", + "program": "utils/codegen/codegen-rust/asn1ToRosMsg.py", + "args": [ + "asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn", + "asn1/raw/denm_en302637_3/cdd/ITS-Container.asn", + "-o", "etsi_its_msgs/etsi_its_denm_msgs/msg" + ], + "console": "integratedTerminal", + "justMyCode": true + }, + + { + "name": "CAM asn1ToConversionHeader (rgen)", + "type": "python", + "request": "launch", + "program": "utils/codegen/codegen-rust/asn1ToConversionHeader.py", + "args": [ + "asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn", + "asn1/raw/cam_en302637_2/cdd/ITS-Container.asn", + "-t", "cam", + "-o", "etsi_its_conversion/etsi_its_cam_conversion/include/etsi_its_cam_conversion" + ], + "console": "integratedTerminal", + "justMyCode": true + }, + + { + "name": "DENM asn1ToConversionHeader (rgen)", + "type": "python", + "request": "launch", + "program": "utils/codegen/codegen-rust/asn1ToConversionHeader.py", "args": [ "asn1/raw/denm_en302637_3/DENM-PDU-Descriptions.asn", "asn1/raw/denm_en302637_3/cdd/ITS-Container.asn", diff --git a/README.md b/README.md index 4307e61dc..e48a93738 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,17 @@ The ROS message files are auto-generated based on the [ASN.1 definitions](https: ```bash # etsi_its_messages$ -./utils/codegen/asn1ToRosMsg.py \ +./utils/codegen/codegen-py/asn1ToRosMsg.py \ + asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn \ + asn1/raw/cam_en302637_2/cdd/ITS-Container.asn \ + -o etsi_its_msgs/etsi_its_cam_msgs/msg +``` + +Note that an alternative Rust-based code generation is currently being tested, which might offer better support for more complex message types such as SPATEM/MAPEM or CPM. + +```bash +# etsi_its_messages$ +./utils/codegen/codegen-rust/asn1ToRosMsg.py \ asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn \ asn1/raw/cam_en302637_2/cdd/ITS-Container.asn \ -o etsi_its_msgs/etsi_its_cam_msgs/msg @@ -179,7 +189,18 @@ The C++ conversion functions are auto-generated based on the [ASN.1 definitions] ```bash # etsi_its_messages$ -./utils/codegen/asn1ToConversionHeader.py \ +./utils/codegen/codegen-py/asn1ToConversionHeader.py \ + asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn \ + asn1/raw/cam_en302637_2/cdd/ITS-Container.asn \ + -t cam \ + -o etsi_its_conversion/etsi_its_cam_conversion/include/etsi_its_cam_conversion +``` + +Note that an alternative Rust-based code generation is currently being tested, which might offer better support for more complex message types such as SPATEM/MAPEM or CPM. + +```bash +# etsi_its_messages$ +./utils/codegen/codegen-rust/asn1ToConversionHeader.py \ asn1/raw/cam_en302637_2/CAM-PDU-Descriptions.asn \ asn1/raw/cam_en302637_2/cdd/ITS-Container.asn \ -t cam \ @@ -191,9 +212,6 @@ The C++ conversion functions are auto-generated based on the [ASN.1 definitions] All *etsi_its_messages* packages are released as official ROS / ROS 2 packages and can easily be installed via a package manager. -> [!WARNING] -> The initial release may not have been synced to the package managers yet. In the meantime, please refer to installation from source as shown below. - ```bash sudo apt update sudo apt install ros-$ROS_DISTRO-etsi-its-messages diff --git a/utils/codegen/asn1CodeGenerationUtils.py b/utils/codegen/codegen-py/asn1CodeGenerationUtils.py similarity index 100% rename from utils/codegen/asn1CodeGenerationUtils.py rename to utils/codegen/codegen-py/asn1CodeGenerationUtils.py diff --git a/utils/codegen/asn1ToConversionHeader.py b/utils/codegen/codegen-py/asn1ToConversionHeader.py similarity index 100% rename from utils/codegen/asn1ToConversionHeader.py rename to utils/codegen/codegen-py/asn1ToConversionHeader.py diff --git a/utils/codegen/asn1ToRosMsg.py b/utils/codegen/codegen-py/asn1ToRosMsg.py similarity index 100% rename from utils/codegen/asn1ToRosMsg.py rename to utils/codegen/codegen-py/asn1ToRosMsg.py diff --git a/utils/codegen/templates/RosMessageType.msg.jinja2 b/utils/codegen/codegen-py/templates/RosMessageType.msg.jinja2 similarity index 100% rename from utils/codegen/templates/RosMessageType.msg.jinja2 rename to utils/codegen/codegen-py/templates/RosMessageType.msg.jinja2 diff --git a/utils/codegen/templates/convertChoiceType.h.jinja2 b/utils/codegen/codegen-py/templates/convertChoiceType.h.jinja2 similarity index 100% rename from utils/codegen/templates/convertChoiceType.h.jinja2 rename to utils/codegen/codegen-py/templates/convertChoiceType.h.jinja2 diff --git a/utils/codegen/templates/convertCustomType.h.jinja2 b/utils/codegen/codegen-py/templates/convertCustomType.h.jinja2 similarity index 100% rename from utils/codegen/templates/convertCustomType.h.jinja2 rename to utils/codegen/codegen-py/templates/convertCustomType.h.jinja2 diff --git a/utils/codegen/templates/convertEnumeratedType.h.jinja2 b/utils/codegen/codegen-py/templates/convertEnumeratedType.h.jinja2 similarity index 100% rename from utils/codegen/templates/convertEnumeratedType.h.jinja2 rename to utils/codegen/codegen-py/templates/convertEnumeratedType.h.jinja2 diff --git a/utils/codegen/templates/convertPrimitiveType.h.jinja2 b/utils/codegen/codegen-py/templates/convertPrimitiveType.h.jinja2 similarity index 100% rename from utils/codegen/templates/convertPrimitiveType.h.jinja2 rename to utils/codegen/codegen-py/templates/convertPrimitiveType.h.jinja2 diff --git a/utils/codegen/templates/convertSequenceOfType.h.jinja2 b/utils/codegen/codegen-py/templates/convertSequenceOfType.h.jinja2 similarity index 100% rename from utils/codegen/templates/convertSequenceOfType.h.jinja2 rename to utils/codegen/codegen-py/templates/convertSequenceOfType.h.jinja2 diff --git a/utils/codegen/templates/convertSequenceType.h.jinja2 b/utils/codegen/codegen-py/templates/convertSequenceType.h.jinja2 similarity index 100% rename from utils/codegen/templates/convertSequenceType.h.jinja2 rename to utils/codegen/codegen-py/templates/convertSequenceType.h.jinja2 diff --git a/utils/codegen/codegen-rust/asn1ToConversionHeader.py b/utils/codegen/codegen-rust/asn1ToConversionHeader.py new file mode 100755 index 000000000..01abc27a6 --- /dev/null +++ b/utils/codegen/codegen-rust/asn1ToConversionHeader.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# ============================================================================== +# MIT License +# +# Copyright (c) 2023 Institute for Automotive Engineering (ika), RWTH Aachen University +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================== + +import argparse +import glob +import os +import re +import shutil +import subprocess +import tempfile + +def parseCli(): + """Parses script's CLI arguments. + + Returns: + argparse.Namespace: arguments + """ + + parser = argparse.ArgumentParser( + description="Creates conversion headers from ASN1 definitions.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument("files", type=str, nargs="+", help="ASN1 files") + parser.add_argument("-o", "--output-dir", type=str, required=True, help="output directory") + parser.add_argument("-td", "--temp-dir", type=str, default=None, help="temporary directory for mounting files to container; uses tempfile by default") + parser.add_argument("-t", "--type", type=str, required=True, help="ASN1 type") + parser.add_argument("-di", "--docker-image", type=str, default="ghcr.io/ika-rwth-aachen/etsi_its_messages:rgen", help="rgen Docker image") + + args = parser.parse_args() + + return args + +def main(): + + args = parseCli() + + # create output directory + os.makedirs(args.output_dir, exist_ok=True) + + # create temporary directories for running rgen in docker container + with tempfile.TemporaryDirectory() as temp_input_dir: + with tempfile.TemporaryDirectory() as temp_output_dir: + + if args.temp_dir is None: + container_input_dir = temp_input_dir + container_output_dir = temp_output_dir + else: + container_input_dir = os.path.join(args.temp_dir, "input") + container_output_dir = os.path.join(args.temp_dir, "output") + os.makedirs(container_input_dir, exist_ok=True) + os.makedirs(container_output_dir, exist_ok=True) + + # copy input asn1 files to temporary directory + for f in args.files: + shutil.copy(f, container_input_dir) + + # run rgen docker container to generate conversion headers + subprocess.run(["docker", "run", "--rm", "-u", f"{os.getuid()}:{os.getgid()}", "-v", f"{container_input_dir}:/input:ro", "-v", f"{container_output_dir}:/output", args.docker_image, 'conversion-headers', args.message], check=True) + + # add auto-gen info, remove in-file type and message name info (optional) + for f in glob.glob(os.path.join(container_output_dir, "*.h")): + with open(f, "r") as file: + msg = file.read() + + msg = re.sub(r"^////\s([\w-]+)\s.*\b", + "// --- Auto-generated by asn1ToConversionHeader.py -----------------------------", + msg, + flags=re.MULTILINE) + + with open(f, "w") as file: + file.write(msg) + + # move generated conversion headers to output directories + for f in glob.glob(os.path.join(container_output_dir, "*.h")): + shutil.move(f, os.path.join(args.output_dir, os.path.basename(f))) + + +if __name__ == "__main__": + + main() diff --git a/utils/codegen/codegen-rust/asn1ToRosMsg.py b/utils/codegen/codegen-rust/asn1ToRosMsg.py new file mode 100755 index 000000000..48e2f03d0 --- /dev/null +++ b/utils/codegen/codegen-rust/asn1ToRosMsg.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 + +# ============================================================================== +# MIT License +# +# Copyright (c) 2023 Institute for Automotive Engineering (ika), RWTH Aachen University +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================== + +import argparse +import glob +import os +import re +import shutil +import subprocess +import tempfile +from typing import Dict, List + +def parseCli(): + """Parses script's CLI arguments. + + Returns: + argparse.Namespace: arguments + """ + + parser = argparse.ArgumentParser( + description="Creates ROS .msg files from ASN1 definitions.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument("files", type=str, nargs="+", help="ASN1 files") + parser.add_argument("-o", "--output-dir", type=str, required=True, help="output directory") + parser.add_argument("-td", "--temp-dir", type=str, default=None, help="temporary directory for mounting files to container; uses tempfile by default") + parser.add_argument("-di", "--docker-image", type=str, default="ghcr.io/ika-rwth-aachen/etsi_its_messages:rgen", help="rgen Docker image") + + args = parser.parse_args() + + return args + +def asn1Definitions(files: List[str]) -> Dict[str, str]: + """Parses ASN1 files, extracts raw string definitions by type. + + Args: + files (List[str]): filepaths + + Returns: + Dict[str, str]]: raw string definition by type + """ + + asn1_raw = {} + for file in files: + with open(file) as f: + lines = f.readlines() + raw_def = None + for line in lines: + if "::=" in line: + if line.rstrip().endswith("{"): + type = line.split("::=")[0].split("{")[0].strip().split()[0] + raw_def = "" + elif len(line.split("::=")) == 2: + type = line.split("::=")[0].strip().split()[0] + if "}" in line or not ("{" in line or "}" in line): + raw_def = line + ros_type = type.replace("-", "") + asn1_raw[ros_type] = raw_def + raw_def = None + else: + raw_def = "" + if raw_def is not None: + raw_def += line + if "}" in line and not "}," in line and not ("::=" in line and line.rstrip().endswith("{")): + ros_type = type.replace("-", "") + asn1_raw[ros_type] = raw_def + raw_def = None + + return asn1_raw + + +def main(): + + args = parseCli() + + # create output directory + os.makedirs(args.output_dir, exist_ok=True) + + # create temporary directories for running rgen in docker container + with tempfile.TemporaryDirectory() as temp_input_dir: + with tempfile.TemporaryDirectory() as temp_output_dir: + + if args.temp_dir is None: + container_input_dir = temp_input_dir + container_output_dir = temp_output_dir + else: + container_input_dir = os.path.join(args.temp_dir, "input") + container_output_dir = os.path.join(args.temp_dir, "output") + os.makedirs(container_input_dir, exist_ok=True) + os.makedirs(container_output_dir, exist_ok=True) + + # copy input asn1 files to temporary directory + for f in args.files: + shutil.copy(f, container_input_dir) + + # run rgen docker container to generate .msg files + subprocess.run(["docker", "run", "--rm", "-u", f"{os.getuid()}:{os.getgid()}", "-v", f"{container_input_dir}:/input:ro", "-v", f"{container_output_dir}:/output", args.docker_image, 'msgs', ""], check=True) + + # edit generated ROS .msg files to add auto-gen info, ASN.1 raw definitions (optional) + asn1_raw = asn1Definitions(args.files) + for f in glob.glob(os.path.join(container_output_dir, "*.msg")): + with open(f, "r") as file: + msg = file.read() + + type = os.path.basename(f).split('.')[0] + raw_def = asn1_raw[type] + comments = "# --- Auto-generated by asn1ToRosMsg.py ----------------------------------------\n\n" +\ + "# --- ASN.1 Definition ---------------------------------------------------------\n" +\ + "\n".join(["# " + line for line in raw_def.split('\n')][:-1]) + '\n' +\ + "# ------------------------------------------------------------------------------" + msg = re.sub(r"^##\s([\w-]+)\s" + type + r"\b", comments, msg, flags=re.MULTILINE) + + with open(f, "w") as file: + file.write(msg) + + # move generated ROS .msg files to output directories + for f in glob.glob(os.path.join(container_output_dir, "*.msg")): + shutil.move(f, os.path.join(args.output_dir, os.path.basename(f))) + + +if __name__ == "__main__": + + main() diff --git a/utils/codegen/codegen-rust/docker/rgen.Dockerfile b/utils/codegen/codegen-rust/docker/rgen.Dockerfile new file mode 100644 index 000000000..c6f335b2a --- /dev/null +++ b/utils/codegen/codegen-rust/docker/rgen.Dockerfile @@ -0,0 +1,54 @@ +# docker build -t ghcr.io/ika-rwth-aachen/etsi_its_messages:rgen -f rgen.Dockerfile . + +FROM ubuntu:22.04 + +# install essentials +RUN apt-get update && \ + apt-get install -y \ + build-essential \ + git \ + curl \ + tar \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# install Rust/Cargo +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +# Compile .msg, conversion headers generators +WORKDIR /setup +COPY rgen/ rgen/ +WORKDIR /setup/rgen +RUN cargo build --release && \ + mv target/release/asn1-to-ros-msgs /usr/local/bin/asn1-to-ros-msgs && \ + mv target/release/asn1-to-ros-conversion-headers /usr/local/bin/asn1-to-ros-conversion-headers + +# cleanup +WORKDIR / +RUN rm -rf /setup + +# command +RUN mkdir input +RUN mkdir output +WORKDIR /output +RUN echo "\ +generator=\$1\n\ +pdu=\$2\n\ +case \$generator in\n\ + 'msgs')\n\ + asn1-to-ros-msgs -o . \$(find /input -name '*.asn')\n\ + ;;\n\ + 'conversion-headers')\n\ + asn1-to-ros-conversion-headers -o . -p \$pdu \$(find /input -name '*.asn')\n\ + ;;\n\ + *)\n\ + echo 'Unknown generator \$generator'\n\ + exit 1\n\ + ;;\n\ +esac\n\ +" > /rgen.sh +ENTRYPOINT ["/bin/bash", "/rgen.sh"] +CMD ["msgs", "test"] diff --git a/utils/codegen/codegen-rust/rgen/.gitignore b/utils/codegen/codegen-rust/rgen/.gitignore new file mode 100644 index 000000000..1276b0ad1 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/.gitignore @@ -0,0 +1,3 @@ +/target +/out/* +!/out/.gitkeep diff --git a/utils/codegen/codegen-rust/rgen/Cargo.lock b/utils/codegen/codegen-rust/rgen/Cargo.lock new file mode 100644 index 000000000..c4e1db83e --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/Cargo.lock @@ -0,0 +1,533 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rasn-compiler" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3adfed8c1b48b6b8bb4c889c947376c9d27c69557975de3a490111c6df32aff9" +dependencies = [ + "chrono", + "nom", + "num", + "proc-macro2", + "quote", + "wasm-bindgen", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "ros-backend" +version = "0.1.0" +dependencies = [ + "clap", + "rasn-compiler", + "regex", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/utils/codegen/codegen-rust/rgen/Cargo.toml b/utils/codegen/codegen-rust/rgen/Cargo.toml new file mode 100644 index 000000000..b9e27e986 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ros-backend" +version = "0.1.0" +edition = "2021" + +[lib] +name = "ros_backend" +path = "src/lib.rs" + +[[bin]] +name = "asn1-to-ros-msgs" +path = "src/msgs/bin.rs" + +[[bin]] +name = "asn1-to-ros-conversion-headers" +path = "src/conversion/bin.rs" + +[dependencies] +rasn-compiler = "0.1.4" +regex = "1.10.4" +clap = { version = "4.5.4", features = ["derive"] } diff --git a/utils/codegen/codegen-rust/rgen/README.md b/utils/codegen/codegen-rust/rgen/README.md new file mode 100644 index 000000000..fd3aeb137 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/README.md @@ -0,0 +1,11 @@ +# ASN.1 to ROS Compiler +Backends for the rasn compiler, which converts ASN.1 files to ROS message files, and generates support conversion headers for translation between asn1c structs and ROS structs. + +Support mainly for ETSI ITS messages. + +## Usage +To generate the ROS `.msg`s run `cargo run --bin asn1-to-ros-msgs -- -o [ASN.1 files ...]`, where `` is the output directory. + +To generate the conversion headers run `cargo run --bin asn1-to-ros-conversion-headers -- -p -o [ASN.1 files ...]`, where `` is the main PDU name used as a reference (e.g. `cam`, `denm`). + +The corresponding ROS `.msg`s and conversion headers will be generated in the `out` directory. diff --git a/utils/codegen/codegen-rust/rgen/src/common/mod.rs b/utils/codegen/codegen-rust/rgen/src/common/mod.rs new file mode 100644 index 000000000..8ca75a289 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/common/mod.rs @@ -0,0 +1,70 @@ +use rasn_compiler::prelude::ir::IntegerType; + +pub trait IntegerTypeExt { + fn to_str(self) -> &'static str; +} + +impl IntegerTypeExt for IntegerType { + fn to_str(self) -> &'static str { + match self { + IntegerType::Int8 => "int8", + IntegerType::Uint8 => "uint8", + IntegerType::Int16 => "int16", + IntegerType::Uint16 => "uint16", + IntegerType::Int32 => "int32", + IntegerType::Uint32 => "uint32", + IntegerType::Int64 => "int64", + IntegerType::Uint64 => "uint64", + IntegerType::Unbounded => "int64", + } + } +} +pub fn to_ros_snake_case(input: &str) -> String { + let input = input.replace('-', "_"); + let mut lowercase = String::with_capacity(input.len()); + + let peekable = &mut input.chars().peekable(); + while let Some(c) = peekable.next() { + if c.is_lowercase() || c.is_numeric() { + lowercase.push(c); + /* underscore before uppercase following a lowercase, aBx -> a_bx + * underscore before numeric following a lowercase, r09 -> r_09 */ + if c != '_' && peekable.peek().map_or(false, |next| next.is_uppercase() || !c.is_numeric() && next.is_numeric()) { + lowercase.push('_'); + } + } else { + /* underscore before uppercase followed by lowercase, ABx -> a_bx */ + if c != '_' + && lowercase.len() > 0 + && !lowercase.ends_with('_') + && peekable.peek().map_or(false, |next| next.is_lowercase()) + { + lowercase.push('_'); + } + lowercase.push(c.to_ascii_lowercase()); + } + } + lowercase +} + +pub fn to_ros_const_case(input: &str) -> String { + to_ros_snake_case(input).to_string().to_uppercase() +} + +pub fn to_ros_title_case(input: &str) -> String { + if !input.is_empty() { + let input = input.replace('-', ""); + input[0..1].to_uppercase() + &input[1..] + } else { + input.to_string() + } +} + +pub fn to_c_title_case(input: &str) -> String { + let input = input.replace('-', "_"); + match input.as_str() { + "long" => "Long".to_string(), + "class" => "Class".to_string(), + _ => input + } +} diff --git a/utils/codegen/codegen-rust/rgen/src/conversion/bin.rs b/utils/codegen/codegen-rust/rgen/src/conversion/bin.rs new file mode 100644 index 000000000..d6579e7ef --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/conversion/bin.rs @@ -0,0 +1,46 @@ +use clap::Parser; +use regex::Regex; + +use rasn_compiler::prelude::*; +use ros_backend::conversion::Conversion; + +#[derive(Parser, Debug)] +struct Cli { + /// Main PDU name + #[arg(short, long)] + pdu: String, + #[clap(short, long)] + /// Output directory + out: std::path::PathBuf, + /// ASN.1 files to compile + paths: Vec, +} + +fn main() { + let args = Cli::parse(); + + let backend = Conversion::default().set_main_pdu_name(&args.pdu.clone()); + + // Compile conversion headers + let compiler_res = Compiler::new() + .with_backend(backend) + .add_asn_sources_by_path(args.paths.iter()) + .compile_to_string(); + let generated = &compiler_res.unwrap().generated; + + // Split generated code into individual messages + let re_name = Regex::new(r"\/\/\/\/\s([\w-]+)\s(\w+)\b").unwrap(); + let re_def = Regex::new(r"#\n((.|\n)*?)#").unwrap(); + generated.split_inclusive("").for_each(|s| { + if let Some(def_caps) = re_def.captures(s) { + let definition = def_caps.get(1).unwrap().as_str(); + let name = if let Some(name_caps) = re_name.captures(definition) { + name_caps.get(2).unwrap().as_str() + } else { + "unknown" + }; + let path = args.out.join(format!("convert{}.h", name)); + std::fs::write(path, definition).unwrap(); + } + }); +} diff --git a/utils/codegen/codegen-rust/rgen/src/conversion/builder.rs b/utils/codegen/codegen-rust/rgen/src/conversion/builder.rs new file mode 100644 index 000000000..f2ae1106e --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/conversion/builder.rs @@ -0,0 +1,530 @@ +use std::error::Error; + +use rasn_compiler::prelude::{ir::*, *}; + +use crate::common::{to_ros_const_case, IntegerTypeExt}; +use crate::conversion::{generate, Conversion, ConversionOptions}; +use crate::conversion::{template::*, utils::*}; + +pub(crate) const INNER_ARRAY_LIKE_PREFIX: &str = "Anonymous_"; + +impl Backend for Conversion { + fn generate_module( + &self, + tlds: Vec, + ) -> Result { + let tlds = merge_tlds(tlds); + let (pdus, warnings): (Vec, Vec>) = + tlds.into_iter().fold((vec![], vec![]), |mut acc, tld| { + match generate(&self.options, tld) { + Ok(s) => { + s.len().gt(&0).then(|| { + acc.0.push(format!( + "#\n\ + {s}\n\ + #" + )) + }); + acc + } + Err(e) => { + acc.1.push(Box::new(e)); + acc + } + } + }); + Ok(GeneratedModule { + generated: Some(format!("{}", pdus.join("\n\n"))), + warnings, + }) + } +} + +pub fn merge_tlds(tlds: Vec) -> Vec { + let mut merged_tlds = Vec::::with_capacity(tlds.len()); + let mut merge_to = Vec::<(&ToplevelDefinition, &String)>::new(); + + // Add value to type's distinguished values + tlds.iter().for_each(|tld| { + if let ToplevelDefinition::Value(v) = &tld { + if let ASN1Value::LinkedIntValue { .. } = &v.value { + merge_to.push((&tld, &v.associated_type)); + } else { + merged_tlds.push(tld.clone()); + } + } else { + merged_tlds.push(tld.clone()); + } + }); + merge_to.iter().for_each(|(tld, ty)| { + for t in &mut merged_tlds { + if let ToplevelDefinition::Type(tt) = t { + if tt.name == **ty { + if let ASN1Type::Integer(ref mut int) = tt.ty { + let value = match &tld { + ToplevelDefinition::Value(v) => { + if let ASN1Value::LinkedIntValue { + integer_type: _, + value, + } = &v.value + { + value + } else { + unreachable!() + } + } + _ => unreachable!(), + }; + match int.distinguished_values { + Some(ref mut dv) => dv.push(DistinguishedValue { + name: tld.name().clone(), + value: *value, + }), + None => { + int.distinguished_values = Some(vec![DistinguishedValue { + name: tld.name().clone(), + value: *value, + }]); + } + } + } + break; + } + } + } + }); + + // Resolve object set references + let mut object_sets = Vec::<(String, ObjectSet)>::new(); + merged_tlds.iter().for_each(|tld| { + if let ToplevelDefinition::Information(i) = tld { + if let ASN1Information::ObjectSet(os) = &i.value { + object_sets.push((tld.name().clone(), os.clone())); + } + } + }); + merged_tlds.iter_mut().for_each(|tld| { + if let ToplevelDefinition::Type(t) = tld { + if let ASN1Type::Sequence(ref mut s) = t.ty { + s.members.iter_mut().for_each(|m| { + if let ASN1Type::InformationObjectFieldReference(ref mut r) = m.ty { + if let Constraint::TableConstraint(ref mut c) = r.constraints[0] { + if let ObjectSetValue::Reference(ref mut osvr) = c.object_set.values[0] + { + object_sets + .iter() + .find(|s| s.0 == *osvr) + .and_then(|(_, os)| { + let mut os = os.clone(); + os.values.push(c.object_set.values[0].clone()); + c.object_set = os; + Some(()) + }); + } + } + } + }); + } + } + }); + merged_tlds +} + +pub fn generate_typealias( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::ElsewhereDeclaredType(dec) = &tld.ty { + Ok(typealias_template( + &options, + &format_comments(&tld.comments)?, + &tld.name, + &dec.identifier, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected type alias top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_integer_value(tld: ToplevelValueDefinition) -> Result { + if let ASN1Value::LinkedIntValue { integer_type, .. } = tld.value { + let formatted_value = value_to_tokens(&tld.value, None)?; + let formatted_name = to_ros_const_case(&tld.name); + let ty = integer_type.to_str(); + if tld.associated_type == INTEGER { + Ok(lazy_static_value_template( + &format_comments(&tld.comments)?, + &formatted_name, + "int64", + &formatted_value, + )) + } else if integer_type.is_unbounded() { + Ok(lazy_static_value_template( + &format_comments(&tld.comments)?, + &formatted_name, + ty, + &formatted_value, + )) + } else { + Ok(integer_value_template( + &format_comments(&tld.comments)?, + &formatted_name, + ty, + &formatted_value, + )) + } + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Value(tld)), + "Expected INTEGER value top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_integer( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::Integer(_) = tld.ty { + Ok(integer_template( + &options, + &format_comments(&tld.comments)?, + &tld.name, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected INTEGER top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_bit_string( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::BitString(_) = tld.ty { + Ok(bit_string_template( + &options, + &format_comments(&tld.comments)?, + &tld.name, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected BIT STRING top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_octet_string( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::OctetString(ref _oct_str) = tld.ty { + Ok(octet_string_template( + &options, + &format_comments(&tld.comments)?, + &tld.name, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected OCTET STRING top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_character_string( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::CharacterString(ref char_str) = tld.ty { + Ok(char_string_template( + &options, + &format_comments(&tld.comments)?, + &tld.name, + &string_type(&char_str.ty)?, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected Character String top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_boolean( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::Boolean(_) = tld.ty { + Ok(boolean_template( + options, + &format_comments(&tld.comments)?, + &tld.name, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected BOOLEAN top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +macro_rules! call_template { + ($fn:ident, $tld:ident, $($args:expr),*) => { + Ok($fn( + &format_comments(&$tld.comments)?, + (&$tld.name), + $($args),* + )) + }; +} + +pub fn generate_value(tld: ToplevelValueDefinition) -> Result { + let ty = tld.associated_type.as_str(); + match &tld.value { + ASN1Value::Null if ty == NULL => todo!(), + ASN1Value::Null => todo!(), + ASN1Value::Boolean(b) if ty == BOOLEAN => { + call_template!(primitive_value_template, tld, "bool", &b.to_string()) + } + ASN1Value::Boolean(_) => todo!(), + ASN1Value::LinkedIntValue { .. } => generate_integer_value(tld), + ASN1Value::BitString(_) if ty == BIT_STRING => todo!(), + ASN1Value::OctetString(_) if ty == OCTET_STRING => todo!(), + ASN1Value::Choice { + variant_name, + inner_value, + .. + } => { + if inner_value.is_const_type() { + call_template!( + const_choice_value_template, + tld, + &tld.associated_type, + variant_name, + &value_to_tokens(inner_value, None)? + ) + } else { + call_template!( + choice_value_template, + tld, + &tld.associated_type, + &variant_name, + &value_to_tokens(inner_value, None)? + ) + } + } + ASN1Value::EnumeratedValue { + enumerated, + enumerable, + } => call_template!(enum_value_template, tld, enumerated, enumerable), + ASN1Value::Time(_) if ty == GENERALIZED_TIME => todo!(), + ASN1Value::Time(_) if ty == UTC_TIME => todo!(), + ASN1Value::LinkedStructLikeValue(s) => { + let _members = s + .iter() + .map(|(_, _, val)| value_to_tokens(val.value(), None)) + .collect::, _>>()?; + todo!() + } + ASN1Value::LinkedNestedValue { .. } => todo!(), + ASN1Value::ObjectIdentifier(_) if ty == OBJECT_IDENTIFIER => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == NUMERIC_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == VISIBLE_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == IA5_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == UTF8_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == BMP_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == PRINTABLE_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == GENERAL_STRING => todo!(), + ASN1Value::LinkedArrayLikeValue(_) if ty.contains(SEQUENCE_OF) => todo!(), + ASN1Value::LinkedArrayLikeValue(_) if ty.contains(SET_OF) => todo!(), + ASN1Value::BitString(_) + | ASN1Value::Time(_) + | ASN1Value::LinkedCharStringValue(_, _) + | ASN1Value::ObjectIdentifier(_) + | ASN1Value::LinkedArrayLikeValue(_) + | ASN1Value::ElsewhereDeclaredValue { .. } + | ASN1Value::OctetString(_) => todo!(), + _ => Ok("".to_string()), + } +} + +pub fn generate_any(tld: ToplevelTypeDefinition) -> Result { + Ok(any_template(&format_comments(&tld.comments)?, &tld.name)) +} + +pub fn generate_generalized_time(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::GeneralizedTime(_) = &tld.ty { + Ok(generalized_time_template( + &format_comments(&tld.comments)?, + &tld.name, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected GeneralizedTime top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_utc_time(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::UTCTime(_) = &tld.ty { + Ok(utc_time_template( + &format_comments(&tld.comments)?, + &tld.name, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected UTCTime top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_oid(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::ObjectIdentifier(_oid) = &tld.ty { + Ok(oid_template(&format_comments(&tld.comments)?, &tld.name)) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected OBJECT IDENTIFIER top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_null(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::Null = tld.ty { + Ok(null_template(&format_comments(&tld.comments)?, &tld.name)) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected NULL top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_enumerated( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::Enumerated(_) = tld.ty { + Ok(enumerated_template( + &options, + &format_comments(&tld.comments)?, + &tld.name, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected ENUMERATED top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_choice( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + if let ASN1Type::Choice(ref choice) = tld.ty { + let members = get_choice_members_names(choice); + Ok(choice_template( + &options, + &format_comments(&tld.comments)?, + &tld.name, + &members, + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected CHOICE top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_sequence_or_set( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + match tld.ty { + ASN1Type::Sequence(ref seq) | ASN1Type::Set(ref seq) => { + let members = get_sequence_or_set_members_names(seq); + Ok(sequence_or_set_template( + options, + &format_comments(&tld.comments)?, + &tld.name, + members, + )) + } + _ => Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected SEQUENCE top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )), + } +} + +pub fn generate_sequence_or_set_of( + options: &ConversionOptions, + tld: ToplevelTypeDefinition, +) -> Result { + let (is_set_of, seq_or_set_of) = match &tld.ty { + ASN1Type::SetOf(se_of) => (true, se_of), + ASN1Type::SequenceOf(se_of) => (false, se_of), + _ => { + return Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected SEQUENCE OF top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } + }; + let anonymous_item = match seq_or_set_of.element_type.as_ref() { + ASN1Type::ElsewhereDeclaredType(_) => None, + n => Some(generate( + &ConversionOptions::default(), + ToplevelDefinition::Type(ToplevelTypeDefinition { + parameterization: None, + comments: format!( + " Anonymous {} OF member ", + if is_set_of { "SET" } else { "SEQUENCE" } + ), + name: String::from(INNER_ARRAY_LIKE_PREFIX) + &tld.name, + ty: n.clone(), + tag: None, + index: None, + }), + )?), + } + .unwrap_or_default(); + let member_type = match seq_or_set_of.element_type.as_ref() { + ASN1Type::ElsewhereDeclaredType(d) => d.identifier.clone(), + _ => format!("Anonymous{}", &tld.name), + }; + Ok(sequence_or_set_of_template( + &options, + is_set_of, + &format_comments(&tld.comments)?, + &tld.name, + &anonymous_item, + &member_type, + )) +} diff --git a/utils/codegen/codegen-rust/rgen/src/conversion/mod.rs b/utils/codegen/codegen-rust/rgen/src/conversion/mod.rs new file mode 100644 index 000000000..ee1ff75b5 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/conversion/mod.rs @@ -0,0 +1,73 @@ +use rasn_compiler::prelude::{ir::ASN1Type, *}; + +mod builder; +mod template; +mod utils; + +#[derive(Default)] +pub struct Conversion { + options: ConversionOptions, +} +pub struct ConversionOptions { + main_pdu: String, +} +impl Default for ConversionOptions { + fn default() -> Self { + Self { + main_pdu: "pdu".into(), + } + } +} +impl Conversion { + pub fn set_main_pdu_name(mut self, main_pdu_name: &str) -> Self { + self.options.main_pdu = main_pdu_name.to_owned(); + self + } +} + +use builder::*; + +fn generate( + options: &ConversionOptions, + tld: ToplevelDefinition, +) -> Result { + match tld { + ToplevelDefinition::Type(t) => { + if t.parameterization.is_some() { + return Ok("".into()); + } + match t.ty { + ASN1Type::Null => generate_null(t), + ASN1Type::Boolean(_) => generate_boolean(&options, t), + ASN1Type::Integer(_) => generate_integer(&options, t), + ASN1Type::Enumerated(_) => generate_enumerated(&options, t), + ASN1Type::BitString(_) => generate_bit_string(&options, t), + ASN1Type::CharacterString(_) => generate_character_string(&options, t), + ASN1Type::Sequence(_) | ASN1Type::Set(_) => generate_sequence_or_set(&options, t), + ASN1Type::SequenceOf(_) | ASN1Type::SetOf(_) => { + generate_sequence_or_set_of(&options, t) + } + ASN1Type::ElsewhereDeclaredType(_) => generate_typealias(&options, t), + ASN1Type::Choice(_) => generate_choice(&options, t), + ASN1Type::OctetString(_) => generate_octet_string(&options, t), + ASN1Type::Time(_) => unimplemented!("TIME types are currently unsupported!"), + ASN1Type::Real(_) => Err(GeneratorError { + kind: GeneratorErrorType::NotYetInplemented, + details: "Real types are currently unsupported!".into(), + top_level_declaration: None, + }), + ASN1Type::ObjectIdentifier(_) => generate_oid(t), + ASN1Type::InformationObjectFieldReference(_) + | ASN1Type::EmbeddedPdv + | ASN1Type::External => generate_any(t), + ASN1Type::GeneralizedTime(_) => generate_generalized_time(t), + ASN1Type::UTCTime(_) => generate_utc_time(t), + ASN1Type::ChoiceSelectionType(_) => unreachable!(), + } + } + ToplevelDefinition::Value(v) => generate_value(v), + ToplevelDefinition::Information(i) => match i.value { + _ => Ok("".into()), + }, + } +} diff --git a/utils/codegen/codegen-rust/rgen/src/conversion/template.rs b/utils/codegen/codegen-rust/rgen/src/conversion/template.rs new file mode 100644 index 000000000..fa1f23922 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/conversion/template.rs @@ -0,0 +1,690 @@ +use crate::common::{to_ros_const_case, to_ros_snake_case, to_ros_title_case, to_c_title_case}; +use crate::conversion::utils::{InnerTypes, NameType, NamedSeqMember}; +use crate::conversion::ConversionOptions; + +use std::collections::{HashMap, HashSet}; + +const CONVERSION_TEMPLATE: &str = +r#"/** ============================================================================ +MIT License + +Copyright (c) 2023 Institute for Automotive Engineering (ika), RWTH Aachen University +Copyright (c) 2024 Instituto de Telecomunicações, Universidade de Aveiro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +============================================================================= */ + +//// {asn1_type} {name} + +#pragma once +{extra-includes} +#include +{c_includes} +#ifdef ROS1 +{ros1_includes} +namespace {pdu}_msgs = etsi_its_{pdu}_msgs; +#else +{ros2_includes} +namespace {pdu}_msgs = etsi_its_{pdu}_msgs::msg; +#endif + + +namespace etsi_its_{pdu}_conversion { + +void toRos_{type}(const {c_type}_t& in, {pdu}_msgs::{ros_type}& out) { + {to_ros_members} +} + +void toStruct_{type}(const {pdu}_msgs::{ros_type}& in, {c_type}_t& out) { + memset(&out, 0, sizeof({c_type}_t)); + + {to_c_members} +} + +}"#; + +pub fn conversion_template( + _comments: &str, + pdu: &str, + includes: &Vec, + extra_includes: &Vec<&str>, + name: &str, + asn1_type: &str, + to_ros_members: &str, + to_c_members: &str, +) -> String { + let mut c_includes_lines = includes + .iter() + .map(|member| { + if !member.is_primitive { + format!( + "#include ", + pdu = pdu, + dep = member.ty + ) + } else { + format!( + "#include ", + pdu = pdu, + dep = member.ty + ) + "\n" + + &format!( + "#include ", + dep = member.ty + ) + } + }) + .collect::>() + .into_iter() + .collect::>(); + c_includes_lines.sort(); + let c_includes = c_includes_lines.join("\n"); + let ros1_includes = format!( + "#include ", + pdu = pdu, + ros_fn = to_ros_title_case(name) + ); + let ros2_includes = format!( + "#include ", + pdu = pdu, + ros_fn = to_ros_snake_case(name) + ); + + let extra_includes = extra_includes + .iter() + .fold( + String::from(if extra_includes.len() > 0 { "\n" } else { "" }), + |acc, inc| acc + &format!("#include <{inc}>\n") + ); + + CONVERSION_TEMPLATE + .replace("{extra-includes}", &extra_includes) + .replace("{c_includes}", &c_includes) + .replace("{ros1_includes}", &ros1_includes) + .replace("{ros2_includes}", &ros2_includes) + .replace("{asn1_type}", asn1_type) + .replace("{name}", &to_ros_title_case(name)) + .replace("{c_type}", &to_c_title_case(name)) + .replace("{c_filename}", name) + .replace("{type}", &to_ros_title_case(name)) + .replace("{ros_type}", &to_ros_title_case(name)) + .replace("{pdu}", pdu) + .replace("{to_ros_members}", &to_ros_members) + .replace("{to_c_members}", &to_c_members) +} + +pub fn typealias_template( + options: &ConversionOptions, + comments: &str, + name: &str, + alias: &str, +) -> String { + conversion_template( + comments, + &options.main_pdu, + &vec![NameType { + name: name.to_string(), + ty: alias.to_string(), + is_primitive: false, + inner_types: None, + }], + &vec![], + name, + "TYPEALIAS", + &format!("toRos_{alias}(in, out.value);"), + &format!("toStruct_{alias}(in.value, out);"), + ) +} + +pub fn integer_value_template(_comments: &str, _name: &str, _vtype: &str, _value: &str) -> String { + todo!() +} + +pub fn lazy_static_value_template( + _comments: &str, + _name: &str, + _vtype: &str, + _value: &str, +) -> String { + todo!() +} + +pub fn integer_template(options: &ConversionOptions, comments: &str, name: &str) -> String { + conversion_template( + comments, + &options.main_pdu, + &vec![NameType { + name: name.to_string(), + ty: "INTEGER".to_string(), + is_primitive: true, + inner_types: None, + }], + &vec![], + name, + "INTEGER", + "etsi_its_primitives_conversion::toRos_INTEGER(in, out.value);", + "etsi_its_primitives_conversion::toStruct_INTEGER(in.value, out);", + ) +} + +pub fn generalized_time_template(_comments: &str, _name: &str) -> String { + todo!(); +} + +pub fn utc_time_template(_comments: &str, _name: &str) -> String { + todo!(); +} + +pub fn bit_string_template(options: &ConversionOptions, comments: &str, name: &str) -> String { + conversion_template( + comments, + &options.main_pdu, + &vec![NameType { + name: name.to_string(), + ty: "BIT_STRING".to_string(), + is_primitive: true, + inner_types: None, + }], + &vec![], + name, + "BIT-STRING", + "etsi_its_primitives_conversion::toRos_BIT_STRING(in, out.value);\n \ + out.bits_unused = in.bits_unused;", + "etsi_its_primitives_conversion::toStruct_BIT_STRING(in.value, out);\n \ + out.bits_unused = in.bits_unused;", + ) +} + +pub fn octet_string_template(options: &ConversionOptions, comments: &str, name: &str) -> String { + conversion_template( + comments, + &options.main_pdu, + &vec![NameType { + name: name.to_string(), + ty: "OCTET_STRING".to_string(), + is_primitive: true, + inner_types: None, + }], + &vec![], + name, + "OCTET-STRING", + "etsi_its_primitives_conversion::toRos_OCTET_STRING(in, out.value);", + "etsi_its_primitives_conversion::toStruct_OCTET_STRING(in.value, out);", + ) +} + +pub fn char_string_template( + options: &ConversionOptions, + comments: &str, + name: &str, + string_type: &str, +) -> String { + conversion_template( + comments, + &options.main_pdu, + &vec![NameType { + name: name.to_string(), + ty: string_type.to_string(), + is_primitive: true, + inner_types: None, + }], + &vec![], + name, + string_type, + &format!("etsi_its_primitives_conversion::toRos_{string_type}(in, out.value);"), + &format!("etsi_its_primitives_conversion::toStruct_{string_type}(in.value, out);"), + ) +} + +pub fn boolean_template(options: &ConversionOptions, comments: &str, name: &str) -> String { + conversion_template( + comments, + &options.main_pdu, + &vec![NameType { + name: name.to_string(), + ty: "BOOLEAN".to_string(), + is_primitive: true, + inner_types: None, + }], + &vec![], + name, + "BOOLEAN", + "etsi_its_primitives_conversion::toRos_BOOLEAN(in, out.value);", + "etsi_its_primitives_conversion::toStruct_BOOLEAN(in.value, out);", + ) +} + +pub fn primitive_value_template( + _comments: &str, + _name: &str, + _type_name: &str, + _assignment: &str, +) -> String { + todo!(); +} + +pub fn enum_value_template( + _comments: &str, + _name: &str, + _enumerated: &str, + _enumeral: &str, +) -> String { + todo!(); +} + +pub fn null_template(_comments: &str, _name: &str) -> String { + todo!(); +} + +pub fn any_template(_comments: &str, _name: &str) -> String { + todo!(); +} + +pub fn oid_template(_comments: &str, _name: &str) -> String { + todo!(); +} + +pub fn enumerated_template(options: &ConversionOptions, comments: &str, name: &str) -> String { + conversion_template( + comments, + &options.main_pdu, + &vec![], + &vec![], + name, + "ENUMERATED", + "out.value = in;", + "out = in.value;", + ) +} + +pub fn _sequence_or_set_value_template( + _comments: &str, + _name: &str, + _vtype: &str, + _members: &str, +) -> String { + todo!() +} + +#[allow(clippy::too_many_arguments)] +pub fn sequence_or_set_template( + options: &ConversionOptions, + comments: &str, + name: &str, + members: Vec, +) -> String { + let links: HashMap = members + .iter() + .filter_map(|m| { + if let Some(inner) = &m.name_type.inner_types { + match inner { + InnerTypes::Choice(c) => Some(( + c.linked_with.clone(), + members + .iter() + .find(|m| m.name_type.name == c.linked_with) + .unwrap(), + )), + } + } else { + None + } + }) + .collect(); + + // C -> ROS + let to_ros_inner_members = |member: &NamedSeqMember| -> String { + if let Some(inner) = &member.name_type.inner_types { + let cases = match inner { + InnerTypes::Choice(c) => { + c.options.iter().map(|im| { + format!(" case {name}__{c_field_name}_PR_{ty}:\n \ + toRos_{ty}(in.{c_field_name}.choice.{c_member}, out.{r_field_name}.{r_member});\n \ + out.{r_field_name}.choice.value = {pdu}_msgs::{linked_with}::{r_const_member};\n \ + break;", + pdu = &options.main_pdu, + linked_with = links.get(&c.linked_with).unwrap().name_type.ty, + c_field_name = member.name_type.name, + r_field_name = &to_ros_snake_case(&member.name_type.name), + ty = im.ty, + c_member = to_ros_title_case(&im.name), + r_member = to_ros_snake_case(&im.name), + r_const_member = to_ros_const_case(&im.name)) + }).collect::>().join("\n") + }, + }; + format!( + "switch (in.{field_name}.present) {{\n\ + {cases}\n \ + }}", + field_name = member.name_type.name, + cases = cases + ) + } else { + "".to_string() + } + }; + + let to_ros_conversion_call = |member: &NamedSeqMember| -> String { + if !member.name_type.is_primitive { + if member.name_type.inner_types.is_some() { + // TODO optional type with inner_types + to_ros_inner_members(&member) + } else { + format!( + "toRos_{ty}({deref}in.{c_member}, out.{r_member});", + ty = member.name_type.ty, + deref = if member.is_optional { "*" } else { "" }, + c_member = member.name_type.name, + r_member = to_ros_snake_case(&member.name_type.name) + ) + } + } else { + format!( + "etsi_its_primitives_conversion::toRos_{ty}({deref}in.{c_member}, out.{r_member});", + ty = member.name_type.ty, + deref = if member.is_optional { "*" } else { "" }, + c_member = member.name_type.name, + r_member = to_ros_snake_case(&member.name_type.name) + ) + } + }; + let to_ros_fmt_member = |member: &NamedSeqMember| -> String { + if member.is_optional { + format!( + "if (in.{c_member}) {{\n \ + {conversion}\n \ + {present}\ + }}", + c_member = member.name_type.name, + conversion = to_ros_conversion_call(&member), + present = if !member.has_default { + format!( + " out.{r_member}_is_present = true;\n ", + r_member = to_ros_snake_case(&member.name_type.name) + ) + } else { + "".to_string() + } + ) + } else { + to_ros_conversion_call(&member) + } + }; + let to_ros_members = members + .iter() + .map(|member| to_ros_fmt_member(&member)) + .collect::>() + .join("\n "); + + // ROS -> C + let to_c_inner_members = |member: &NamedSeqMember| -> String { + if let Some(inner) = &member.name_type.inner_types { + let cases = match inner { + InnerTypes::Choice(c) => { + c.options.iter().map(|im| { + format!(" case {pdu}_msgs::{linked_with}::{r_const_member}:\n \ + toStruct_{ty}(in.{r_field_name}.{r_member}, out.{c_field_name}.choice.{c_member});\n \ + out.{c_field_name}.present = {name}__{c_field_name}_PR::{name}__{c_field_name}_PR_{c_member};\n \ + break;", + pdu = &options.main_pdu, + linked_with = links.get(&c.linked_with).unwrap().name_type.ty, + c_field_name = member.name_type.name, + r_field_name = &to_ros_snake_case(&member.name_type.name), + ty = im.ty, + c_member = to_ros_title_case(&im.name), + r_member = to_ros_snake_case(&im.name), + r_const_member = to_ros_const_case(&im.name)) + }).collect::>().join("\n") + }, + }; + format!( + "switch (in.{field_name}.choice.value) {{\n\ + {cases}\n \ + }}", + field_name = to_ros_snake_case(&member.name_type.name), + cases = cases + ) + } else { + "".to_string() + } + }; + + let to_c_conversion_call = |member: &NamedSeqMember| -> String { + if !member.name_type.is_primitive { + if member.name_type.inner_types.is_some() { + // TODO optional type with inner_types + to_c_inner_members(&member) + } else { + format!( + "toStruct_{ty}(in.{r_member}, {deref}out.{c_member});", + ty = member.name_type.ty, + deref = if member.is_optional { "*" } else { "" }, + c_member = member.name_type.name, + r_member = to_ros_snake_case(&member.name_type.name) + ) + } + } else { + format!("etsi_its_primitives_conversion::toStruct_{ty}(in.{r_member}, {deref}out.{c_member});", + ty = member.name_type.ty, + deref = if member.is_optional { "*" } else { "" }, + c_member = member.name_type.name, + r_member = to_ros_snake_case(&member.name_type.name) + ) + } + }; + let to_c_fmt_member = |member: &NamedSeqMember| -> String { + if member.is_optional { + if !member.has_default { + format!( + "if (in.{r_member}_is_present) {{\n \ + out.{c_member} = ({ty}_t*) calloc(1, sizeof({ty}_t));\n \ + {conversion}\n \ + }}", + ty = member.name_type.ty, + c_member = member.name_type.name, + conversion = to_c_conversion_call(&member), + r_member = to_ros_snake_case(&member.name_type.name) + ) + } else { + format!( + "out.{c_member} = ({ty}_t*) calloc(1, sizeof({ty}_t));\n \ + {conversion}", + ty = member.name_type.ty, + c_member = member.name_type.name, + conversion = to_c_conversion_call(&member) + ) + } + } else { + to_c_conversion_call(&member) + } + }; + let to_c_members = members + .iter() + .map(|member| to_c_fmt_member(&member)) + .collect::>() + .join("\n "); + + let includes = &members + .iter() + .flat_map(|m| { + if let Some(inners) = &m.name_type.inner_types { + match inners { + InnerTypes::Choice(c) => c.options.clone(), + } + } else { + vec![m.name_type.clone()] + } + }) + .collect(); + + conversion_template( + comments, + &options.main_pdu, + &includes, + &vec![], + name, + "SEQUENCE", + &to_ros_members, + &to_c_members, + ) +} + +pub fn sequence_or_set_of_template( + options: &ConversionOptions, + _is_set_of: bool, + comments: &str, + name: &str, + _anonymous_item: &str, + member_type: &str, +) -> String { + let to_ros_loop = format!( + "for (int i = 0; i < in.list.count; ++i) {{\n \ + {pdu}_msgs::{ty} el;\n \ + toRos_{ty}(*(in.list.array[i]), el);\n \ + out.array.push_back(el);\n \ + }}", + pdu = &options.main_pdu, + ty = member_type + ); + + let to_c_loop = + format!("for (int i = 0; i < in.array.size(); ++i) {{\n \ + {ty}_t* el = ({ty}_t*) calloc(1, sizeof({ty}_t));\n \ + toStruct_{ty}(in.array[i], *el);\n \ + if (asn_sequence_add(&out, el)) throw std::invalid_argument(\"Failed to add to A_SEQUENCE_OF\");\n \ + }}", + ty = member_type); + + conversion_template( + comments, + &options.main_pdu, + &vec![ + NameType { + name: name.to_string(), + ty: name.to_string(), + is_primitive: false, + inner_types: None, + }, + NameType { + name: member_type.to_string(), + ty: member_type.to_string(), + is_primitive: false, + inner_types: None, + }, + ], + &vec!["stdexcept"], + name, + "SEQUENCE-OF", + &to_ros_loop, + &to_c_loop, + ) +} + +pub fn choice_value_template( + _comments: &str, + _name: &str, + _type_id: &str, + _choice_name: &str, + _inner_decl: &str, +) -> String { + todo!(); +} + +pub fn const_choice_value_template( + _comments: &str, + _name: &str, + _type_id: &str, + _choice_name: &str, + _inner_decl: &str, +) -> String { + todo!(); +} + +pub fn choice_template( + options: &ConversionOptions, + comments: &str, + name: &str, + members: &Vec, +) -> String { + let to_ros_members = format!("switch (in.present) {{\n") + + &members + .iter() + .map(|member| { + if !member.is_primitive { + format!( + " case {parent}_PR_{c_member}:\n \ + toRos_{ty}(in.choice.{c_member}, out.{r_member});\n \ + out.choice = {pdu}_msgs::{parent}::CHOICE_{r_ch_member};", + parent = &name, + ty = member.ty, + pdu = &options.main_pdu, + c_member = member.name, + r_member = to_ros_snake_case(&member.name), + r_ch_member = to_ros_const_case(&member.name) + ) + } else { + format!( + "etsi_its_primitives_conversion::toRos_{ty}(in, out.value);", + ty = member.ty, + ) + } + }) + .collect::>() + .join("\n break;\n") + + "\n break;\n default: break;\n }"; + + let to_c_members = format!("switch (in.choice) {{\n") + + &members + .iter() + .map(|member| { + if !member.is_primitive { + format!( + " case {pdu}_msgs::{parent}::CHOICE_{r_ch_member}:\n \ + toStruct_{ty}(in.{r_member}, out.choice.{c_member});\n \ + out.present = {parent}_PR::{parent}_PR_{c_member};", + parent = &name, + ty = member.ty, + pdu = &options.main_pdu, + c_member = member.name, + r_member = to_ros_snake_case(&member.name), + r_ch_member = to_ros_const_case(&member.name) + ) + } else { + format!( + "etsi_its_primitives_conversion::toStruct_{ty}(in, out.value);", + ty = member.ty, + ) + } + }) + .collect::>() + .join("\n break;\n") + + "\n break;\n default: break;\n }"; + + conversion_template( + comments, + &options.main_pdu, + &members, + &vec![], + name, + "CHOICE", + &to_ros_members, + &to_c_members, + ) +} diff --git a/utils/codegen/codegen-rust/rgen/src/conversion/utils.rs b/utils/codegen/codegen-rust/rgen/src/conversion/utils.rs new file mode 100644 index 000000000..65d36720f --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/conversion/utils.rs @@ -0,0 +1,368 @@ +use rasn_compiler::intermediate::{ + constraints::Constraint, + information_object::InformationObjectField, + types::{Choice, SequenceOrSet}, + ASN1Type, ASN1Value, CharacterStringType, IntegerType, +}; +use rasn_compiler::prelude::{ir::*, *}; + +use crate::common::{to_ros_title_case, IntegerTypeExt}; + +macro_rules! error { + ($kind:ident, $($arg:tt)*) => { + GeneratorError { + details: format!($($arg)*), + top_level_declaration: None, + kind: GeneratorErrorType::$kind, + } + }; +} + +pub(crate) use error; + +pub fn format_comments(comments: &str) -> Result { + if comments.is_empty() { + Ok("".into()) + } else { + let joined = String::from("// ") + &comments.replace('\n', "\n//") + "\n"; + Ok(joined) + } +} + +pub fn inner_name(name: &String, parent_name: &String) -> String { + format!("{}{}", parent_name, name) +} + +#[derive(Clone, Debug)] +pub struct NameType { + pub name: String, + pub ty: String, + pub is_primitive: bool, + pub inner_types: Option, +} + +#[derive(Clone, Debug)] +pub enum InnerTypes { + Choice(InnerTypesChoice), +} + +#[derive(Clone, Debug)] +pub struct InnerTypesChoice { + pub linked_with: String, + pub options: Vec, +} + +#[derive(Clone, Debug)] +pub struct NamedSeqMember { + pub name_type: NameType, + pub is_optional: bool, + pub has_default: bool, +} + +fn get_inner_types_names(ty: &ASN1Type) -> Option { + match ty { + ASN1Type::InformationObjectFieldReference(r) => { + if let Constraint::TableConstraint(ref tc) = r.constraints[0] { + let object_set = &tc.object_set; + let mut names = vec![]; + for value in &object_set.values { + if let ObjectSetValue::Inline(ref i) = value { + match i { + InformationObjectFields::DefaultSyntax(ds) => { + let mut name = "".to_string(); + let mut ty: ASN1Type = ASN1Type::Null; + ds.iter().for_each(|f| match f { + InformationObjectField::TypeField(f) => ty = f.ty.clone(), + InformationObjectField::FixedValueField(f) => { + name = value_to_tokens(&f.value, None).unwrap() + } + _ => todo!(), + }); + names.push(NameType { + name: name.clone(), + ty: constraints_and_type_name(&ty, &name, &"".to_string()) + .unwrap() + .1, + is_primitive: ty.is_builtin_type(), + inner_types: None, + }); + } + _ => todo!(), + } + } + } + let linked_with = tc + .linked_fields + .get(0) + .map(|f| f.field_name.clone()) + .unwrap_or_default(); + Some(InnerTypes::Choice(InnerTypesChoice { + linked_with, + options: names, + })) + } else { + unreachable!() + } + } + _ => None, + } +} + +pub fn get_sequence_or_set_members_names(sequence_or_set: &SequenceOrSet) -> Vec { + sequence_or_set + .members + .iter() + .map(|member| NamedSeqMember { + name_type: NameType { + name: member.name.clone(), + ty: constraints_and_type_name(&member.ty, &member.name, &"".to_string()) + .unwrap() + .1, + is_primitive: member.ty.is_builtin_type(), + inner_types: get_inner_types_names(&member.ty), + }, + is_optional: member.is_optional, + has_default: member.default_value.is_some(), + }) + .collect::>() +} + +pub fn get_choice_members_names(choice: &Choice) -> Vec { + choice + .options + .iter() + .map(|member| + NameType { + name: member.name.clone(), + ty: constraints_and_type_name(&member.ty, &member.name, &"".to_string()) + .unwrap() + .1, + is_primitive: member.ty.is_builtin_type(), + inner_types: None, + } + ).collect::>() +} + +fn constraints_and_type_name( + ty: &ASN1Type, + name: &String, + parent_name: &String, +) -> Result<(Vec, String), GeneratorError> { + Ok(match ty { + ASN1Type::Null => (vec![], "byte".into()), + ASN1Type::Boolean(b) => (b.constraints.clone(), "BOOLEAN".into()), + ASN1Type::Integer(i) => (i.constraints.clone(), "INTEGER".into()), + ASN1Type::Real(_) => (vec![], "float64".into()), + ASN1Type::ObjectIdentifier(_o) => todo!(), + ASN1Type::BitString(_b) => todo!(), + ASN1Type::OctetString(o) => (o.constraints.clone(), "uint8[]".into()), + ASN1Type::GeneralizedTime(_o) => todo!(), + ASN1Type::UTCTime(_o) => todo!(), + ASN1Type::Time(_t) => todo!(), + ASN1Type::CharacterString(c) => ( + c.constraints.clone(), + string_type(&c.ty).unwrap_or("STRING".into()), + ), + ASN1Type::Enumerated(_) + | ASN1Type::Choice(_) + | ASN1Type::Sequence(_) + | ASN1Type::SetOf(_) + | ASN1Type::Set(_) => (vec![], inner_name(name, parent_name)), + ASN1Type::SequenceOf(s) => { + let (_, inner_type) = constraints_and_type_name(&s.element_type, name, parent_name)?; + (s.constraints().clone(), format!("{inner_type}[]").into()) + } + ASN1Type::ElsewhereDeclaredType(e) => { + (e.constraints.clone(), to_ros_title_case(&e.identifier)) + } + ASN1Type::InformationObjectFieldReference(_) + | ASN1Type::EmbeddedPdv + | ASN1Type::External => { + let tx = &ty.constraints().unwrap()[0]; + let rname = if let Constraint::TableConstraint(ref tc) = tx { + tc.object_set + .values + .iter() + .find_map(|v| match v { + ObjectSetValue::Reference(ref r) => Some(r.clone()), + _ => None, + }) + .unwrap_or_default() + } else { + "".to_string() + }; + (vec![], rname) + } + ASN1Type::ChoiceSelectionType(_) => unreachable!(), + }) +} + +pub fn string_type(c_type: &CharacterStringType) -> Result { + match c_type { + CharacterStringType::NumericString => Ok("NumericString".into()), + CharacterStringType::VisibleString => Ok("VisibleString".into()), + CharacterStringType::IA5String => Ok("IA5String".into()), + CharacterStringType::TeletexString => Ok("TeletexString".into()), + CharacterStringType::VideotexString => Ok("VideotexString".into()), + CharacterStringType::GraphicString => Ok("GraphicString".into()), + CharacterStringType::GeneralString => Ok("GeneralString".into()), + CharacterStringType::UniversalString => Ok("UniversalString".into()), + CharacterStringType::UTF8String => Ok("UTF8String".into()), + CharacterStringType::BMPString => Ok("BMPString".into()), + CharacterStringType::PrintableString => Ok("PrintableString".into()), + } +} + +pub fn value_to_tokens( + value: &ASN1Value, + type_name: Option<&String>, +) -> Result { + match value { + ASN1Value::All => Err(error!( + NotYetInplemented, + "All values are currently unsupported!" + )), + ASN1Value::Null => todo!(), + ASN1Value::Choice { inner_value, .. } => { + if let Some(_ty_n) = type_name { + todo!() + } else { + Err(error!( + Unidentified, + "A type name is needed to stringify choice value {:?}", inner_value + )) + } + } + ASN1Value::OctetString(o) => { + let _bytes = o.iter().map(|byte| *byte); + todo!() + } + ASN1Value::SequenceOrSet(_) => Err(error!( + Unidentified, + "Unexpectedly encountered unlinked struct-like ASN1 value!" + )), + ASN1Value::LinkedStructLikeValue(fields) => { + if let Some(_ty_n) = type_name { + let _tokenized_fields = fields + .iter() + .map(|(_, _, val)| value_to_tokens(val.value(), None)) + .collect::, _>>()?; + todo!() + } else { + Err(error!( + Unidentified, + "A type name is needed to stringify sequence value {:?}", value + )) + } + } + ASN1Value::Boolean(b) => Ok(b.to_string()), + ASN1Value::Integer(i) => Ok(i.to_string()), + ASN1Value::String(s) => Ok(s.to_string()), + ASN1Value::Real(r) => Ok(r.to_string()), + ASN1Value::BitString(b) => { + let _bits = b.iter().map(|bit| bit.to_string()); + todo!() + } + ASN1Value::EnumeratedValue { + enumerated, + enumerable, + } => Ok(format!("{}_{}", enumerated, enumerable)), + ASN1Value::LinkedElsewhereDefinedValue { identifier: e, .. } + | ASN1Value::ElsewhereDeclaredValue { identifier: e, .. } => Ok(e.to_string()), + ASN1Value::ObjectIdentifier(oid) => { + let _arcs = oid + .0 + .iter() + .filter_map(|arc| arc.number.map(|id| id.to_string())); + todo!() + } + ASN1Value::Time(_t) => match type_name { + Some(_time_type) => todo!(), + None => todo!(), + }, + ASN1Value::LinkedArrayLikeValue(seq) => { + let _elems = seq + .iter() + .map(|v| value_to_tokens(v, None)) + .collect::, _>>()?; + todo!() + } + ASN1Value::LinkedNestedValue { + supertypes: _, + value, + } => Ok(value_to_tokens(value, type_name)?), + ASN1Value::LinkedIntValue { + integer_type, + value, + } => { + let val = *value; + match integer_type { + IntegerType::Unbounded => Ok(val.to_string()), + _ => Ok(val.to_string()), + } + } + ASN1Value::LinkedCharStringValue(string_type, value) => { + let _val = value; + match string_type { + CharacterStringType::NumericString => { + todo!() + } + CharacterStringType::VisibleString => { + todo!() + } + CharacterStringType::IA5String => { + todo!() + } + CharacterStringType::UTF8String => todo!(), + CharacterStringType::BMPString => { + todo!() + } + CharacterStringType::PrintableString => { + todo!() + } + CharacterStringType::GeneralString => { + todo!() + } + CharacterStringType::VideotexString + | CharacterStringType::GraphicString + | CharacterStringType::UniversalString + | CharacterStringType::TeletexString => Err(GeneratorError::new( + None, + &format!("{:?} values are currently unsupported!", string_type), + GeneratorErrorType::NotYetInplemented, + )), + } + } + } +} + +pub fn _format_sequence_or_set_of_item_type( + type_name: String, + first_item: Option<&ASN1Value>, +) -> String { + match type_name { + name if name == NULL => todo!(), + name if name == BOOLEAN => "bool".into(), + name if name == INTEGER => { + match first_item { + Some(ASN1Value::LinkedIntValue { integer_type, .. }) => { + integer_type.to_str().into() + } + _ => "int64".into(), // best effort + } + } + name if name == BIT_STRING => "BitString".into(), + name if name == OCTET_STRING => "OctetString".into(), + name if name == GENERALIZED_TIME => "GeneralizedTime".into(), + name if name == UTC_TIME => "UtcTime".into(), + name if name == OBJECT_IDENTIFIER => "ObjectIdentifier".into(), + name if name == NUMERIC_STRING => "NumericString".into(), + name if name == VISIBLE_STRING => "VisibleString".into(), + name if name == IA5_STRING => "IA5String".into(), + name if name == UTF8_STRING => "UTF8String".into(), + name if name == BMP_STRING => "BMPString".into(), + name if name == PRINTABLE_STRING => "PrintableString".into(), + name if name == GENERAL_STRING => "GeneralString".into(), + name => name, + } +} diff --git a/utils/codegen/codegen-rust/rgen/src/lib.rs b/utils/codegen/codegen-rust/rgen/src/lib.rs new file mode 100644 index 000000000..09dabbaeb --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/lib.rs @@ -0,0 +1,3 @@ +mod common; +pub mod conversion; +pub mod msgs; diff --git a/utils/codegen/codegen-rust/rgen/src/msgs/bin.rs b/utils/codegen/codegen-rust/rgen/src/msgs/bin.rs new file mode 100644 index 000000000..7eb64b1ed --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/msgs/bin.rs @@ -0,0 +1,41 @@ +use clap::Parser; +use regex::Regex; + +use rasn_compiler::prelude::*; +use ros_backend::msgs::Msgs; + +#[derive(Parser, Debug)] +struct Cli { + #[clap(short, long)] + /// Output directory + out: std::path::PathBuf, + /// ASN.1 files to compile + paths: Vec, +} + +fn main() { + let args = Cli::parse(); + + // Compile ROS messages + let compiler_res = Compiler::new() + .with_backend(Msgs) + .add_asn_sources_by_path(args.paths.iter()) + .compile_to_string(); + let generated = &compiler_res.unwrap().generated; + + // Split generated code into individual messages + let re_name = Regex::new(r"##\s([\w-]+)\s(\w+)\b").unwrap(); + let re_def = Regex::new(r"\n((.|\n)*?)\n").unwrap(); + generated.split_inclusive("").for_each(|s| { + if let Some(def_caps) = re_def.captures(s) { + let definition = def_caps.get(1).unwrap().as_str(); + let name = if let Some(name_caps) = re_name.captures(definition) { + name_caps.get(2).unwrap().as_str() + } else { + "unknown" + }; + let path = args.out.join(format!("{}.msg", name)); + std::fs::write(path, definition).unwrap(); + } + }); +} diff --git a/utils/codegen/codegen-rust/rgen/src/msgs/builder.rs b/utils/codegen/codegen-rust/rgen/src/msgs/builder.rs new file mode 100644 index 000000000..93b4ae218 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/msgs/builder.rs @@ -0,0 +1,642 @@ +use std::{collections::BTreeMap, error::Error}; + +use rasn_compiler::prelude::ir::*; +use rasn_compiler::prelude::*; + +use crate::common::*; +use crate::msgs::{generate, Msgs}; +use crate::msgs::{template::*, utils::*}; + +pub(crate) const INNER_ARRAY_LIKE_PREFIX: &str = "Anonymous_"; + +impl Backend for Msgs { + fn generate_module( + &self, + tlds: Vec, + ) -> Result { + let tlds = merge_tlds(tlds); + let (pdus, warnings): (Vec, Vec>) = + tlds.into_iter() + .fold((vec![], vec![]), |mut acc, tld| match generate(tld) { + Ok(s) => { + s.len().gt(&0).then(|| { + acc.0.push(format!( + "\n\ + {s}\n\ + " + )) + }); + acc + } + Err(e) => { + acc.1.push(Box::new(e)); + acc + } + }); + Ok(GeneratedModule { + generated: Some(format!("{}", pdus.join("\n\n"))), + warnings, + }) + } +} + +pub fn merge_tlds(tlds: Vec) -> Vec { + let mut merged_tlds = Vec::::with_capacity(tlds.len()); + let mut merge_to = Vec::<(&ToplevelDefinition, &String)>::new(); + tlds.iter().for_each(|tld| { + if let ToplevelDefinition::Value(v) = &tld { + if let ASN1Value::LinkedIntValue { .. } = &v.value { + merge_to.push((&tld, &v.associated_type)); + } else { + merged_tlds.push(tld.clone()); + } + } else { + merged_tlds.push(tld.clone()); + } + }); + merge_to.iter().for_each(|(tld, ty)| { + for t in &mut merged_tlds { + if let ToplevelDefinition::Type(tt) = t { + if tt.name == **ty { + // Add value to type's distinguished values + if let ASN1Type::Integer(ref mut int) = tt.ty { + let value = match &tld { + ToplevelDefinition::Value(v) => { + if let ASN1Value::LinkedIntValue { + integer_type: _, + value, + } = &v.value + { + value + } else { + unreachable!() + } + } + _ => unreachable!(), + }; + match int.distinguished_values { + Some(ref mut dv) => dv.push(DistinguishedValue { + name: tld.name().clone(), + value: *value, + }), + None => { + int.distinguished_values = Some(vec![DistinguishedValue { + name: tld.name().clone(), + value: *value, + }]); + } + } + } + break; + } + } + } + }); + merged_tlds +} + +pub fn generate_typealias(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::ElsewhereDeclaredType(dec) = &tld.ty { + Ok(typealias_template( + &format_comments(&tld.comments)?, + &tld.name, + &dec.identifier, + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected type alias top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_integer_value(tld: ToplevelValueDefinition) -> Result { + if let ASN1Value::LinkedIntValue { integer_type, .. } = tld.value { + let formatted_value = value_to_tokens(&tld.value, None)?; + let formatted_name = to_ros_const_case(&tld.name); + let ty = integer_type.to_str(); + if tld.associated_type == INTEGER { + Ok(lazy_static_value_template( + &format_comments(&tld.comments)?, + &formatted_name, + "int64", + &formatted_value, + )) + } else if integer_type.is_unbounded() { + Ok(lazy_static_value_template( + &format_comments(&tld.comments)?, + &formatted_name, + ty, + &formatted_value, + )) + } else { + Ok(integer_value_template( + &format_comments(&tld.comments)?, + &formatted_name, + ty, + &formatted_value, + )) + } + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Value(tld)), + "Expected INTEGER value top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_integer(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::Integer(ref int) = tld.ty { + Ok(integer_template( + &format_comments(&tld.comments)?, + &to_ros_title_case(&tld.name), + &format_constraints(true, &int.constraints)? + .replace("{prefix}", ""), + int.int_type().to_str(), + &format_distinguished_values(&int.distinguished_values), + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected INTEGER top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_bit_string(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::BitString(ref bitstr) = tld.ty { + Ok(bit_string_template( + &format_comments(&tld.comments)?, + &tld.name, + &format_constraints(true, &bitstr.constraints)? + .replace("{prefix}", "") + .replace("SIZE =", "SIZE_BITS ="), + &format_distinguished_values(&bitstr.distinguished_values), + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected BIT STRING top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_octet_string(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::OctetString(ref oct_str) = tld.ty { + Ok(octet_string_template( + &format_comments(&tld.comments)?, + &tld.name, + &format_constraints(false, &oct_str.constraints)? + .replace("{prefix}", ""), + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected OCTET STRING top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_character_string(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::CharacterString(ref char_str) = tld.ty { + Ok(char_string_template( + &format_comments(&tld.comments)?, + &tld.name, + &string_type(&char_str.ty)?, + &format_constraints(false, &char_str.constraints)? + .replace("{prefix}", ""), + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected Character String top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_boolean(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::Boolean(_) = tld.ty { + Ok(boolean_template( + &format_comments(&tld.comments)?, + &tld.name, + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected BOOLEAN top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +macro_rules! call_template { + ($fn:ident, $tld:ident, $($args:expr),*) => { + Ok($fn( + &format_comments(&$tld.comments)?, + (&$tld.name), + $($args),* + )) + }; +} + +pub fn generate_value(tld: ToplevelValueDefinition) -> Result { + let ty = tld.associated_type.as_str(); + match &tld.value { + ASN1Value::Null if ty == NULL => { + call_template!( + primitive_value_template, + tld, + "quote!(())".into(), + "quote!(())".into() + ) + } + ASN1Value::Null => todo!(), + ASN1Value::Boolean(b) if ty == BOOLEAN => { + call_template!(primitive_value_template, tld, "bool", &b.to_string()) + } + ASN1Value::Boolean(_) => todo!(), + ASN1Value::LinkedIntValue { .. } => generate_integer_value(tld), + ASN1Value::BitString(_) if ty == BIT_STRING => todo!(), + ASN1Value::OctetString(_) if ty == OCTET_STRING => todo!(), + ASN1Value::Choice { + variant_name, + inner_value, + .. + } => { + if inner_value.is_const_type() { + call_template!( + const_choice_value_template, + tld, + &tld.associated_type, + variant_name, + &value_to_tokens(inner_value, None)? + ) + } else { + call_template!( + choice_value_template, + tld, + &tld.associated_type, + &variant_name, + &value_to_tokens(inner_value, None)? + ) + } + } + ASN1Value::EnumeratedValue { + enumerated, + enumerable, + } => call_template!(enum_value_template, tld, enumerated, enumerable), + ASN1Value::Time(_) if ty == GENERALIZED_TIME => todo!(), + ASN1Value::Time(_) if ty == UTC_TIME => todo!(), + ASN1Value::LinkedStructLikeValue(_) => todo!(), + ASN1Value::LinkedNestedValue { .. } => todo!(), + ASN1Value::ObjectIdentifier(_) if ty == OBJECT_IDENTIFIER => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == NUMERIC_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == VISIBLE_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == IA5_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == UTF8_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == BMP_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == PRINTABLE_STRING => todo!(), + ASN1Value::LinkedCharStringValue(_, _) if ty == GENERAL_STRING => todo!(), + ASN1Value::LinkedArrayLikeValue(_) if ty.contains(SEQUENCE_OF) => todo!(), + ASN1Value::LinkedArrayLikeValue(_) if ty.contains(SET_OF) => todo!(), + ASN1Value::BitString(_) + | ASN1Value::Time(_) + | ASN1Value::LinkedCharStringValue(_, _) + | ASN1Value::ObjectIdentifier(_) + | ASN1Value::LinkedArrayLikeValue(_) + | ASN1Value::ElsewhereDeclaredValue { .. } + | ASN1Value::OctetString(_) => todo!(), + _ => Ok("".to_string()), + } +} + +pub fn generate_any(tld: ToplevelTypeDefinition) -> Result { + Ok(any_template( + &format_comments(&tld.comments)?, + &tld.name, + "", + )) +} + +pub fn generate_generalized_time(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::GeneralizedTime(_) = &tld.ty { + Ok(generalized_time_template( + &format_comments(&tld.comments)?, + &tld.name, + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected GeneralizedTime top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_utc_time(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::UTCTime(_) = &tld.ty { + Ok(utc_time_template( + &format_comments(&tld.comments)?, + &tld.name, + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected UTCTime top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_oid(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::ObjectIdentifier(_oid) = &tld.ty { + Ok(oid_template( + &format_comments(&tld.comments)?, + &tld.name, + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected OBJECT IDENTIFIER top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_null(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::Null = tld.ty { + Ok(null_template( + &format_comments(&tld.comments)?, + &tld.name, + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected NULL top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_enumerated(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::Enumerated(ref enumerated) = tld.ty { + Ok(enumerated_template( + &format_comments(&tld.comments)?, + &tld.name, + &format_enum_members(enumerated), + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected ENUMERATED top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_choice(tld: ToplevelTypeDefinition) -> Result { + if let ASN1Type::Choice(ref choice) = tld.ty { + let inner_options = format_nested_choice_options(choice, &tld.name)?; + Ok(choice_template( + &format_comments(&tld.comments)?, + &tld.name, + &format_choice_options(choice, &tld.name)?, + inner_options, + "", + )) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected CHOICE top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} + +pub fn generate_sequence_or_set(tld: ToplevelTypeDefinition) -> Result { + match tld.ty { + ASN1Type::Sequence(ref seq) | ASN1Type::Set(ref seq) => { + let declaration = format_sequence_or_set_members(seq, &tld.name)?; + Ok(sequence_or_set_template( + &format_comments(&tld.comments)?, + &tld.name, + &declaration, + format_nested_sequence_members(seq, &tld.name)?, + "", + &format_default_methods(&seq.members, &tld.name)?, + "", + )) + } + _ => Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected SEQUENCE top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )), + } +} + +pub fn generate_sequence_or_set_of(tld: ToplevelTypeDefinition) -> Result { + let (is_set_of, seq_or_set_of) = match &tld.ty { + ASN1Type::SetOf(se_of) => (true, se_of), + ASN1Type::SequenceOf(se_of) => (false, se_of), + _ => { + return Err(GeneratorError::new( + Some(ToplevelDefinition::Type(tld)), + "Expected SEQUENCE OF top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } + }; + let anonymous_item = match seq_or_set_of.element_type.as_ref() { + ASN1Type::ElsewhereDeclaredType(_) => None, + n => Some(generate(ToplevelDefinition::Type( + ToplevelTypeDefinition { + parameterization: None, + comments: format!( + " Anonymous {} OF member ", + if is_set_of { "SET" } else { "SEQUENCE" } + ), + name: String::from(INNER_ARRAY_LIKE_PREFIX) + &tld.name, + ty: n.clone(), + tag: None, + index: None, + }, + ))?), + } + .unwrap_or_default(); + let member_type = match seq_or_set_of.element_type.as_ref() { + ASN1Type::ElsewhereDeclaredType(d) => d.identifier.clone(), + _ => format!("Anonymous{}", &tld.name), + }; + let constraints = format_constraints(true, &seq_or_set_of.constraints)? + .replace("{prefix}", ""); + Ok(sequence_or_set_of_template( + is_set_of, + &format_comments(&tld.comments)?, + &tld.name, + &anonymous_item, + &member_type, + &constraints, + )) +} + +pub fn generate_information_object_set( + tld: ToplevelInformationDefinition, +) -> Result { + if let ASN1Information::ObjectSet(o) = &tld.value { + let class: &InformationObjectClass = match tld.class { + Some(ClassLink::ByReference(ref c)) => c, + _ => { + return Err(GeneratorError::new( + None, + "Missing class link in Information Object Set", + GeneratorErrorType::MissingClassKey, + )) + } + }; + let mut keys_to_types = o + .values + .iter() + .map(|v| match v { + ObjectSetValue::Reference(r) => Err(GeneratorError::new( + None, + &format!("Could not resolve reference of Information Object Set {r}"), + GeneratorErrorType::MissingClassKey, + )), + ObjectSetValue::Inline(InformationObjectFields::CustomSyntax(_)) => { + Err(GeneratorError::new( + Some(ToplevelDefinition::Information(tld.clone())), + "Unexpectedly encountered unresolved custom syntax!", + GeneratorErrorType::MissingClassKey, + )) + } + ObjectSetValue::Inline(InformationObjectFields::DefaultSyntax(s)) => { + resolve_standard_syntax(class, s) + } + }) + .collect::)>, _>>()?; + let mut choices = BTreeMap::>::new(); + for (key, items) in keys_to_types.drain(..) { + for (index, item) in items { + let id = class + .fields + .get(index) + .map(|f| f.identifier.identifier()) + .ok_or_else(|| GeneratorError { + top_level_declaration: Some(ToplevelDefinition::Information(tld.clone())), + details: "Could not find class field for index.".into(), + kind: GeneratorErrorType::SyntaxMismatch, + })?; + match choices.get_mut(id) { + Some(entry) => entry.push((key.clone(), item)), + None => { + choices.insert(id.clone(), vec![(key.clone(), item)]); + } + } + } + } + + if choices.is_empty() { + for InformationObjectClassField { identifier, .. } in &class.fields { + choices.insert(identifier.identifier().clone(), Vec::new()); + } + } + + let name = &tld.name; + let class_unique_id_type = class + .fields + .iter() + .find_map(|f| (f.is_unique).then(|| f.ty.clone())) + .flatten() + .ok_or_else(|| GeneratorError { + top_level_declaration: None, + details: "Could not determine unique class identifier type.".into(), + kind: GeneratorErrorType::SyntaxMismatch, + })?; + let class_unique_id_type_name = type_to_tokens(&class_unique_id_type)?; + + let mut field_enums = vec![]; + for (_field_name, fields) in choices.iter() { + let field_enum_name = name.clone(); + let mut ids = vec![]; + for (index, (id, ty)) in fields.iter().enumerate() { + let identifier_value = match id { + ASN1Value::LinkedElsewhereDefinedValue { + can_be_const: false, + .. + } => { + let _tokenized_value = + value_to_tokens(id, Some(&class_unique_id_type_name))?; + todo!() + } + ASN1Value::LinkedNestedValue { value, .. } + if matches![ + &**value, + ASN1Value::LinkedElsewhereDefinedValue { + can_be_const: false, + .. + } + ] => + { + let _tokenized_value = + value_to_tokens(value, Some(&class_unique_id_type_name))?; + todo!() + } + ASN1Value::LinkedNestedValue { value, .. } + if matches![&**value, ASN1Value::LinkedElsewhereDefinedValue { .. }] => + { + value_to_tokens(value, Some(&class_unique_id_type_name))? + } + _ => value_to_tokens(id, Some(&class_unique_id_type_name))?, + }; + let type_id = type_to_tokens(ty).unwrap_or("type?".into()); + let variant_name = match id { + ASN1Value::LinkedElsewhereDefinedValue { + identifier: ref_id, .. + } + | ASN1Value::ElsewhereDeclaredValue { + identifier: ref_id, .. + } => ref_id.clone(), + _ => format!("{field_enum_name}_{index}"), + }; + if ty.constraints().map_or(true, |c| c.is_empty()) { + ids.push((variant_name, type_id, identifier_value)); + } + } + + let variants = ids.iter().map(|(variant_name, type_id, _)| { + format!("{type_id} {}", to_ros_snake_case(variant_name)) + }); + + field_enums.push(format!( + "## OPEN-TYPE {field_enum_name}\n{class_unique_id_type_name} choice\n{}", + variants.fold("".to_string(), |mut acc, v| { + acc.push_str(&v); + acc.push_str("\n"); + acc + }), + )); + } + + Ok(field_enums.join("\n")) + } else { + Err(GeneratorError::new( + Some(ToplevelDefinition::Information(tld)), + "Expected Object Set top-level declaration", + GeneratorErrorType::Asn1TypeMismatch, + )) + } +} diff --git a/utils/codegen/codegen-rust/rgen/src/msgs/mod.rs b/utils/codegen/codegen-rust/rgen/src/msgs/mod.rs new file mode 100644 index 000000000..3bde1ca4a --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/msgs/mod.rs @@ -0,0 +1,51 @@ +use rasn_compiler::prelude::ir::{ASN1Information, ASN1Type}; +use rasn_compiler::prelude::*; + +mod builder; +mod template; +mod utils; + +pub struct Msgs; + +use builder::*; + +fn generate(tld: ToplevelDefinition) -> Result { + match tld { + ToplevelDefinition::Type(t) => { + if t.parameterization.is_some() { + return Ok("".into()); + } + match t.ty { + ASN1Type::Null => generate_null(t), + ASN1Type::Boolean(_) => generate_boolean(t), + ASN1Type::Integer(_) => generate_integer(t), + ASN1Type::Enumerated(_) => generate_enumerated(t), + ASN1Type::BitString(_) => generate_bit_string(t), + ASN1Type::CharacterString(_) => generate_character_string(t), + ASN1Type::Sequence(_) | ASN1Type::Set(_) => generate_sequence_or_set(t), + ASN1Type::SequenceOf(_) | ASN1Type::SetOf(_) => generate_sequence_or_set_of(t), + ASN1Type::ElsewhereDeclaredType(_) => generate_typealias(t), + ASN1Type::Choice(_) => generate_choice(t), + ASN1Type::OctetString(_) => generate_octet_string(t), + ASN1Type::Time(_) => unimplemented!("rasn does not support TIME types yet!"), + ASN1Type::Real(_) => Err(GeneratorError { + kind: GeneratorErrorType::NotYetInplemented, + details: "Real types are currently unsupported!".into(), + top_level_declaration: None, + }), + ASN1Type::ObjectIdentifier(_) => generate_oid(t), + ASN1Type::InformationObjectFieldReference(_) + | ASN1Type::EmbeddedPdv + | ASN1Type::External => generate_any(t), + ASN1Type::GeneralizedTime(_) => generate_generalized_time(t), + ASN1Type::UTCTime(_) => generate_utc_time(t), + ASN1Type::ChoiceSelectionType(_) => unreachable!(), + } + } + ToplevelDefinition::Value(v) => generate_value(v), + ToplevelDefinition::Information(i) => match i.value { + ASN1Information::ObjectSet(_) => generate_information_object_set(i), + _ => Ok("".into()), + }, + } +} diff --git a/utils/codegen/codegen-rust/rgen/src/msgs/template.rs b/utils/codegen/codegen-rust/rgen/src/msgs/template.rs new file mode 100644 index 000000000..423653da9 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/msgs/template.rs @@ -0,0 +1,271 @@ +const MSG_TEMPLATE: &str = +r#"# ============================================================================== +# MIT License +# +# Copyright (c) 2023 Institute for Automotive Engineering (ika), RWTH Aachen University +# Copyright (c) 2024 Instituto de Telecomunicações, Universidade de Aveiro +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================== + +{definition}"#; + +macro_rules! licensed { + ($definition:expr) => { + MSG_TEMPLATE.replace("{definition}", $definition) + }; +} + +pub fn typealias_template(comments: &str, name: &str, alias: &str, annotations: &str) -> String { + licensed!(&format!( + "## TYPE-ALIAS {name}\n\ + {comments}\n\ + {alias} value\n\ + {annotations}" + )) +} + +pub fn integer_value_template(comments: &str, name: &str, vtype: &str, value: &str) -> String { + licensed!(&format!( + "## INTEGER-DEF {name}\n\ + {comments}\n\ + {vtype} {name} = {value}" + )) +} + +pub fn lazy_static_value_template(comments: &str, name: &str, vtype: &str, value: &str) -> String { + licensed!(&format!( + "## VALUE {name}\n\ + {comments}\n\ + {vtype} value = {value}" + )) +} + +pub fn integer_template( + comments: &str, + name: &str, + constraints: &str, + integer_type: &str, + dvalues: &str, +) -> String { + let typed_dvalues = dvalues + .replace("{type}", &integer_type) + .replace("{prefix}", ""); + licensed!(&format!( + "## INTEGER {name}\n\ + {comments}\n\ + {integer_type} value\n\ + {constraints}\n\ + {typed_dvalues}\n" + )) +} + +pub fn generalized_time_template(_comments: &str, _name: &str, _annotations: &str) -> String { + todo!(); +} + +pub fn utc_time_template(_comments: &str, _name: &str, _annotations: &str) -> String { + todo!(); +} + +pub fn bit_string_template(comments: &str, name: &str, constraints: &str, dvalues: &str) -> String { + let typed_dvalues = dvalues + .replace("{type}", "uint8") + .replace("{prefix}", "BIT_INDEX_"); + licensed!(&format!( + "## BIT-STRING {name}\n\ + {comments}\n\ + uint8[] value\n\ + uint8 bits_unused\n\ + {constraints}\n\ + {typed_dvalues}\n" + )) +} + +pub fn octet_string_template(comments: &str, name: &str, constraints: &str) -> String { + licensed!(&format!( + "## OCTET-STRING {name}\n\ + {comments}\n\ + uint8[] value\n\ + {constraints}\n" + )) +} + +pub fn char_string_template( + comments: &str, + name: &str, + string_type: &str, + constraints: &str, +) -> String { + licensed!(&format!( + "## {string_type} {name}\n\ + {comments}\n\ + string value\n\ + {constraints}\n" + )) +} + +pub fn boolean_template(comments: &str, name: &str, constraints: &str) -> String { + licensed!(&format!( + "## BOOLEAN {name}\n\ + {comments}\n\ + bool value\n\ + {constraints}\n" + )) +} + +pub fn primitive_value_template( + _comments: &str, + _name: &str, + _type_name: &str, + _assignment: &str, +) -> String { + todo!(); +} + +pub fn enum_value_template( + _comments: &str, + _name: &str, + _enumerated: &str, + _enumeral: &str, +) -> String { + todo!(); +} + +pub fn null_template(_comments: &str, _name: &str, _annotations: &str) -> String { + todo!(); +} + +pub fn any_template(_comments: &str, _name: &str, _annotations: &str) -> String { + todo!(); +} + +pub fn oid_template(_comments: &str, _name: &str, _annotations: &str) -> String { + todo!(); +} + +pub fn enumerated_template( + comments: &str, + name: &str, + enum_members: &str, + annotations: &str, +) -> String { + licensed!(&format!( + "## ENUMERATED {name}\n\ + {comments}\n\ + uint8 value\n\ + {enum_members}\n\ + {annotations}" + )) +} + +pub fn _sequence_or_set_value_template( + _comments: &str, + _name: &str, + _vtype: &str, + _members: &str, +) -> String { + todo!() +} + +#[allow(clippy::too_many_arguments)] +pub fn sequence_or_set_template( + comments: &str, + name: &str, + members: &str, + nested_members: Vec, + annotations: &str, + default_methods: &str, + class_fields: &str, +) -> String { + licensed!( + &vec![ + &format!("## SEQUENCE {name}"), + comments, + members, + &nested_members.join("\n"), + annotations, + default_methods, + class_fields + ] + .into_iter() + .filter(|s| !s.is_empty()) + .collect::>() + .join("\n") + ) +} + +pub fn sequence_or_set_of_template( + _is_set_of: bool, + comments: &str, + name: &str, + _anonymous_item: &str, + member_type: &str, + constraints: &str, +) -> String { + licensed!(&format!( + "## SEQUENCE-OF {name}\n\ + {comments}\n\ + {member_type}[] array\n\ + {constraints}\n" + )) +} + +pub fn choice_value_template( + _comments: &str, + _name: &str, + _type_id: &str, + _choice_name: &str, + _inner_decl: &str, +) -> String { + todo!(); +} + +pub fn const_choice_value_template( + _comments: &str, + _name: &str, + _type_id: &str, + _choice_name: &str, + _inner_decl: &str, +) -> String { + todo!(); +} + +pub fn choice_template( + comments: &str, + name: &str, + options: &str, + nested_options: Vec, + annotations: &str, +) -> String { + licensed!( + &vec![ + &format!("## CHOICE {name}"), + comments, + "uint8 choice\n", + options, + &nested_options.join("\n"), + annotations + ] + .into_iter() + .filter(|s| !s.is_empty()) + .collect::>() + .join("\n") + ) +} diff --git a/utils/codegen/codegen-rust/rgen/src/msgs/utils.rs b/utils/codegen/codegen-rust/rgen/src/msgs/utils.rs new file mode 100644 index 000000000..527da63be --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/src/msgs/utils.rs @@ -0,0 +1,707 @@ +use crate::common::{to_ros_const_case, to_ros_snake_case, to_ros_title_case, IntegerTypeExt}; +use rasn_compiler::intermediate::{ + constraints::Constraint, + encoding_rules::per_visible::per_visible_range_constraints, + information_object::{InformationObjectClass, InformationObjectField}, + types::{Choice, ChoiceOption, Enumerated, SequenceOrSet, SequenceOrSetMember}, + ASN1Type, ASN1Value, CharacterStringType, IntegerType, ToplevelDefinition, + ToplevelTypeDefinition, +}; +use rasn_compiler::prelude::{ir::*, *}; + +macro_rules! error { + ($kind:ident, $($arg:tt)*) => { + GeneratorError { + details: format!($($arg)*), + top_level_declaration: None, + kind: GeneratorErrorType::$kind, + } + }; +} + +pub(crate) use error; + +use super::*; + +pub fn inner_name(name: &String, parent_name: &String) -> String { + format!("{}{}", parent_name, name) +} + +pub fn int_type_token(opt_min: Option, opt_max: Option, is_extensible: bool) -> String { + if let (Some(min), Some(max)) = (opt_min, opt_max) { + format!( + "{}", + if is_extensible { + "int64" + } else if min >= 0 { + match max { + r if r <= u8::MAX.into() => "uint8", + r if r <= u16::MAX.into() => "uint16", + r if r <= u32::MAX.into() => "uint32", + r if r <= u64::MAX.into() => "uint64", + _ => "uint64", + } + } else { + match (min, max) { + (mi, ma) if mi >= i8::MIN.into() && ma <= i8::MAX.into() => "int8", + (mi, ma) if mi >= i16::MIN.into() && ma <= i16::MAX.into() => "int16", + (mi, ma) if mi >= i32::MIN.into() && ma <= i32::MAX.into() => "int32", + (mi, ma) if mi >= i64::MIN.into() && ma <= i64::MAX.into() => "int64", + _ => "int64", + } + } + ) + } else { + format!("int64") + } +} + +pub fn format_comments(comments: &str) -> Result { + if comments.is_empty() { + Ok("".into()) + } else { + let joined = String::from("# ") + &comments.replace('\n', "\n#") + "\n"; + Ok(joined) + } +} + +pub fn format_constraints( + signed: bool, + constraints: &Vec, +) -> Result { + if constraints.is_empty() { + return Ok("".into()); + } + let per_constraints = per_visible_range_constraints(signed, constraints)?; + let range_type = int_type_token( + per_constraints.min::(), + per_constraints.max::(), + per_constraints.is_extensible(), + ); + let range_suffix = if per_constraints.is_size_constraint() { + "_SIZE" + } else { + "" + }; + // handle default size constraints + if per_constraints.is_size_constraint() + && !per_constraints.is_extensible() + && per_constraints.min::() == Some(0) + && per_constraints.max::().is_none() + { + return Ok("".into()); + } + Ok( + match ( + per_constraints.min::(), + per_constraints.max::(), + per_constraints.is_extensible(), + ) { + (Some(min), Some(max), true) if min == max => { + format!( + "{range_type} {{prefix}}MIN{range_suffix} = {min}\n\ + {range_type} {{prefix}}MAX{range_suffix} = {max}" + ) + } + (Some(min), Some(max), true) => { + format!( + "{range_type} {{prefix}}MIN{range_suffix} = {min}\n\ + {range_type} {{prefix}}MAX{range_suffix} = {max}" + ) + } + (Some(min), Some(max), false) if min == max => { + format!("{range_type} {{prefix}}{range_suffix} = {min}", range_suffix = range_suffix.replace("_","")) + } + (Some(min), Some(max), false) => { + format!( + "{range_type} {{prefix}}MIN{range_suffix} = {min}\n\ + {range_type} {{prefix}}MAX{range_suffix} = {max}" + ) + } + (Some(min), None, true) => { + format!("{range_type} {{prefix}}MIN{range_suffix} = {min}") + } + (Some(min), None, false) => { + format!("{range_type} {{prefix}}MIN{range_suffix} = {min}") + } + (None, Some(max), true) => { + format!("{range_type} {{prefix}}MAX{range_suffix} = {max}") + } + (None, Some(max), false) => { + format!("{range_type} {{prefix}}MAX{range_suffix} = {max}") + } + _ => "".into(), + }, + ) +} + +pub fn get_distinguished_values(ty: &ASN1Type) -> Option> { + match ty { + ASN1Type::Integer(i) => i.distinguished_values.clone(), + _ => None, + } +} + +pub fn format_distinguished_values(dvalues: &Option>) -> String { + let mut result = String::from(""); + if let Some(dvalues) = dvalues { + result = dvalues + .iter() + .map(|dvalue| { + format!( + "{{type}} {{prefix}}{} = {}", + to_ros_const_case(&dvalue.name), + dvalue.value + ) + }) + .collect::>() + .join("\n"); + } + result +} + +pub fn format_enum_members(enumerated: &Enumerated) -> String { + let first_extension_index = enumerated.extensible; + enumerated + .members + .iter() + .enumerate() + .map(|(i, e)| { + let name = to_ros_const_case(&e.name); + let index = e.index; + let extension = if i >= first_extension_index.unwrap_or(usize::MAX) { + "# .extended\n".to_string() + } else { + "".to_string() + }; + String::from(&format!("{extension}uint8 {name} = {index}")) + }) + .fold("".to_string(), |mut acc, e| { + acc.push_str(&e); + acc.push_str("\n"); + acc + }) +} + +pub fn format_sequence_or_set_members( + sequence_or_set: &SequenceOrSet, + parent_name: &String, +) -> Result { + sequence_or_set + .members + .iter() + .try_fold("".to_string(), |mut acc, m| { + format_sequence_member(m, parent_name).map(|declaration| { + acc.push_str(&format!("{declaration}\n\n")); + acc + }) + }) +} + +fn format_sequence_member( + member: &SequenceOrSetMember, + parent_name: &String, +) -> Result { + let name = &member.name; + let (mut all_constraints, mut formatted_type_name) = + constraints_and_type_name(&member.ty, &member.name, parent_name)?; + all_constraints.append(&mut member.constraints.clone()); + let formatted_constraints = format_constraints(false, &all_constraints)? + .replace("{prefix}", &(to_ros_const_case(name).to_string() + &"_")); + let distinguished_values = format_distinguished_values(&get_distinguished_values(&member.ty)) + .replace("{prefix}", &(to_ros_const_case(name).to_string() + &"_")) + .replace("{type}", &formatted_type_name); + let name = to_ros_snake_case(name); + if (member.is_optional && member.default_value.is_none()) + || member.name.starts_with("ext_group_") + { + formatted_type_name = format!( + "{formatted_type_name} {name}\n\ + bool {name}_is_present" + ) + } else { + formatted_type_name = format!("{formatted_type_name} {name}") + } + + Ok( + vec![ + formatted_type_name, + formatted_constraints, + distinguished_values + ] + .into_iter() + .filter(|s| !s.is_empty()) + .collect::>() + .join("\n") + ) +} + +pub fn format_choice_options( + choice: &Choice, + parent_name: &String, +) -> Result { + let options = choice + .options + .iter() + .enumerate() + .map(|(i, o)| { + format_choice_option(o, parent_name, i) + }) + .collect::, _>>()?; + let folded_options = options.iter().fold( + "".to_string(), + |mut acc, option| { + acc.push_str(&format!("{option}\n\n")); + acc + }, + ); + Ok(folded_options) +} + +fn format_choice_option( + member: &ChoiceOption, + parent_name: &String, + index: usize, +) -> Result { + let (_, formatted_type_name) = + constraints_and_type_name(&member.ty, &member.name, parent_name)?; + let option = format!("{formatted_type_name} {}\n\ + uint8 CHOICE_{} = {index}", + to_ros_snake_case(&member.name), + to_ros_const_case(&member.name) + ); + Ok(option) +} + +fn constraints_and_type_name( + ty: &ASN1Type, + name: &String, + parent_name: &String, +) -> Result<(Vec, String), GeneratorError> { + Ok(match ty { + ASN1Type::Null => (vec![], "byte".into()), + ASN1Type::Boolean(b) => (b.constraints.clone(), "bool".into()), + ASN1Type::Integer(i) => { + let per_constraints = per_visible_range_constraints(true, &i.constraints)?; + ( + i.constraints.clone(), + int_type_token( + per_constraints.min(), + per_constraints.max(), + per_constraints.is_extensible(), + ), + ) + } + ASN1Type::Real(_) => (vec![], "float64".into()), + ASN1Type::ObjectIdentifier(_o) => todo!(), + ASN1Type::BitString(_b) => todo!(), + ASN1Type::OctetString(o) => (o.constraints.clone(), "uint8[]".into()), + ASN1Type::GeneralizedTime(_o) => todo!(), + ASN1Type::UTCTime(_o) => todo!(), + ASN1Type::Time(_t) => todo!(), + ASN1Type::CharacterString(c) => (c.constraints.clone(), "string".into()), + ASN1Type::Enumerated(_) + | ASN1Type::Choice(_) + | ASN1Type::Sequence(_) + | ASN1Type::SetOf(_) + | ASN1Type::Set(_) => (vec![], inner_name(name, parent_name)), + ASN1Type::SequenceOf(s) => { + let (_, inner_type) = constraints_and_type_name(&s.element_type, name, parent_name)?; + (s.constraints().clone(), format!("{inner_type}[]").into()) + } + ASN1Type::ElsewhereDeclaredType(e) => { + (e.constraints.clone(), to_ros_title_case(&e.identifier)) + } + ASN1Type::InformationObjectFieldReference(_) + | ASN1Type::EmbeddedPdv + | ASN1Type::External => { + let tx = &ty.constraints().unwrap()[0]; + let rname = if let Constraint::TableConstraint(ref tc) = tx { + let v = &tc.object_set.values[0]; + if let ObjectSetValue::Reference(ref r) = v { + r.clone() + } else { + "".to_string() + } + } else { + "".to_string() + }; + (vec![], rname) + } + ASN1Type::ChoiceSelectionType(_) => unreachable!(), + }) +} + +pub fn string_type(c_type: &CharacterStringType) -> Result { + match c_type { + CharacterStringType::NumericString => Ok("NumericString".into()), + CharacterStringType::VisibleString => Ok("VisibleString".into()), + CharacterStringType::IA5String => Ok("Ia5String".into()), + CharacterStringType::TeletexString => Ok("TeletexString".into()), + CharacterStringType::VideotexString => Ok("VideotexString".into()), + CharacterStringType::GraphicString => Ok("GraphicString".into()), + CharacterStringType::GeneralString => Ok("GeneralString".into()), + CharacterStringType::UniversalString => Ok("UniversalString".into()), + CharacterStringType::UTF8String => Ok("Utf8String".into()), + CharacterStringType::BMPString => Ok("BmpString".into()), + CharacterStringType::PrintableString => Ok("PrintableString".into()), + } +} + +pub fn format_default_methods( + members: &Vec, + _parent_name: &str, +) -> Result { + let mut output = "".to_string(); + for member in members { + if let Some(value) = member.default_value.as_ref() { + let val = match value { + ASN1Value::EnumeratedValue { .. } => continue, /* TODO */ + _ => value_to_tokens(value, Some(&type_to_tokens(&member.ty)?.to_string()))?, + }; + // TODO generalize + let ty = match value { + ASN1Value::LinkedNestedValue { + supertypes: _, + value, + } => match value.as_ref() { + ASN1Value::LinkedIntValue { + integer_type, + value: _, + } => integer_type.to_str().to_string(), + _ => type_to_tokens(&member.ty)?, + }, + ASN1Value::EnumeratedValue { .. } => "uint8".into(), + _ => type_to_tokens(&member.ty)?, + }; + let method_name = format!("{}_DEFAULT", to_ros_const_case(&member.name)); + output.push_str(&format!("{ty} {method_name} = {val}\n")); + } + } + Ok(output) +} + +pub fn type_to_tokens(ty: &ASN1Type) -> Result { + match ty { + ASN1Type::Null => todo!(), + ASN1Type::Boolean(_) => Ok("bool".into()), + ASN1Type::Integer(i) => Ok(i.int_type().to_str().to_string()), + ASN1Type::Real(_) => Ok("float64".into()), + ASN1Type::BitString(_) => Ok("BitString".into()), + ASN1Type::OctetString(_) => Ok("OctetString".into()), + ASN1Type::CharacterString(CharacterString { ty, .. }) => string_type(ty), + ASN1Type::Enumerated(_) => Err(error!( + NotYetInplemented, + "Enumerated values are currently unsupported!" + )), + ASN1Type::Choice(_) => Err(error!( + NotYetInplemented, + "Choice values are currently unsupported!" + )), + ASN1Type::Sequence(_) => Err(error!( + NotYetInplemented, + "Sequence values are currently unsupported!" + )), + ASN1Type::SetOf(so) | ASN1Type::SequenceOf(so) => { + let _inner = type_to_tokens(&so.element_type)?; + Ok("SequenceOf".into()) + } + ASN1Type::ObjectIdentifier(_) => Err(error!( + NotYetInplemented, + "Object Identifier values are currently unsupported!" + )), + ASN1Type::Set(_) => Err(error!( + NotYetInplemented, + "Set values are currently unsupported!" + )), + ASN1Type::ElsewhereDeclaredType(e) => Ok(e.identifier.clone()), + ASN1Type::InformationObjectFieldReference(_) => Err(error!( + NotYetInplemented, + "Information Object field reference values are currently unsupported!" + )), + ASN1Type::Time(_) => Err(error!( + NotYetInplemented, + "Time values are currently unsupported!" + )), + ASN1Type::GeneralizedTime(_) => Ok("GeneralizedTime".into()), + ASN1Type::UTCTime(_) => Ok("UtcTime".into()), + ASN1Type::EmbeddedPdv | ASN1Type::External => Ok("Any".into()), + ASN1Type::ChoiceSelectionType(_) => { + todo!() + } + } +} + +pub fn value_to_tokens( + value: &ASN1Value, + type_name: Option<&String>, +) -> Result { + match value { + ASN1Value::All => Err(error!( + NotYetInplemented, + "All values are currently unsupported!" + )), + ASN1Value::Null => todo!(), + ASN1Value::Choice { inner_value, .. } => { + if let Some(_) = type_name { + todo!() + } else { + Err(error!( + Unidentified, + "A type name is needed to stringify choice value {:?}", inner_value + )) + } + } + ASN1Value::OctetString(o) => { + let _bytes = o.iter().map(|byte| *byte); + todo!() + } + ASN1Value::SequenceOrSet(_) => Err(error!( + Unidentified, + "Unexpectedly encountered unlinked struct-like ASN1 value!" + )), + ASN1Value::LinkedStructLikeValue(fields) => { + if let Some(_ty_n) = type_name { + let _tokenized_fields = fields + .iter() + .map(|(_, _, val)| value_to_tokens(val.value(), None)) + .collect::, _>>()?; + todo!() + } else { + Err(error!( + Unidentified, + "A type name is needed to stringify sequence value {:?}", value + )) + } + } + ASN1Value::Boolean(b) => Ok(b.to_string()), + ASN1Value::Integer(i) => Ok(i.to_string()), + ASN1Value::String(s) => Ok(s.to_string()), + ASN1Value::Real(r) => Ok(r.to_string()), + ASN1Value::BitString(b) => { + let _bits = b.iter().map(|bit| bit.to_string()); + todo!() + } + ASN1Value::EnumeratedValue { + enumerated, + enumerable, + } => Ok(format!("{}_{}", enumerated, enumerable)), + ASN1Value::LinkedElsewhereDefinedValue { identifier: e, .. } + | ASN1Value::ElsewhereDeclaredValue { identifier: e, .. } => Ok(e.to_string()), + ASN1Value::ObjectIdentifier(oid) => { + let _arcs = oid + .0 + .iter() + .filter_map(|arc| arc.number.map(|id| id.to_string())); + todo!() + } + ASN1Value::Time(_t) => match type_name { + Some(_time_type) => todo!(), + None => todo!(), + }, + ASN1Value::LinkedArrayLikeValue(seq) => { + let _elems = seq + .iter() + .map(|v| value_to_tokens(v, None)) + .collect::, _>>()?; + todo!() + } + ASN1Value::LinkedNestedValue { + supertypes: _, + value, + } => Ok(value_to_tokens(value, type_name)?), + ASN1Value::LinkedIntValue { + integer_type, + value, + } => { + let val = *value; + match integer_type { + IntegerType::Unbounded => Ok(val.to_string()), + _ => Ok(val.to_string()), + } + } + ASN1Value::LinkedCharStringValue(string_type, value) => { + let _val = value; + match string_type { + CharacterStringType::NumericString => { + todo!() + } + CharacterStringType::VisibleString => { + todo!() + } + CharacterStringType::IA5String => { + todo!() + } + CharacterStringType::UTF8String => todo!(), + CharacterStringType::BMPString => { + todo!() + } + CharacterStringType::PrintableString => { + todo!() + } + CharacterStringType::GeneralString => { + todo!() + } + CharacterStringType::VideotexString + | CharacterStringType::GraphicString + | CharacterStringType::UniversalString + | CharacterStringType::TeletexString => Err(GeneratorError::new( + None, + &format!("{:?} values are currently unsupported!", string_type), + GeneratorErrorType::NotYetInplemented, + )), + } + } + } +} + +pub fn format_nested_sequence_members( + sequence_or_set: &SequenceOrSet, + parent_name: &String, +) -> Result, GeneratorError> { + sequence_or_set + .members + .iter() + .filter(|m| needs_unnesting(&m.ty)) + .map(|m| { + generate(ToplevelDefinition::Type(ToplevelTypeDefinition { + parameterization: None, + comments: " Inner type ".into(), + name: inner_name(&m.name, parent_name).to_string(), + ty: m.ty.clone(), + tag: None, + index: None, + })) + }) + .collect::, _>>() +} + +fn needs_unnesting(ty: &ASN1Type) -> bool { + match ty { + ASN1Type::Enumerated(_) + | ASN1Type::Choice(_) + | ASN1Type::Sequence(_) + | ASN1Type::Set(_) => true, + ASN1Type::SequenceOf(SequenceOrSetOf { element_type, .. }) + | ASN1Type::SetOf(SequenceOrSetOf { element_type, .. }) => needs_unnesting(element_type), + _ => false, + } +} + +pub fn format_nested_choice_options( + choice: &Choice, + parent_name: &String, +) -> Result, GeneratorError> { + choice + .options + .iter() + .filter(|m| { + matches!( + m.ty, + ASN1Type::Enumerated(_) + | ASN1Type::Choice(_) + | ASN1Type::Sequence(_) + | ASN1Type::SequenceOf(_) + | ASN1Type::Set(_) + ) + }) + .map(|m| { + generate(ToplevelDefinition::Type(ToplevelTypeDefinition { + parameterization: None, + comments: " Inner type ".into(), + name: inner_name(&m.name, parent_name).to_string(), + ty: m.ty.clone(), + tag: None, + index: None, + })) + }) + .collect::, _>>() +} + +pub fn _format_sequence_or_set_of_item_type( + type_name: String, + first_item: Option<&ASN1Value>, +) -> String { + match type_name { + name if name == NULL => todo!(), + name if name == BOOLEAN => "bool".into(), + name if name == INTEGER => { + match first_item { + Some(ASN1Value::LinkedIntValue { integer_type, .. }) => { + integer_type.to_str().into() + } + _ => "int64".into(), // best effort + } + } + name if name == BIT_STRING => "BitString".into(), + name if name == OCTET_STRING => "OctetString".into(), + name if name == GENERALIZED_TIME => "GeneralizedTime".into(), + name if name == UTC_TIME => "UtcTime".into(), + name if name == OBJECT_IDENTIFIER => "ObjectIdentifier".into(), + name if name == NUMERIC_STRING => "NumericString".into(), + name if name == VISIBLE_STRING => "VisibleString".into(), + name if name == IA5_STRING => "IA5String".into(), + name if name == UTF8_STRING => "UTF8String".into(), + name if name == BMP_STRING => "BMPString".into(), + name if name == PRINTABLE_STRING => "PrintableString".into(), + name if name == GENERAL_STRING => "GeneralString".into(), + name => name, + } +} + +/// Resolves the custom syntax declared in an information object class' WITH SYNTAX clause +pub fn resolve_standard_syntax( + class: &InformationObjectClass, + application: &[InformationObjectField], +) -> Result<(ASN1Value, Vec<(usize, ASN1Type)>), GeneratorError> { + let mut key = None; + let mut field_index_map = Vec::<(usize, ASN1Type)>::new(); + + let key_index = class + .fields + .iter() + .enumerate() + .find_map(|(i, f)| f.is_unique.then_some(i)) + .ok_or_else(|| GeneratorError { + details: format!("Could not find key for class {class:?}"), + kind: GeneratorErrorType::MissingClassKey, + ..Default::default() + })?; + + let mut appl_iter = application.iter().enumerate(); + 'syntax_matching: for class_field in &class.fields { + if let Some((index, field)) = appl_iter.next() { + if class_field.identifier.identifier() == field.identifier() { + match field { + InformationObjectField::TypeField(f) => { + field_index_map.push((index, f.ty.clone())); + } + InformationObjectField::FixedValueField(f) => { + if index == key_index { + key = Some(f.value.clone()); + } + } + InformationObjectField::ObjectSetField(_) => todo!(), + } + } else if !class_field.is_optional { + return Err(GeneratorError { + top_level_declaration: None, + details: "Syntax mismatch while resolving information object.".to_string(), + kind: GeneratorErrorType::SyntaxMismatch, + }); + } else { + continue 'syntax_matching; + } + } + } + field_index_map.sort_by(|&(a, _), &(b, _)| a.cmp(&b)); + let types = field_index_map.into_iter().collect(); + match key { + Some(k) => Ok((k, types)), + None => Err(GeneratorError { + top_level_declaration: None, + details: "Could not find class key!".into(), + kind: GeneratorErrorType::MissingClassKey, + }), + } +} diff --git a/utils/codegen/codegen-rust/rgen/tests/conversion.rs b/utils/codegen/codegen-rust/rgen/tests/conversion.rs new file mode 100644 index 000000000..992c2ccc6 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/tests/conversion.rs @@ -0,0 +1,136 @@ +mod utils; + +e2e_hs!( + single_byte, + r#" SingleByte ::= INTEGER (0..255)"#, + r#" +#pragma once +#include + +#include +#include +#include +#ifdef ROS1 +#include +namespace test_msgs = etsi_its_test_msgs; +#else +#include +namespace test_msgs = etsi_its_test_msgs::msg; +#endif + +namespace etsi_its_test_conversion { + +void toRos_SingleByte(const SingleByte_t& in, test_msgs::SingleByte& out) { + etsi_its_primitives_conversion::toRos_INTEGER(in, out.value); +} + +void toStruct_SingleByte(const test_msgs::SingleByte& in, SingleByte_t& out) { + memset(&out, 0, sizeof(SingleByte_t)); + + etsi_its_primitives_conversion::toStruct_INTEGER(in.value, out); +} + +}"# +); + +e2e_hs!( + integer_unconstrained, + r#" Unbound ::= INTEGER "#, + r#" +#pragma once +#include + +#include +#include +#include +#ifdef ROS1 +#include +namespace test_msgs = etsi_its_test_msgs; +#else +#include +namespace test_msgs = etsi_its_test_msgs::msg; +#endif + +namespace etsi_its_test_conversion { + +void toRos_Unbound(const Unbound_t& in, test_msgs::Unbound& out) { + etsi_its_primitives_conversion::toRos_INTEGER(in, out.value); +} + +void toStruct_Unbound(const test_msgs::Unbound& in, Unbound_t& out) { + memset(&out, 0, sizeof(Unbound_t)); + + etsi_its_primitives_conversion::toStruct_INTEGER(in.value, out); +} + +}"# +); + +e2e_hs!( + sequence, + r#" Seq ::= SEQUENCE { aBigNumber INTEGER, anotherBigNumber INTEGER} "#, + r#" +#pragma once +#include + +#include +#include +#include +#include +#include +#ifdef ROS1 +#include +namespace test_msgs = etsi_its_test_msgs; +#else +#include +namespace test_msgs = etsi_its_test_msgs::msg; +#endif + +namespace etsi_its_test_conversion { + +void toRos_Seq(const Seq_t& in, test_msgs::Seq& out) { + etsi_its_primitives_conversion::toRos_INTEGER(in.aBigNumber, out.a_big_number); + etsi_its_primitives_conversion::toRos_INTEGER(in.anotherBigNumber, out.another_big_number); +} + +void toStruct_Seq(const test_msgs::Seq& in, Seq_t& out) { + memset(&out, 0, sizeof(Seq_t)); + + etsi_its_primitives_conversion::toStruct_INTEGER(in.a_big_number, out.aBigNumber); + etsi_its_primitives_conversion::toStruct_INTEGER(in.another_big_number, out.anotherBigNumber); +} + +}"# +); + +e2e_hs!(boolean, + r#" Maybe ::= BOOLEAN "#, + r#" +#pragma once +#include + +#include +#include +#include +#ifdef ROS1 +#include +namespace test_msgs = etsi_its_test_msgs; +#else +#include +namespace test_msgs = etsi_its_test_msgs::msg; +#endif + +namespace etsi_its_test_conversion { + +void toRos_Maybe(const Maybe_t& in, test_msgs::Maybe& out) { + etsi_its_primitives_conversion::toRos_BOOLEAN(in, out.value); +} + +void toStruct_Maybe(const test_msgs::Maybe& in, Maybe_t& out) { + memset(&out, 0, sizeof(Maybe_t)); + + etsi_its_primitives_conversion::toStruct_BOOLEAN(in.value, out); +} + +}"# +); diff --git a/utils/codegen/codegen-rust/rgen/tests/msgs.rs b/utils/codegen/codegen-rust/rgen/tests/msgs.rs new file mode 100644 index 000000000..b6863db2b --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/tests/msgs.rs @@ -0,0 +1,50 @@ +mod utils; + +e2e_msgs!( + single_byte, + r#" SingleByte ::= INTEGER (0..255)"#, + r#" uint8 value + uint8 VALUE_MIN = 0 + uint8 VALUE_MAX = 255 "# +); + +e2e_msgs!( + integer_unconstrained, + r#" Unbound ::= INTEGER "#, + r#" int64 value "# +); + +e2e_msgs!( + integer_constrained_positive, + r#" PositiveNumber ::= INTEGER (0..MAX) "#, + r#" int64 value + int64 VALUE_MIN = 0 "# +); + +e2e_msgs!( + integer_constrained_negative, + r#" NegativeNumber ::= INTEGER (MIN..-1) "#, + r#" int64 value + int64 VALUE_MAX = -1 "# +); + +e2e_msgs!( + sequence, + r#" Seq ::= SEQUENCE { aBigNumber INTEGER, anotherBigNumber INTEGER} "#, + r#" int64 a_big_number + int64 another_big_number "# +); + +e2e_msgs!(boolean, r#" Maybe ::= BOOLEAN "#, r#" bool value "#); + +e2e_msgs!( + choice, + r#" Choose ::= CHOICE {aNumber INTEGER, aByteString OCTET STRING}"#, + r#" uint8 choice + + int64 a_number + uint8[] a_byte_string + + uint8 CHOICE_A_NUMBER = 0 + uint8 CHOICE_A_BYTE_STRING = 1"# +); diff --git a/utils/codegen/codegen-rust/rgen/tests/utils.rs b/utils/codegen/codegen-rust/rgen/tests/utils.rs new file mode 100644 index 000000000..9b0849ad0 --- /dev/null +++ b/utils/codegen/codegen-rust/rgen/tests/utils.rs @@ -0,0 +1,61 @@ +#[macro_export] +macro_rules! e2e_msgs { + ($suite:ident, $asn1:literal, $expected:literal) => { + #[test] + fn $suite() { + assert_eq!( + rasn_compiler::Compiler::new() + .with_backend(ros_backend::msgs::Msgs) + .add_asn_literal(&format!( + "TestModule DEFINITIONS AUTOMATIC TAGS::= BEGIN {} END", + $asn1 + )) + .compile_to_string() + .unwrap() + .generated + .replace("\n", "") + .replace("\n", "") + .lines() + .skip(1) + .collect::>() + .join("\n") + .replace(|c: char| c.is_whitespace(), ""), + format!("{}", $expected) + .to_string() + .replace(|c: char| c.is_whitespace(), ""), + ) + } + }; +} + +#[macro_export] +macro_rules! e2e_hs { + ($suite:ident, $asn1:literal, $expected:literal) => { + #[test] + fn $suite() { + assert_eq!( + rasn_compiler::Compiler::new() + .with_backend( + ros_backend::conversion::Conversion::default().set_main_pdu_name("test") + ) + .add_asn_literal(&format!( + "TestModule DEFINITIONS AUTOMATIC TAGS::= BEGIN {} END", + $asn1 + )) + .compile_to_string() + .unwrap() + .generated + .replace("#\n", "") + .replace("\n#", "") + .lines() + .skip(1) + .collect::>() + .join("\n") + .replace(|c: char| c.is_whitespace(), ""), + format!("{}", $expected) + .to_string() + .replace(|c: char| c.is_whitespace(), ""), + ) + } + }; +}