Create a Rust API for part of //aos:configuration

Change-Id: I00044634f437e72487aaaca28e9a00fe65a4aa48
Signed-off-by: Brian Silverman <bsilver16384@gmail.com>
diff --git a/aos/BUILD b/aos/BUILD
index 9394295..1380f7d 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -1,5 +1,6 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_py_library", "flatbuffer_rust_library", "flatbuffer_ts_library")
 load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+load("//tools/build_rules:autocxx.bzl", "autocxx_library")
 
 exports_files(["aos_dump_autocomplete.sh"])
 
@@ -222,6 +223,14 @@
     visibility = ["//visibility:public"],
 )
 
+flatbuffer_rust_library(
+    name = "configuration_rust_fbs",
+    srcs = ["configuration.fbs"],
+    crate_name = "aos_configuration_fbs",
+    target_compatible_with = ["//tools/platforms/rust:has_support"],
+    visibility = ["//visibility:public"],
+)
+
 cc_library(
     name = "configuration",
     srcs = [
@@ -246,6 +255,51 @@
     ],
 )
 
+cc_library(
+    name = "configuration_for_rust",
+    srcs = [
+        "configuration_for_rust.cc",
+    ],
+    hdrs = [
+        "configuration_for_rust.h",
+    ],
+    deps = [
+        ":configuration",
+        ":for_rust",
+        "//third_party/cargo:cxx_cc",
+    ],
+)
+
+autocxx_library(
+    name = "configuration_rs",
+    srcs = ["configuration.rs"],
+    crate_name = "aos_configuration",
+    libs = [
+        ":configuration",
+        ":configuration_for_rust",
+        ":configuration_fbs",
+    ],
+    override_cc_toolchain = "@llvm_toolchain//:cc-clang-x86_64-linux",
+    target_compatible_with = ["//tools/platforms/rust:has_support"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":configuration_rust_fbs",
+        ":flatbuffers_rs",
+        "//third_party/cargo:thiserror",
+    ],
+)
+
+rust_test(
+    name = "configuration_rs_test",
+    crate = ":configuration_rs",
+    data = [
+        "//aos/testdata:test_configs",
+    ],
+    # TODO: Make Rust play happy with pic vs nopic. Details at:
+    # https://github.com/bazelbuild/rules_rust/issues/118
+    rustc_flags = ["-Crelocation-model=static"],
+)
+
 flatbuffer_ts_library(
     name = "json_to_flatbuffer_fbs_ts",
     srcs = ["json_to_flatbuffer.fbs"],
@@ -637,3 +691,14 @@
         "@com_github_google_glog//:glog",
     ],
 )
