From dafb3d9da8c4751e7767e36d92f89c55939cfc60 Mon Sep 17 00:00:00 2001 From: Holger Rapp Date: Thu, 28 Sep 2023 17:59:19 +0200 Subject: [PATCH] Rust integration (experimental) (#99) * First support for Rust. * Bump version to 0.7.0 --------- Signed-off-by: Holger Rapp Signed-off-by: Kai-Uwe Hermann Co-authored-by: Kai-Uwe Hermann --- .gitignore | 2 + CMakeLists.txt | 7 +- cmake/cxxrs.cmake | 19 ++ everestrs/CMakeLists.txt | 35 ++++ everestrs/Cargo.lock | 196 ++++++++++++++++++++ everestrs/Cargo.toml | 5 + everestrs/everestrs/Cargo.toml | 11 ++ everestrs/everestrs/README.md | 29 +++ everestrs/everestrs/build.rs | 88 +++++++++ everestrs/everestrs/src/lib.rs | 176 ++++++++++++++++++ everestrs/everestrs/src/schema/interface.rs | 151 +++++++++++++++ everestrs/everestrs/src/schema/manifest.rs | 26 +++ everestrs/everestrs/src/schema/mod.rs | 16 ++ everestrs/everestrs_sys/everestrs_sys.cpp | 71 +++++++ everestrs/everestrs_sys/everestrs_sys.hpp | 33 ++++ 15 files changed, 864 insertions(+), 1 deletion(-) create mode 100644 cmake/cxxrs.cmake create mode 100644 everestrs/CMakeLists.txt create mode 100644 everestrs/Cargo.lock create mode 100644 everestrs/Cargo.toml create mode 100644 everestrs/everestrs/Cargo.toml create mode 100644 everestrs/everestrs/README.md create mode 100644 everestrs/everestrs/build.rs create mode 100644 everestrs/everestrs/src/lib.rs create mode 100644 everestrs/everestrs/src/schema/interface.rs create mode 100644 everestrs/everestrs/src/schema/manifest.rs create mode 100644 everestrs/everestrs/src/schema/mod.rs create mode 100644 everestrs/everestrs_sys/everestrs_sys.cpp create mode 100644 everestrs/everestrs_sys/everestrs_sys.hpp diff --git a/.gitignore b/.gitignore index 001c498b..22599542 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *build *build-cross +target +watcher.lua workspace.yaml diff --git a/CMakeLists.txt b/CMakeLists.txt index b5eaa9e8..146a832c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) project(everest-framework - VERSION 0.6.2 + VERSION 0.7.0 DESCRIPTION "The open operating system for e-mobility charging stations" LANGUAGES CXX C ) @@ -16,6 +16,7 @@ option(FRAMEWORK_INSTALL "Install the library (shared data might be installed an option(CMAKE_RUN_CLANG_TIDY "Run clang-tidy" OFF) option(EVEREST_ENABLE_JS_SUPPORT "Enable everestjs for JavaScript modules" ON) option(EVEREST_ENABLE_PY_SUPPORT "Enable everestpy for Python modules" ON) +option(EVEREST_ENABLE_RS_SUPPORT "Enable everestrs for Rust modules" OFF) # make own cmake modules available list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -93,6 +94,10 @@ if (EVEREST_ENABLE_PY_SUPPORT) add_subdirectory(everestpy) endif() +if (EVEREST_ENABLE_RS_SUPPORT) + add_subdirectory(everestrs) +endif() + # FIXME (aw): should this be installed or not? Right now it is needed for the # current packaging approach install(TARGETS framework diff --git a/cmake/cxxrs.cmake b/cmake/cxxrs.cmake new file mode 100644 index 00000000..bd0f9a1a --- /dev/null +++ b/cmake/cxxrs.cmake @@ -0,0 +1,19 @@ +function(emit_cxxrs_header) + set(output_dir "${CMAKE_CURRENT_BINARY_DIR}/cxxbridge/rust/") + file(MAKE_DIRECTORY ${output_dir}) + execute_process(COMMAND ${CXXBRIDGE} --header -o ${output_dir}cxx.h) +endfunction() + +function(emit_cxxrs_for_module module_name) + set(output_dir "${CMAKE_CURRENT_BINARY_DIR}/cxxbridge/${module_name}/") + file(MAKE_DIRECTORY ${output_dir}) + + add_custom_command( + OUTPUT ${output_dir}lib.rs.h ${output_dir}lib.rs.cc + COMMAND ${CXXBRIDGE} ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}/src/lib.rs --header -o ${output_dir}lib.rs.h + COMMAND ${CXXBRIDGE} ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}/src/lib.rs -o ${output_dir}lib.rs.cc + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}/src/lib.rs + COMMENT "Generating cxx for ${module_name}" + VERBATIM + ) +endfunction() diff --git a/everestrs/CMakeLists.txt b/everestrs/CMakeLists.txt new file mode 100644 index 00000000..17db4f6f --- /dev/null +++ b/everestrs/CMakeLists.txt @@ -0,0 +1,35 @@ +include(cxxrs) + +find_program(CXXBRIDGE cxxbridge PATHS "$ENV{HOME}/.cargo/bin/") +if (CXXBRIDGE STREQUAL "CXXBRIDGE-NOTFOUND") + message("Could not find cxxbridge, trying to install with `cargo install cxxbridge-cmd'") + find_program(CARGO cargo PATHS "$ENV{HOME}/.cargo/bin/") + if (CARGO STREQUAL "CARGO-NOTFOUND") + message(FATAL_ERROR "Requires cargo available in path, install via rustup https://rustup.rs/") + endif() + execute_process(COMMAND ${CARGO} install cxxbridge-cmd --version 1.0.107) + find_program(CXXBRIDGE cxxbridge PATHS "$ENV{HOME}/.cargo/bin/") +endif() + +emit_cxxrs_header() +emit_cxxrs_for_module(everestrs) + +add_library(everestrs_sys STATIC + ${CMAKE_CURRENT_BINARY_DIR}/cxxbridge/everestrs/lib.rs.cc + everestrs_sys/everestrs_sys.cpp +) + +target_include_directories(everestrs_sys PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/cxxbridge +) + +# This is a requirement that linking works on systems enforcing PIE. +set_property(TARGET everestrs_sys PROPERTY POSITION_INDEPENDENT_CODE ON) +target_link_libraries(everestrs_sys + PRIVATE + everest::framework + everest::log +) + +install(TARGETS everestrs_sys LIBRARY) diff --git a/everestrs/Cargo.lock b/everestrs/Cargo.lock new file mode 100644 index 00000000..c4eb47c2 --- /dev/null +++ b/everestrs/Cargo.lock @@ -0,0 +1,196 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "argh" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cxx" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe98ba1789d56fb3db3bee5e032774d4f421b685de7ba703643584ba24effbe" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20888d9e1d2298e2ff473cee30efe7d5036e437857ab68bbfea84c74dba91da2" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa16a70dd58129e4dfffdff535fb1bce66673f7bbeec4a5a1765a504e1ccd84" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "everestrs" +version = "0.1.0" +dependencies = [ + "argh", + "cxx", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" diff --git a/everestrs/Cargo.toml b/everestrs/Cargo.toml new file mode 100644 index 00000000..81876617 --- /dev/null +++ b/everestrs/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ + "everestrs", +] diff --git a/everestrs/everestrs/Cargo.toml b/everestrs/everestrs/Cargo.toml new file mode 100644 index 00000000..0d0629b2 --- /dev/null +++ b/everestrs/everestrs/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "everestrs" +version = "0.1.0" +edition = "2021" + +[dependencies] +argh = "0.1.10" +cxx = { version = "1.0.107", features = ["c++17"] } +serde = { version = "1.0.175", features = ["derive"] } +serde_json = "1" +thiserror = "1.0.48" diff --git a/everestrs/everestrs/README.md b/everestrs/everestrs/README.md new file mode 100644 index 00000000..4461d562 --- /dev/null +++ b/everestrs/everestrs/README.md @@ -0,0 +1,29 @@ +# Rust support for everest + +This is Rust support using cxx.rs to wrap the framework C++ library. + +## Trying it out + + - Install rust as outlined on , which should just be this + one line: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` + - Built your workspace as outlined in `everest-core` README, make sure to tell + cMake to enable `EVEREST_ENABLE_RS_SUPPORT`. Note, that the Rust code relies + on being built in a workspace where `make install` was run once. + - You can now try building the code, but it will not do anything: `cd everestrs + && cargo build --all` + - If you want to play with a node, check out `https://github.com/EVerest/everest-core/pull/344` in your workspace and run make install. + - Go to `everest-core/modules/RsSomkeTest` and run `cargo build` there. + - There is no support for building or installing Rust modules with cMake + currently, so let's fake the installation: + - Go to `everest-core/build/dist/libexec/everest/modules` and create the stuff needed for a module: + ~~~bash + mkdir RsSmokeTest + ln -s ../../../../../../modules/RsSmokeTest/target/debug/smoke_test RsSmokeTest + ln -s ../../../../../../modules/RsSmokeTest/manifest.yaml . + ~~~ + - You should now be able to configure the `RsSmokeTest` module in your config + YAML. + +## Status + +This code is currently only supporting providing an interface to be implemented, i.e. no variables publish or receiving and no calling of other interfaces. Those features are straightforward, quick and easy to implement, but for now this is probably enough to iron out the integration questions. diff --git a/everestrs/everestrs/build.rs b/everestrs/everestrs/build.rs new file mode 100644 index 00000000..337da27a --- /dev/null +++ b/everestrs/everestrs/build.rs @@ -0,0 +1,88 @@ +use std::env; +use std::path::{Path, PathBuf}; + +struct Libraries { + everestrs_sys: PathBuf, + framework: PathBuf, +} + +fn find_everest_workspace_root() -> PathBuf { + let mut cur_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("always set in build.rs execution")); + + // A poor heuristic: We traverse the directories upwards until we find a directory called + // everest-framework and hope that this is the EVerest workspace. + while cur_dir.parent().is_some() { + cur_dir = cur_dir.parent().unwrap().to_path_buf(); + if cur_dir.join("everest-framework").is_dir() { + return cur_dir; + } + } + panic!("everstrs is not build within an EVerest workspace."); +} + +/// Returns the Libraries path if this is a standalone build of everest-framework or None if it is +/// not. +fn find_libs_in_everest_framework(root: &Path) -> Option { + let everestrs_sys = root.join("everest-framework/build/everestrs/libeverestrs_sys.a"); + let framework = root.join("everest-framework/build/lib/libframework.so"); + if everestrs_sys.exists() && framework.exists() { + Some(Libraries { + everestrs_sys, + framework, + }) + } else { + None + } +} + +/// Returns the Libraries path if this is an EVerest workspace where make install was run in +/// everest-core/build or None if not. +fn find_libs_in_everest_core_build_dist(root: &Path) -> Option { + let everestrs_sys = root.join("everest-core/build/dist/lib/libeverestrs_sys.a"); + let framework = root.join("everest-core/build/dist/lib/libframework.so"); + if everestrs_sys.exists() && framework.exists() { + Some(Libraries { + everestrs_sys, + framework, + }) + } else { + None + } +} + +/// Takes a path to a library like `libframework.so` and returns the name for the linker, aka +/// `framework` +fn libname_from_path(p: &Path) -> String { + p.file_stem() + .and_then(|os_str| os_str.to_str()) + .expect("'p' must be valid UTF-8 and have a .so extension.") + .strip_prefix("lib") + .expect("'p' should start with `lib`") + .to_string() +} + +fn print_link_options(p: &Path) { + println!( + "cargo:rustc-link-search=native={}", + p.parent().unwrap().to_string_lossy() + ); + println!("cargo:rustc-link-lib={}", libname_from_path(p)); +} + +fn find_libs(root: &Path) -> Libraries { + let libs = find_libs_in_everest_core_build_dist(&root); + if libs.is_some() { + return libs.unwrap(); + } + find_libs_in_everest_framework(&root) + .expect("everestrs is not build in a EVerest workspace that already ran cmake build") +} + +fn main() { + let root = find_everest_workspace_root(); + let libs = find_libs(&root); + + print_link_options(&libs.everestrs_sys); + print_link_options(&libs.framework); +} diff --git a/everestrs/everestrs/src/lib.rs b/everestrs/everestrs/src/lib.rs new file mode 100644 index 00000000..6b7f12c6 --- /dev/null +++ b/everestrs/everestrs/src/lib.rs @@ -0,0 +1,176 @@ +mod schema; + +use argh::FromArgs; +use serde::de::DeserializeOwned; +use std::collections::HashMap; +use std::path::PathBuf; +use std::pin::Pin; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("missing argument to command call: '{0}'")] + MissingArgument(&'static str), + #[error("invalid argument to command call: '{0}'")] + InvalidArgument(&'static str), +} + +pub type Result = ::std::result::Result; + +#[cxx::bridge] +mod ffi { + struct CommandMeta { + implementation_id: String, + name: String, + } + + extern "Rust" { + type Runtime; + fn handle_command(self: &Runtime, meta: &CommandMeta, json: JsonBlob) -> JsonBlob; + fn on_ready(&self); + } + + struct JsonBlob { + data: Vec, + } + + unsafe extern "C++" { + include!("everestrs_sys/everestrs_sys.hpp"); + + type Module; + fn create_module(module_id: &str, prefix: &str, conf: &str) -> UniquePtr; + + /// Connects to the message broker and launches the main everest thread to push work + /// forward. Returns the module manifest. + fn initialize(self: Pin<&mut Module>) -> JsonBlob; + + /// Returns the interface definition. + fn get_interface(self: &Module, interface_name: &str) -> JsonBlob; + + /// Registers the callback of the `GenericModule` to be called and calls + /// `Everest::Module::signal_ready`. + fn signal_ready(self: &Module, rt: &Runtime); + + /// Informs the runtime that we implement the command described in `meta` and registers the + /// `handle_command` method from the `GenericModule` as the handler. + fn provide_command(self: &Module, rt: &Runtime, meta: &CommandMeta); + } +} + +impl ffi::JsonBlob { + fn as_bytes(&self) -> &[u8] { + &self.data + } + + fn deserialize(self) -> T { + // TODO(hrapp): Error handling + serde_json::from_slice(self.as_bytes()).unwrap() + } + + fn from_vec(data: Vec) -> Self { + Self { data } + } +} + +#[derive(FromArgs, Debug)] +/// An everest Node. +struct Args { + /// prefix of installation. + #[argh(option)] + #[allow(unused)] + pub prefix: PathBuf, + + /// configuration yml that we are running. + #[argh(option)] + #[allow(unused)] + pub conf: PathBuf, + + /// module name for us. + #[argh(option)] + pub module: String, +} + +/// Implements the handling of commands & variables, but has no specific information about the +/// details of the current module, i.e. it deals with JSON blobs and strings as command names. Code +/// generation is used to build the concrete, strongly typed abstractions that are then used by +/// final implementors. +pub trait GenericModule: Sync { + /// Handler for the command `name` on `implementation_id` with the given `parameters`. The return value + /// will be returned as the result of the call. + fn handle_command( + &self, + implementation_id: &str, + name: &str, + parameters: HashMap, + ) -> Result; + + fn on_ready(&self) {} +} + +pub struct Runtime { + // There are two subtleties here: + // 1. We are handing out pointers to `module_impl` to `cpp_module` for callbacks. The pointers + // must must stay valid for as long as `cpp_module` is alive. Hence `module_impl` must never + // move in memory. Rust can model this through the Pin concept which upholds this guarantee. + // We use a Box to put the object on the heap. + // 2. For the same reason, `module_impl` should outlive `cpp_module`, hence should be dropped + // after it. Rust drops fields in declaration order, hence `cpp_module` should come before + // `module_impl` in this struct. + cpp_module: cxx::UniquePtr, + module_impl: Pin>, +} + +impl Runtime { + fn on_ready(&self) { + self.module_impl.on_ready(); + } + + fn handle_command(&self, meta: &ffi::CommandMeta, json: ffi::JsonBlob) -> ffi::JsonBlob { + let blob = self + .module_impl + .handle_command(&meta.implementation_id, &meta.name, json.deserialize()) + .unwrap(); + ffi::JsonBlob::from_vec(serde_json::to_vec(&blob).unwrap()) + } + + // TODO(hrapp): This function could use some error handling. + pub fn from_commandline(module_impl: T) -> Self { + let args: Args = argh::from_env(); + let mut cpp_module = ffi::create_module( + &args.module, + &args.prefix.to_string_lossy(), + &args.conf.to_string_lossy(), + ); + let manifest_json = cpp_module.as_mut().unwrap().initialize(); + let manifest: schema::Manifest = manifest_json.deserialize(); + let module = Self { + cpp_module, + module_impl: Box::pin(module_impl), + }; + + // Implement all commands for all of our implementations, dispatch everything to the + // GenericModule. + for (implementation_id, implementation) in manifest.provides { + let interface_s = module.cpp_module.get_interface(&implementation.interface); + let interface: schema::Interface = interface_s.deserialize(); + for (name, _) in interface.cmds { + let meta = ffi::CommandMeta { + implementation_id: implementation_id.clone(), + name, + }; + + module + .cpp_module + .as_ref() + .unwrap() + .provide_command(&module, &meta); + } + } + + // Since users can choose to overwrite `on_ready`, we can call signal_ready right away. + // TODO(sirver): There were some doubts if this strategy is too inflexible, discuss design + // again. + module.cpp_module.as_ref().unwrap().signal_ready(&module); + module + } +} diff --git a/everestrs/everestrs/src/schema/interface.rs b/everestrs/everestrs/src/schema/interface.rs new file mode 100644 index 00000000..6d79317d --- /dev/null +++ b/everestrs/everestrs/src/schema/interface.rs @@ -0,0 +1,151 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::BTreeMap; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Interface { + pub description: String, + #[serde(default)] + pub cmds: BTreeMap, + #[serde(default)] + pub vars: BTreeMap, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Command { + pub description: String, + #[serde(default)] + pub arguments: BTreeMap, + pub result: Option, +} + +#[derive(Debug, Serialize)] +pub struct Variable { + pub description: Option, + pub arg: Argument, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Argument { + Single(Type), + Multiple(Vec), +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NumberOptions { + pub minimum: Option, + pub maximum: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct IntegerOptions { + pub minimum: Option, + pub maximum: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ArrayOptions { + pub min_items: Option, + pub max_items: Option, + pub items: Option>, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ObjectOptions { + #[serde(default)] + pub properties: BTreeMap, + + #[serde(default)] + pub required: Vec, + + #[serde(default)] + pub additional_properties: bool, + + #[serde(rename = "$ref")] + pub object_reference: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum StringFormat { + #[serde(rename = "date-time")] + DateTime, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StringOptions { + pub pattern: Option, + pub format: Option, + pub max_length: Option, + pub min_length: Option, + + #[serde(rename = "enum")] + pub enum_items: Option>, + + #[serde(rename = "$ref")] + pub object_reference: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", tag = "type")] +pub enum Type { + Null, + Boolean, + String(StringOptions), + Number(NumberOptions), + Integer(IntegerOptions), + Array(ArrayOptions), + Object(ObjectOptions), +} + +impl<'de> Deserialize<'de> for Variable { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let serde_json::Value::Object(mut map) = Deserialize::deserialize(deserializer)? else { +return Err(serde::de::Error::custom("Variable must be a mapping")); +}; + + let description: Option = match map.remove("description") { + None => None, + Some(v) => Some( + serde_json::from_value(v) + .map_err(|_| serde::de::Error::custom("'description' is not a String'"))?, + ), + }; + + let arg_type = map + .remove("type") + .ok_or(serde::de::Error::custom("Missing 'type'"))?; + + let arg = match arg_type { + val @ serde_json::Value::String(_) => { + map.insert("type".to_string(), val); + let t: Type = serde_json::from_value(serde_json::Value::Object(map)) + .map_err(|e| serde::de::Error::custom(e.to_string()))?; + Argument::Single(t) + } + serde_json::Value::Array(s) => { + let mut types = Vec::new(); + for t in s.into_iter() { + let mut mapping = serde_json::Map::new(); + mapping.insert("type".to_string(), t); + let t: Type = serde_json::from_value(serde_json::Value::Object(mapping)) + .map_err(|e| serde::de::Error::custom(e.to_string()))?; + types.push(t); + } + Argument::Multiple(types) + } + _ => { + return Err(serde::de::Error::custom( + "'type' must be a sequence or a string.", + )) + } + }; + + Ok(Variable { description, arg }) + } +} diff --git a/everestrs/everestrs/src/schema/manifest.rs b/everestrs/everestrs/src/schema/manifest.rs new file mode 100644 index 00000000..939b966a --- /dev/null +++ b/everestrs/everestrs/src/schema/manifest.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; +use std::collections::BTreeMap; + +#[derive(Debug, Deserialize)] +pub struct Manifest { + pub description: String, + pub provides: BTreeMap, + pub metadata: Metadata, +} + +#[derive(Debug, Deserialize)] +pub struct YamlData { + pub description: String, +} + +#[derive(Debug, Deserialize)] +pub struct ProvidesEntry { + pub interface: String, + pub description: String, +} + +#[derive(Debug, Deserialize)] +pub struct Metadata { + pub license: String, + pub authors: Vec, +} diff --git a/everestrs/everestrs/src/schema/mod.rs b/everestrs/everestrs/src/schema/mod.rs new file mode 100644 index 00000000..168d4eb3 --- /dev/null +++ b/everestrs/everestrs/src/schema/mod.rs @@ -0,0 +1,16 @@ +pub mod interface; +pub mod manifest; + +// We ignore unknown fields everywhere, because the linting and verification of the YAMLs is done +// by libframework.so. So we only mention the fields we actually care about. + +pub use interface::Interface; +pub use manifest::Manifest; +use serde::Deserialize; +use std::collections::BTreeMap; + +#[derive(Debug, Deserialize)] +pub struct DataTypes { + pub description: String, + pub types: BTreeMap, +} diff --git a/everestrs/everestrs_sys/everestrs_sys.cpp b/everestrs/everestrs_sys/everestrs_sys.cpp new file mode 100644 index 00000000..f553efed --- /dev/null +++ b/everestrs/everestrs_sys/everestrs_sys.cpp @@ -0,0 +1,71 @@ +#include "everestrs_sys.hpp" + +#include +#include + +#include "everestrs/lib.rs.h" + +namespace { + +std::unique_ptr create_everest_instance(const std::string& module_id, + const Everest::RuntimeSettings& rs, + const Everest::Config& config) { + return std::make_unique(module_id, config, true /* FIXME */, rs.mqtt_broker_host, + rs.mqtt_broker_port, rs.mqtt_everest_prefix, rs.mqtt_external_prefix, + rs.telemetry_prefix, rs.telemetry_enabled); +} + +std::unique_ptr create_config_instance(const Everest::RuntimeSettings& rs) { + // FIXME (aw): where to initialize the logger? + Everest::Logging::init(rs.logging_config_file); + return std::make_unique(rs.schemas_dir.string(), rs.config_file.string(), rs.modules_dir.string(), + rs.interfaces_dir.string(), rs.types_dir.string(), rs.mqtt_everest_prefix, + rs.mqtt_external_prefix); +} + +JsonBlob json2blob(const json& j) { + // I did not find a way to not copy the data at least once here. + const std::string dumped = j.dump(); + rust::Vec vec; + vec.reserve(dumped.size()); + std::copy(dumped.begin(), dumped.end(), std::back_inserter(vec)); + return JsonBlob{vec}; +} + +} // namespace + +Module::Module(const std::string& module_id, const std::string& prefix, const std::string& config_file) : + module_id_(module_id), + rs_(prefix, config_file), + config_(create_config_instance(rs_)), + handle_(create_everest_instance(module_id, rs_, *config_)) { +} + +JsonBlob Module::get_interface(rust::Str interface_name) const { + const auto& interface_def = config_->get_interface_definition(std::string(interface_name)); + return json2blob(interface_def); +} + +JsonBlob Module::initialize() { + handle_->connect(); + handle_->spawn_main_loop_thread(); + + const std::string& module_name = config_->get_main_config().at(module_id_).at("module"); + return json2blob(config_->get_manifests().at(module_name)); +} + +void Module::signal_ready(const Runtime& rt) const { + handle_->register_on_ready_handler([&rt]() { rt.on_ready(); }); + handle_->signal_ready(); +} + +void Module::provide_command(const Runtime& rt, const CommandMeta& meta) const { + handle_->provide_cmd(std::string(meta.implementation_id), std::string(meta.name), [&rt, meta](json args) { + JsonBlob blob = rt.handle_command(meta, json2blob(args)); + return json::parse(blob.data.begin(), blob.data.end()); + }); +} + +std::unique_ptr create_module(rust::Str module_id, rust::Str prefix, rust::Str conf) { + return std::make_unique(std::string(module_id), std::string(prefix), std::string(conf)); +} diff --git a/everestrs/everestrs_sys/everestrs_sys.hpp b/everestrs/everestrs_sys/everestrs_sys.hpp new file mode 100644 index 00000000..e2f40864 --- /dev/null +++ b/everestrs/everestrs_sys/everestrs_sys.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +#include "rust/cxx.h" + +struct CommandMeta; +struct JsonBlob; +struct Runtime; + +class Module { +public: + Module(const std::string& module_id, const std::string& prefix, const std::string& conf); + + JsonBlob initialize(); + JsonBlob get_interface(rust::Str interface_name) const; + + void signal_ready(const Runtime& rt) const; + void provide_command(const Runtime& rt, const CommandMeta& meta) const; + + // TODO(hrapp): Add call_command, publish_variable and subscribe_variable. + +private: + const std::string module_id_; + Everest::RuntimeSettings rs_; + std::unique_ptr config_; + std::unique_ptr handle_; +}; + +std::unique_ptr create_module(rust::Str module_name, rust::Str prefix, rust::Str conf);