+
+cc_library(
+    name = "for_rust",
+    hdrs = [
+        "for_rust.h",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//third_party/cargo:cxx_cc",
+    ],
+)
diff --git a/aos/configuration.rs b/aos/configuration.rs
new file mode 100644
index 0000000..479a065
--- /dev/null
+++ b/aos/configuration.rs
@@ -0,0 +1,121 @@
+use std::path::Path;
+
+use thiserror::Error;
+
+use aos_configuration_fbs::aos::Configuration as RustConfiguration;
+use aos_flatbuffers::{Flatbuffer, NonSizePrefixedFlatbuffer};
+
+autocxx::include_cpp! (
+#include "aos/configuration.h"
+#include "aos/configuration_for_rust.h"
+#include "aos/configuration_generated.h"
+
+safety!(unsafe)
+
+generate!("aos::Configuration")
+generate!("aos::Channel")
+generate!("aos::Node")
+block!("flatbuffers::String")
+block!("flatbuffers::Verifier")
+
+generate!("aos::configuration::GetChannelForRust")
+);
+
+#[cxx::bridge]
+mod ffi2 {
+    #[namespace = "aos::configuration"]
+    unsafe extern "C++" {
+        include!("aos/configuration_for_rust.h");
+        fn MaybeReadConfigForRust(path: &str, extra_import_paths: &[&str]) -> Vec<u8>;
+    }
+}
+
+pub use ffi::aos::{Channel, Configuration, Node};
+
+#[derive(Clone, Copy, Eq, PartialEq, Debug, Error)]
+pub enum ChannelLookupError {
+    #[error("channel not found")]
+    NotFound,
+}
+
+#[derive(Clone, Copy, Eq, PartialEq, Debug, Error)]
+pub enum ReadConfigError {
+    #[error("duplicate imports or invalid paths")]
+    ReadFailed,
+}
+
+impl Configuration {
+    pub fn get_channel(
+        &self,
+        name: &str,
+        typename: &str,
+        application_name: &str,
+        node: &Node,
+    ) -> Result<&Channel, ChannelLookupError> {
+        // SAFETY: All the input references are valid pointers, and we're not doing anything with
+        // the result yet. It doesn't store any of the input references.
+        let channel = unsafe {
+            ffi::aos::configuration::GetChannelForRust(self, name, typename, application_name, node)
+        };
+        if channel.is_null() {
+            Err(ChannelLookupError::NotFound)
+        } else {
+            // SAFETY: We know this is a valid pointer now, and we're returning it with the lifetime
+            // inherited from `configuration` which owns it.
+            Ok(unsafe { &*channel })
+        }
+    }
+}
+
+/// # Panics
+///
+/// `path` must be valid UTF-8.
+pub fn read_config_from(
+    path: &Path,
+) -> Result<impl Flatbuffer<RustConfiguration<'static>>, ReadConfigError> {
+    read_config_from_import_paths(path, &[])
+}
+
+/// # Panics
+///
+/// `path` and all members of `extra_import_paths` must be valid UTF-8.
+pub fn read_config_from_import_paths(
+    path: &Path,
+    extra_import_paths: &[&Path],
+) -> Result<impl Flatbuffer<RustConfiguration<'static>>, ReadConfigError> {
+    let extra_import_paths: Vec<_> = extra_import_paths
+        .iter()
+        .map(|p| p.to_str().expect("Paths must be UTF-8"))
+        .collect();
+    let buffer = ffi2::MaybeReadConfigForRust(
+        path.to_str().expect("Paths must be UTF-8"),
+        &extra_import_paths,
+    );
+    if buffer.is_empty() {
+        return Err(ReadConfigError::ReadFailed);
+    }
+    // SAFETY: The C++ code returns a valid flatbuffer (unless it returned an error, which we
+    // checked above).
+    return Ok(unsafe { NonSizePrefixedFlatbuffer::new_unchecked(buffer) });
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn read_config() {
+        let config = read_config_from(Path::new("aos/testdata/config1.json")).unwrap();
+        assert!(
+            config
+                .message()
+                .channels()
+                .unwrap()
+                .iter()
+                .find(|channel| channel.type_() == Some(".aos.bar"))
+                .is_some(),
+            "Failed to find the .aos.bar channel: {:?}",
+            config.message()
+        );
+    }
+}
diff --git a/aos/configuration_for_rust.cc b/aos/configuration_for_rust.cc
new file mode 100644
index 0000000..3c3daad
--- /dev/null
+++ b/aos/configuration_for_rust.cc
@@ -0,0 +1,32 @@
+#include "aos/configuration_for_rust.h"
+
+#include "aos/for_rust.h"
+
+namespace aos::configuration {
+
+const Channel *GetChannelForRust(const Configuration *config, rust::Str name,
+                                 rust::Str type, rust::Str application_name,
+                                 const Node *node) {
+  return GetChannel(config, RustStrToStringView(name),
+                    RustStrToStringView(type),
+                    RustStrToStringView(application_name), node);
+}
+
+rust::Vec<uint8_t> MaybeReadConfigForRust(
+    rust::Str path, rust::Slice<const rust::Str> extra_import_paths) {
+  std::vector<std::string_view> cc_extra_import_paths;
+  for (const rust::Str &extra_import_path : extra_import_paths) {
+    cc_extra_import_paths.push_back(RustStrToStringView(extra_import_path));
+  }
+  const auto cc_result = MaybeReadConfig(RustStrToStringView(path));
+  rust::Vec<uint8_t> result;
+  if (cc_result) {
+    result.reserve(cc_result->span().size());
+    for (uint8_t b : cc_result->span()) {
+      result.push_back(b);
+    }
+  }
+  return result;
+}
+
+}  // namespace aos::configuration
diff --git a/aos/configuration_for_rust.h b/aos/configuration_for_rust.h
new file mode 100644
index 0000000..ae18346
--- /dev/null
+++ b/aos/configuration_for_rust.h
@@ -0,0 +1,21 @@
+#ifndef AOS_CONFIGURATION_FOR_RUST_H_
+#define AOS_CONFIGURATION_FOR_RUST_H_
+
+#include "aos/configuration.h"
+#include "cxx.h"
+
+namespace aos::configuration {
+
+const Channel *GetChannelForRust(const Configuration *config, rust::Str name,
+                                 rust::Str type, rust::Str application_name,
+                                 const Node *node);
+
+// Returns a Configuration flatbuffer. Returns an empty vector on errors.
+// TODO: It would be nice to return more detailed errors (not found vs could not
+// parse vs merging error).
+rust::Vec<uint8_t> MaybeReadConfigForRust(
+    rust::Str path, rust::Slice<const rust::Str> extra_import_paths);
+
+}  // namespace aos::configuration
+
+#endif  // AOS_CONFIGURATION_FOR_RUST_H_
diff --git a/aos/for_rust.h b/aos/for_rust.h
new file mode 100644
index 0000000..388a854
--- /dev/null
+++ b/aos/for_rust.h
@@ -0,0 +1,23 @@
+#ifndef AOS_FOR_RUST_H_
+#define AOS_FOR_RUST_H_
+
+// This file has some shared utilities for the for_rust C++ code, which provides
+// autocxx-friendly versions of C++ APIs.
+
+#include <string_view>
+
+#include "cxx.h"
+
+namespace aos {
+
+inline rust::Str StringViewToRustStr(std::string_view s) {
+  return rust::Str(s.data(), s.size());
+}
+
+inline std::string_view RustStrToStringView(rust::Str s) {
+  return std::string_view(s.data(), s.size());
+}
+
+}  // namespace aos
+
+#endif  // AOS_FOR_RUST_H_