]> git.puffer.fish Git - matthieu/frr.git/commitdiff
rustlibd: rust daemon template dev/rust-skel
authorChristian Hopps <chopps@labn.net>
Wed, 26 Feb 2025 21:13:47 +0000 (21:13 +0000)
committerChristian Hopps <chopps@labn.net>
Mon, 3 Mar 2025 12:14:22 +0000 (12:14 +0000)
Signed-off-by: Christian Hopps <chopps@labn.net>
18 files changed:
Makefile.am
configure.ac
doc/developer/index.rst
doc/developer/rust-dev.rst [new file with mode: 0644]
lib/libfrr.h
rustlibd/.gitignore [new file with mode: 0644]
rustlibd/Cargo.toml.in [new file with mode: 0644]
rustlibd/Makefile [new file with mode: 0644]
rustlibd/README.org [new file with mode: 0644]
rustlibd/build.rs.in [new file with mode: 0644]
rustlibd/c_shim.rs [new file with mode: 0644]
rustlibd/frrutil.rs [new symlink]
rustlibd/rustfmt.toml.default [new file with mode: 0644]
rustlibd/rustlib_lib.rs [new file with mode: 0644]
rustlibd/rustlib_main.c [new file with mode: 0644]
rustlibd/sandbox.rs [new file with mode: 0644]
rustlibd/subdir.am [new file with mode: 0644]
rustlibd/wrapper.h.in [new file with mode: 0644]

index 0ce716e345c7c02fbf492adbd55db368c44deab8..c2d35fcb0da627955fdb2c67fe2e6296b7f8b6ac 100644 (file)
@@ -185,6 +185,7 @@ include grpc/subdir.am
 include tools/subdir.am
 
 include mgmtd/subdir.am
+include rustlibd/subdir.am
 
 include bgpd/subdir.am
 include bgpd/rfp-example/librfp/subdir.am
@@ -285,6 +286,7 @@ EXTRA_DIST += \
        qpb/Makefile \
        ripd/Makefile \
        ripngd/Makefile \
+       rustlibd/Makefile \
        staticd/Makefile \
        tests/Makefile \
        tools/Makefile \
index 09e2d20c3a2cc08ca24849e7104aac2fc107562f..cedfc8c0af8b71569591678626e08a05b1d8e89a 100644 (file)
@@ -732,6 +732,8 @@ AC_ARG_ENABLE([mgmtd_local_validations],
   AS_HELP_STRING([--enable-mgmtd-local-validations], [dev: unimplemented local validation]))
 AC_ARG_ENABLE([mgmtd_test_be_client],
   AS_HELP_STRING([--enable-mgmtd-test-be-client], [build test backend client]))
+AC_ARG_ENABLE([rustlibd],
+  AS_HELP_STRING([--enable-rustlibd], [enable rust library based daemon template]))
 AC_ARG_ENABLE([fpm_listener],
   AS_HELP_STRING([--enable-fpm-listener], [build fpm listener test program]))
 AC_ARG_ENABLE([ripd],
@@ -1872,6 +1874,10 @@ AS_IF([test "$enable_ripngd" != "no"], [
   AC_DEFINE([HAVE_RIPNGD], [1], [ripngd])
 ])
 
+AS_IF([test "$enable_rustlibd" != "no"], [
+  AC_DEFINE([HAVE_RUSTLIBD], [1], [rustlibd])
+])
+
 AS_IF([test "$enable_ospfd" != "no"], [
   AC_DEFINE([HAVE_OSPFD], [1], [ospfd])
 ])
@@ -2113,6 +2119,40 @@ if test "$enable_config_rollbacks" = "yes"; then
   ])
 fi
 
+dnl ------------------------------------------------------
+dnl rust general (add to conditional any new rust daemons)
+dnl ------------------------------------------------------
+if test "$enable_rustlibd" = "yes"; then
+  AC_PATH_PROG([CARGO], [cargo], [notfound])
+  AS_IF([test "$CARGO" = "notfound"], [AC_MSG_ERROR([cargo is required])])
+
+  AC_PATH_PROG([RUSTC], [rustc], [notfound])
+  AS_IF([test "$RUSTC" = "notfound"], [AC_MSG_ERROR([rustc is required])])
+
+  if test "$enable_dev_build" = "yes"; then
+    CARGO_TARGET_DIR=debug
+  else
+    CARGO_TARGET_DIR=release
+  fi
+  AC_SUBST(CARGO_TARGET_DIR)
+fi
+
+dnl ---------------
+dnl rustlibd
+dnl ---------------
+if test "$enable_rustlibd" = "yes"; then
+  AC_CONFIG_FILES([rustlibd/build.rs rustlibd/wrapper.h rustlibd/Cargo.toml])
+
+  AC_CONFIG_COMMANDS([gen-dot-cargo-config], [
+    if test "$ac_abs_top_builddir" != "$ac_abs_top_srcdir"; then
+      mkdir -p ${srcdir}/rustlibd/.cargo
+      if ! test -e "${srcdir}/rustlibd/.cargo/config.toml"; then
+        printf '[[build]]\ntarget-dir = "%s"\n' "${ac_abs_top_builddir}/rustlibd/target" > "${srcdir}/rustlibd/.cargo/config.toml"
+      fi
+    fi]
+  )
+fi
+
 dnl ---------------
 dnl sysrepo
 dnl ---------------
@@ -2782,6 +2822,7 @@ AM_CONDITIONAL([ENABLE_BGP_VNC], [test "$enable_bgp_vnc" != "no"])
 AM_CONDITIONAL([BGP_BMP], [$bgpd_bmp])
 dnl northbound
 AM_CONDITIONAL([SQLITE3], [$SQLITE3])
+AM_CONDITIONAL([RUSTLIBD], [test "$enable_rustlibd" = "yes"])
 AM_CONDITIONAL([SYSREPO], [test "$enable_sysrepo" = "yes"])
 AM_CONDITIONAL([GRPC], [test "$enable_grpc" = "yes"])
 AM_CONDITIONAL([ZEROMQ], [test "$ZEROMQ" = "true"])
index bd794b11a8dfe8bf0d21444e04d885dc304e6170..24436338594c883ec8226113be39b7ecd44567d2 100644 (file)
@@ -23,4 +23,5 @@ FRRouting Developer's Guide
    path
    pceplib
    link-state
+   rust-dev
    northbound/northbound
diff --git a/doc/developer/rust-dev.rst b/doc/developer/rust-dev.rst
new file mode 100644 (file)
index 0000000..f06f4ba
--- /dev/null
@@ -0,0 +1,177 @@
+.. -*- coding: utf-8 -*-
+..
+.. SPDX-License-Identifier: GPL-2.0-or-later
+..
+.. February 26 2025, Christian Hopps <chopps@labn.net>
+..
+.. Copyright (c) 2025, LabN Consulting, L.L.C.
+..
+
+.. _rust_dev:
+
+Rust Development
+================
+
+Overview
+--------
+
+The FRR project has started adding support for daemons written in rust. The
+following sections document the infrastructure to support to-date. This is the
+initial approach of rust integration, we expect changes as best-practices within
+the community evolve.
+
+General Structure
+-----------------
+
+An example template of the general structure of a rust based daemon can be found
+in ``rustlib/`` sub-directory. The recommended structure so far is to use a C
+main file and function to drive initialization of the daemon calling out to rust
+at 3 critical points. The Rust code is then built as a static library and linked
+into the daemon. Rust bindings are built for ``libfrr`` and accessed through a
+c_shim sub-module. Here's the files and as of the time of this writing:
+
+.. code-block:: make
+
+rustlibd/
+    .gitignore
+    Cargo.toml.in
+    Makefile
+    README.org
+    build.rs.in
+    c_shim.rs
+    frrutil.rs (symlink)
+    rustlib_lib.rs
+    rustlib_main.c
+    sandbox.rs
+    subdir.am
+    wrapper.h.in
+
+:file:`frrutil.rs` is a symlink to :file:`../lib/frrutil.rs` kept here to keep
+various rust tools happy about files being inside or below the main source
+directory.
+
+
+NOTE: if you use a separate build dir (named `build` in the below example) and
+you want to have your development environment proper analyze code (e.g.,
+vs-code/emacs LSP mode) you should create an additional 2 symlinks and create a
+local :file:`Cargo.toml` file like so:
+
+.. code-block:: sh
+
+    cd frr/rustlibd
+    sed -e 's,@srcdir@/,,g' < Cargo.toml.in > Cargo.toml
+    ln -s ../build/rustlibd/build.rs .
+    ln -s ../build/rustlibd/wrapper.h .
+
+Logging
+-------
+
+FRR logging is transparently supported using some bridging code that connects
+the native rust ``tracing`` calls directly to the ``zlog`` functionality in FRR.
+The only thing you have to do is call the function :func:`bridge_rust_logging`
+at startup. This is already done for you in the `rustlibd` template :func:`main`
+if you started with that code.
+
+.. code-block:: rust
+
+    use tracing::{debug, info};
+
+    fn myrustfunc(sval: &str, uval: u32) {
+       debug!("Some DEBUG level output of str value: {}", sval);
+       info!("Some INFO level output of uint value: {}", uval);
+    }
+
+Northbound Integration
+----------------------
+
+Support for the FRR northbound callback system is handled through rust macros.
+These rust macros define C shims which then call your rust functions which will
+use natural rust types. The rust macros hide the unsafe and tricky conversion
+code. You put pointers to the generated C shim functions into the
+:struct:`frr_yang_module_info` structure.
+
+NOTE: Locking will probably be important as your callbacks will be called in the
+FRR event loop main thread and your rust code is probably running in it's own
+different thread (perhaps using the tokio async runtime as setup in the
+:file:`rustlibd` template).
+
+Here's an example of defining a handler for a config leave value `enable`:
+
+.. code-block:: C
+
+    const struct frr_yang_module_info frr_my_module_nb_info = {
+       .name = "frr-my-module",
+       .nodes = {
+               {
+                       .xpath = "/frr-my-module:lib/bvalue",
+                       .cbs = {
+                               .modify = my_module_bvalue_modify_shim,
+                               .destroy = my_module_bvalue_destroy_shim
+                       }
+               },
+                ...
+
+.. code-block:: rust
+
+    use crate::{define_nb_destroy_shim, define_nb_modify_shim};
+
+    pub(crate) fn my_module_bvalue_modify(
+        event: NbEvent,
+        _node: &DataNodeRef,
+    ) -> Result<(), nb_error> {
+        debug!("RUST: bvalue modify: {}", event);
+        match event {
+            NbEvent::APPLY(_) => {
+                // handle the change to the `bvalue` leaf.
+                Ok(())
+            },
+            _ => Ok(()), // All other events just return Ok.
+        }
+    }
+
+    pub(crate) fn my_module_bvalue_destroy(
+        event: NbEvent,
+        _node: &DataNodeRef,
+    ) -> Result<(), nb_error> {
+        // handle the removal of the `bvalue` leaf.
+        // ...
+    }
+
+    define_nb_modify_shim!(
+        my_module_bvalue_modify_shim,
+        my_module_bvalue_modify);
+
+    define_nb_destroy_shim!(
+        my_module_bvalue_destroy_shim,
+        my_module_bvalue_destroy);
+
+
+CLI commands
+~~~~~~~~~~~~
+
+For CLI commands you should continue to write the DEFPY_YANG() calls in C which
+simply set your YANG config data base on the args to DEFPY_YANG(). The actual
+configuration will be handled in your rust based callbacks you defined for your
+YANG model that are describe above.
+
+Operational State
+~~~~~~~~~~~~~~~~~
+
+You have 2 choices with operation state. You can implement the operation state
+callbacks in rust and use the rust macros to bridge these to the
+:struct:`frr_yang_module_info` definition as you did with your config handlers, or you
+can keep your operational state in a ``yang-rs`` (i.e., ``libyang``) based tree.
+Here's an example of using the macros:
+
+If you choose to do the latter and save all your operational state in a
+``libyang`` :struct:`DataTree`, you only need to define 2 callback functions, a
+:func:`get_tree_locked()` function which returns the :struct:`DataTree` in a
+:struct:`MutexGuard` (i.e., a held lock), and an :func:`unlock_tree()` function
+which is passed back the :struct:`MutexGuard` object for unlocking. You use 2
+macros: :func:`define_nb_get_tree_locked`, and :func:`define_nb_unlock_tree` to
+create the C based shims to plug into your :struct:`frr_yang_module_info`
+structure.
+
+NOTE: As with config, locking will probably be important as your callbacks will
+be called in the FRR event loop main thread and your rust code is probably
+running in it's own different thread.
index df537e2e3b43593c2d68634bb31033813beb8245..02c57859b8a12c24ef5e5355a53cf3ebc3537499 100644 (file)
@@ -101,6 +101,7 @@ DECLARE_DLIST(log_args, struct log_arg, itm);
 #define PATH_VTY_PORT 2621
 #define PIM6D_VTY_PORT 2622
 #define MGMTD_VTY_PORT 2623
+#define RUSTLIBD_VTY_PORT 2635 /* Move this value not actual daemons if needed */
 /* Registry of daemons' port defaults */
 
 enum frr_cli_mode {
diff --git a/rustlibd/.gitignore b/rustlibd/.gitignore
new file mode 100644 (file)
index 0000000..3185cdb
--- /dev/null
@@ -0,0 +1,6 @@
+/.cargo
+/build.rs
+/target
+/wrapper.h
+/Cargo.lock
+/Cargo.toml
diff --git a/rustlibd/Cargo.toml.in b/rustlibd/Cargo.toml.in
new file mode 100644 (file)
index 0000000..16baf26
--- /dev/null
@@ -0,0 +1,30 @@
+[package]
+name = "rustlibd"
+version = "0.1.0"
+edition = "2021"
+default-run = "sandbox"
+
+[[bin]]
+name = "sandbox"
+path = "@srcdir@/sandbox.rs"
+test = false
+bench = false
+
+[lib]
+name = "rustlibd"
+path = "@srcdir@/rustlib_lib.rs"
+crate-type = ["staticlib"]
+test = true
+bench = false
+
+[dependencies]
+lazy_static = "1.5.0"
+libc = "0.2.169"
+libyang = { package = "libyang3-sys", version = "0.2.0" }
+tokio = { version = "1.38.0", features = ["full"]}
+tracing = "0.1.40"
+tracing-subscriber = { version = "0.3.18", features = ["env-filter"]}
+yang = { package = "yang3", version = "0.13.0" }
+
+[build-dependencies]
+bindgen = "0.71.1"
\ No newline at end of file
diff --git a/rustlibd/Makefile b/rustlibd/Makefile
new file mode 100644 (file)
index 0000000..631b7e7
--- /dev/null
@@ -0,0 +1,10 @@
+all: ALWAYS
+       @$(MAKE) -s -C .. rustlibd/rustlibd
+%: ALWAYS
+       @$(MAKE) -s -C .. rustlibd/$@
+
+Makefile:
+       #nothing
+ALWAYS:
+.PHONY: ALWAYS makefiles
+.SUFFIXES:
diff --git a/rustlibd/README.org b/rustlibd/README.org
new file mode 100644 (file)
index 0000000..d30991b
--- /dev/null
@@ -0,0 +1,21 @@
+* Rust Library Based Daemon Skeleton
+
+This is the skeleton for a rust library based FRR daemon.
+
+** Development
+*** Editor/LSP support
+
+In order for LSP support to work you will likely need to do a couple things if
+you use a build directory.
+
+- Create 2 symlinks from the generated files in ~BUILDDIR/rustlibd~ for
+   `build.rs` and `wrapper.h` e.g. if your build dir is in `frr/build`, and
+  create a `Cargo.toml` in the source directory that points to itself. Example
+  follows:
+
+  #+begin_src bash
+    cd frr/rustlibd
+    ln -s ../build/rustlibd/build.rs .
+    ln -s ../build/rustlibd/wrapper.h .
+    sed -e 's,@srcdir@/,,g' < Cargo.toml.in > Cargo.toml
+  #+end_src
diff --git a/rustlibd/build.rs.in b/rustlibd/build.rs.in
new file mode 100644 (file)
index 0000000..9d5741e
--- /dev/null
@@ -0,0 +1,94 @@
+extern crate bindgen;
+// extern crate gcc;
+
+use std::env;
+use std::path::PathBuf;
+
+use bindgen::callbacks::{MacroParsingBehavior, ParseCallbacks};
+use std::sync::{Arc, RwLock};
+
+use std::collections::HashSet;
+
+//
+// This complexity is need to ignore a #define in netdb.h that is shadowing an
+// anonymous enum in netinet/in.h
+//
+#[derive(Debug)]
+struct MacroCallback {
+    macros: Arc<RwLock<HashSet<String>>>,
+}
+
+impl ParseCallbacks for MacroCallback {
+    fn will_parse_macro(&self, name: &str) -> MacroParsingBehavior {
+        self.macros.write().unwrap().insert(name.into());
+
+        if name == "IPPORT_RESERVED" {
+            return MacroParsingBehavior::Ignore;
+        }
+
+        MacroParsingBehavior::Default
+    }
+}
+
+fn main() {
+    // // Tell cargo to look for shared libraries in the specified directory
+    // println!("cargo:rustc-link-search=@abs_top_builddir@/lib/.libs/libfrr.so");
+    // // Tell cargo to tell rustc to link libfrr shared library.
+    // println!("cargo:rustc-link-lib=frr");
+
+    // Tell cargo to invalidate the built crate whenever the wrapper changes
+    // println!("cargo:rerun-if-changed=@abs_srcdir@/wrapper.h");
+    println!("cargo:rerun-if-changed=wrapper.h");
+
+    let macros = Arc::new(RwLock::new(HashSet::new()));
+
+    // The bindgen::Builder is the main entry point
+    // to bindgen, and lets you build up options for
+    // the resulting bindings.
+    let bindings = bindgen::Builder::default()
+        .clang_args(&[
+            "-I@abs_top_builddir@", // need to get this dynamically
+            "-I@abs_top_srcdir@",
+            "-I@abs_top_srcdir@/lib",
+            "-DHAVE_CONFIG_H",
+            "-D_ATOMIC_WANT_TYPEDEFS",
+        ])
+        // The input header we would like to generate
+        // bindings for.
+        // .header("@abs_srcdir@/wrapper.h")
+        .header("wrapper.h")
+        // Tell cargo to invalidate the built crate whenever any of the
+        // included header files changed.
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+        .parse_callbacks(Box::new(MacroCallback {
+            macros: macros.clone(),
+        }))
+        // Finish the builder and generate the bindings.
+        .blocklist_type("IPPORT_.*")
+        .blocklist_type("IPPORT_RESERVED")
+        // avoid creating bindings for things with u128 which doesn't yet have a
+        // stable FFI, for now.
+
+        .blocklist_function("lyd_eval_xpath4")
+        .blocklist_function("q[efg]cvt.*")
+        .blocklist_function("strto.*")
+        .blocklist_function("strfrom.*")
+
+        .generate()
+        // Unwrap the Result and panic on failure.
+        .expect("Unable to generate bindings");
+
+    // Write the bindings to the $OUT_DIR/bindings.rs file.
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("Couldn't write bindings!");
+
+    // cc::Build::new()
+    //     .include("@abs_top_builddir@")
+    //     .include("@abs_top_srcdir@")
+    //     .include("@abs_top_srcdir@/lib")
+    //     .define("HAVE_CONFIG_H", None)
+    //     .file("@abs_srcdir@/rustlib_main.c")
+    //     .compile("c_main_code");
+}
diff --git a/rustlibd/c_shim.rs b/rustlibd/c_shim.rs
new file mode 100644 (file)
index 0000000..cd59c76
--- /dev/null
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// September 22 2024, Christian Hopps <chopps@labn.net>
+//
+// Copyright (c) 2024, LabN Consulting, L.L.C.
+//
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+
+use std::ffi::c_void;
+
+struct ShimGlobals {
+    arg: *mut c_void,
+    runtime: tokio::runtime::Runtime,
+}
+
+#[no_mangle]
+pub extern "C" fn _rust_preinit(daemon: *mut frr_daemon_info) -> *mut c_void {
+    crate::rust_preinit(daemon)
+}
+
+#[no_mangle]
+pub extern "C" fn _rust_init(_master: *mut event_loop, arg: *mut c_void) -> *mut c_void {
+    crate::rust_init(arg)
+}
+
+#[no_mangle]
+pub extern "C" fn _rust_run(_master: *mut event_loop, arg: *mut c_void) -> *mut c_void {
+    let runtime = crate::get_runtime();
+
+    let mut globals = Box::new(ShimGlobals {
+        arg: crate::rust_init(arg),
+        runtime,
+    });
+
+    globals.arg = crate::rust_run(&mut globals.runtime, globals.arg);
+
+    Box::into_raw(globals) as *mut c_void
+}
+
+#[no_mangle]
+pub extern "C" fn _rust_fini(_master: *mut event_loop, arg: *mut c_void) {
+    let globals = unsafe { Box::from_raw(arg as *mut ShimGlobals) };
+
+    crate::rust_fini(globals.arg);
+
+    // Make this explicit, not really needed but makes it clear
+    drop(globals.runtime);
+}
diff --git a/rustlibd/frrutil.rs b/rustlibd/frrutil.rs
new file mode 120000 (symlink)
index 0000000..55797a7
--- /dev/null
@@ -0,0 +1 @@
+../lib/frrutil.rs
\ No newline at end of file
diff --git a/rustlibd/rustfmt.toml.default b/rustlibd/rustfmt.toml.default
new file mode 100644 (file)
index 0000000..58fba6a
--- /dev/null
@@ -0,0 +1,80 @@
+array_width = 60
+attr_fn_like_width = 70
+binop_separator = "Front"
+blank_lines_lower_bound = 0
+blank_lines_upper_bound = 1
+brace_style = "SameLineWhere"
+chain_width = 60
+color = "Auto"
+combine_control_expr = true
+comment_width = 80
+condense_wildcard_suffixes = false
+control_brace_style = "AlwaysSameLine"
+disable_all_formatting = false
+doc_comment_code_block_width = 100
+edition = "2015"
+emit_mode = "Files"
+empty_item_single_line = true
+enum_discrim_align_threshold = 0
+error_on_line_overflow = false
+error_on_unformatted = false
+fn_call_width = 60
+fn_params_layout = "Tall"
+fn_single_line = false
+force_explicit_abi = true
+force_multiline_blocks = false
+format_code_in_doc_comments = false
+format_generated_files = true
+format_macro_bodies = true
+format_macro_matchers = false
+format_strings = false
+generated_marker_line_search_limit = 5
+group_imports = "Preserve"
+hard_tabs = false
+hex_literal_case = "Preserve"
+ignore = []
+imports_granularity = "Preserve"
+imports_indent = "Block"
+imports_layout = "Mixed"
+indent_style = "Block"
+inline_attribute_width = 0
+make_backup = false
+match_arm_blocks = true
+match_arm_leading_pipes = "Never"
+match_block_trailing_comma = false
+max_width = 100
+merge_derives = true
+newline_style = "Auto"
+normalize_comments = false
+normalize_doc_attributes = false
+overflow_delimited_expr = false
+remove_nested_parens = true
+reorder_impl_items = false
+reorder_imports = true
+reorder_modules = true
+required_version = "1.8.0"
+short_array_element_width_threshold = 10
+show_parse_errors = true
+single_line_if_else_max_width = 50
+single_line_let_else_max_width = 50
+skip_children = false
+skip_macro_invocations = []
+space_after_colon = true
+space_before_colon = false
+spaces_around_ranges = false
+struct_field_align_threshold = 0
+struct_lit_single_line = true
+struct_lit_width = 18
+struct_variant_width = 35
+style_edition = "2015"
+tab_spaces = 4
+trailing_comma = "Vertical"
+trailing_semicolon = true
+type_punctuation_density = "Wide"
+unstable_features = false
+use_field_init_shorthand = false
+use_small_heuristics = "Default"
+use_try_shorthand = false
+version = "One"
+where_single_line = false
+wrap_comments = false
diff --git a/rustlibd/rustlib_lib.rs b/rustlibd/rustlib_lib.rs
new file mode 100644 (file)
index 0000000..363d801
--- /dev/null
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// September 9 2024, Christian Hopps <chopps@labn.net>
+//
+// Copyright (C) 2024 LabN Consulting, L.L.C.
+//
+
+pub mod c_shim;
+#[macro_use]
+pub mod frrutil;
+
+use std::ffi::c_void;
+use std::io::Result;
+use std::sync::OnceLock;
+
+use tokio::sync::mpsc::{channel, Receiver, Sender};
+use tracing::{debug, error};
+
+// -------
+// Globals
+// -------
+
+enum Command {
+    Quit,
+}
+
+static TX: OnceLock<Sender<Command>> = OnceLock::new();
+static RX: OnceLock<std::sync::Mutex<Receiver<Result<()>>>> = OnceLock::new();
+
+////
+/// Get an tokio runtime for async execution
+///
+/// This function should create a runtime for the daemon. The generic rust infra
+/// code will call it to create the runtime for the daemon. The runtime will be
+/// passed to `rust_run()` so that a main task (or whatever) can be started.
+fn get_runtime() -> tokio::runtime::Runtime {
+    tokio::runtime::Builder::new_multi_thread()
+        .on_thread_start(|| {
+            frrutil::frr_register_thread("tokio-worker-thread");
+            debug!("tokio thread started")
+        })
+        .on_thread_stop(|| debug!("tokio thread stop"))
+        .enable_all()
+        .build()
+        .unwrap()
+}
+
+// ===================================================
+// Initialization/Teardown Callbacks (from xxx_main.c)
+// ===================================================
+
+///
+/// Pre-init (called after frr_preinit()), returned value is passed to rust_init()
+fn rust_preinit(_daemon: *mut c_shim::frr_daemon_info) -> *mut c_void {
+    debug!("in rust_preinit");
+    std::ptr::null_mut()
+}
+
+///
+/// Pre-daemonize (called after frr_init()), returned value is passed to rust_run()
+fn rust_init(_preinit_val: *mut c_void) -> *mut c_void {
+    debug!("in rust_init");
+    std::ptr::null_mut()
+}
+
+///
+/// Called prior to entering the FRR event loop (frr_run())
+///
+/// This function should spawn a task into the tokio runtime to run the daemon
+fn rust_run(runtime: &mut tokio::runtime::Runtime, _init_val: *mut c_void) -> *mut c_void {
+    debug!("in rust_run");
+
+    //
+    // Setup command channel with async_main thread
+    //
+
+    let (tx, rx) = channel::<Command>(10);
+    let (result_tx, result_rx) = channel::<Result<()>>(10);
+    TX.set(tx.clone()).expect("only set once");
+    RX.set(std::sync::Mutex::new(result_rx))
+        .expect("only set once");
+
+    //
+    // Start running our main routine
+    //
+    runtime.spawn(async { async_main(rx, result_tx).await });
+    std::ptr::null_mut()
+}
+
+///
+/// FRR exiting callback
+///
+/// Do any cleanup prior to the rust runtime being dropped and all spawned tasks
+/// being joined/canceled.
+pub fn rust_fini(_run_val: *mut c_void) {
+    debug!("rust_fini: sending quit command to async_main");
+    if TX.get().unwrap().blocking_send(Command::Quit).is_err() {
+        return;
+    }
+
+    debug!("rust_fini: waiting on exit result from async_main");
+    if let Some(result) = RX.get().unwrap().lock().unwrap().blocking_recv() {
+        match result {
+            Ok(()) => debug!("Runtime quit successfully"),
+            Err(x) => error!("Failed to quit runtime: {}", x),
+        }
+    }
+}
+
+// ============================
+// Main Rust Execution Function
+// ============================
+
+///
+/// Main loop -- process commands from FRR
+async fn async_main(mut cmd_rx: Receiver<Command>, result_tx: Sender<Result<()>>) {
+    loop {
+        debug!("Waiting on cmd channel");
+        let cmd = match cmd_rx.recv().await {
+            None => continue,
+            Some(cmd) => cmd,
+        };
+        match cmd {
+            Command::Quit => {
+                debug!("Quit command");
+                let _ = result_tx.send(Ok(())).await;
+                return;
+            }
+        }
+    }
+}
diff --git a/rustlibd/rustlib_main.c b/rustlibd/rustlib_main.c
new file mode 100644 (file)
index 0000000..eb129a6
--- /dev/null
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * September 9 2024, Christian Hopps <chopps@labn.net>
+ *
+ * Copyright (c) 2024, LabN Consulting, L.L.C.
+ */
+
+#include <lib/libfrr.h>
+#include <lib/zebra.h>
+#include <lib/privs.h>
+#include <lib/version.h>
+#include "mgmt_be_client.h"
+
+extern void *_rust_preinit(struct frr_daemon_info *daemon);
+extern void *_rust_init(struct event_loop *master, void *arg);
+extern void *_rust_run(struct event_loop *master, void *arg);
+extern void _rust_fini(struct event_loop *master, void *arg);
+extern void bridge_rust_logging(void);
+extern struct frr_daemon_info *rust_get_daemon_info(void);
+static void sighup(void);
+static void sigint(void);
+static void sigusr1(void);
+
+struct event_loop *master;
+struct mgmt_be_client *mgmt_be_client;
+void *rust_fini_arg;
+
+static struct option __longopts[] = { { 0 } };
+
+static zebra_capabilities_t __caps_p[] = {ZCAP_NET_RAW, ZCAP_BIND, ZCAP_SYS_ADMIN};
+
+static struct zebra_privs_t __privs = {
+#if defined(FRR_USER)
+       .user = FRR_USER,
+#endif
+#if defined FRR_GROUP
+       .group = FRR_GROUP,
+#endif
+#ifdef VTY_GROUP
+       .vty_group = VTY_GROUP,
+#endif
+       .caps_p = __caps_p,
+       .cap_num_p = array_size(__caps_p),
+       .cap_num_i = 0
+};
+
+static struct frr_signal_t __signals[] = {
+       {
+               .signal = SIGHUP,
+               .handler = &sighup,
+       },
+       {
+               .signal = SIGUSR1,
+               .handler = &sigusr1,
+       },
+       {
+               .signal = SIGINT,
+               .handler = &sigint,
+       },
+       {
+               .signal = SIGTERM,
+               .handler = &sigint,
+       },
+};
+
+static const struct frr_yang_module_info *const __yang_modules[] = {};
+
+/* clang-format off */
+FRR_DAEMON_INFO(rustlibd, RUST,
+               .vty_port = RUSTLIBD_VTY_PORT,
+               .proghelp = "Implementation of the RUST daemon template.",
+
+               .signals = __signals,
+               .n_signals = array_size(__signals),
+
+               .privs = &__privs,
+
+               .yang_modules = __yang_modules,
+               .n_yang_modules = array_size(__yang_modules),
+
+               /* mgmtd will load the per-daemon config file now */
+               .flags = FRR_NO_SPLIT_CONFIG,
+       );
+/* clang-format on */
+
+struct frr_daemon_info *rust_get_daemon_info(void)
+{
+       return &rustlibd_di;
+}
+
+static void sighup(void)
+{
+       zlog_info("SIGHUP received and ignored");
+}
+
+static void sigint(void)
+{
+       zlog_notice("Terminating on signal");
+
+       nb_oper_cancel_all_walks();
+
+       _rust_fini(master, rust_fini_arg);
+
+       mgmt_be_client_destroy(mgmt_be_client);
+       mgmt_be_client = NULL;
+       frr_fini();
+       exit(0);
+}
+
+static void sigusr1(void)
+{
+       zlog_rotate();
+}
+
+/* Main routine of ripd. */
+int main(int argc, char **argv)
+{
+       void *rust_arg;
+
+       frr_preinit(&rustlibd_di, argc, argv);
+
+       bridge_rust_logging();
+
+       rust_arg = _rust_preinit(&rustlibd_di);
+
+       frr_opt_add("", __longopts, "");
+
+       /* Command line option parse. */
+       while (1) {
+               int opt;
+
+               opt = frr_getopt(argc, argv, NULL);
+               if (opt == EOF)
+                       break;
+
+               switch (opt) {
+               case 0:
+                       break;
+               default:
+                       frr_help_exit(1);
+               }
+       }
+
+       /* Prepare master thread. */
+       master = frr_init();
+       rust_arg = _rust_init(master, rust_arg);
+       mgmt_be_client = mgmt_be_client_create("rustlibd", NULL, 0, master);
+
+       frr_config_fork();
+
+       rust_fini_arg = _rust_run(master, rust_arg);
+       frr_run(master);
+
+       /* Not reached. */
+       return 0;
+}
diff --git a/rustlibd/sandbox.rs b/rustlibd/sandbox.rs
new file mode 100644 (file)
index 0000000..ca8a3c5
--- /dev/null
@@ -0,0 +1,29 @@
+// -*- coding: utf-8 -*-
+//
+// February 26 2025, Christian Hopps <chopps@labn.net>
+//
+// Copyright (c) 2025, LabN Consulting, L.L.C.
+//
+#![allow(clippy::disallowed_names)]
+
+// =======
+// HashMap
+// =======
+
+fn test_hashmap() {
+    let v: Vec<&str> = "foobar=1baz&&bf%2Clag".split('&').collect();
+    let v: Vec<&str> = v.into_iter().filter(|&x| !x.is_empty()).collect();
+    println!("HASHMAP: split: {:?}", v);
+
+    let qmap: HashMap<_, _> = v
+        .into_iter()
+        .map(|x| x.split_once('=').unwrap_or((x, "")))
+        .map(|(x, y)| (_percent_decode(x), _percent_decode(y)))
+        .map(|(x, y)| (String::from(x), String::from(y)))
+        .collect();
+    println!("HASHMAP: qmap: {:?}", qmap);
+}
+
+fn main() {
+    test_hashmap();
+}
diff --git a/rustlibd/subdir.am b/rustlibd/subdir.am
new file mode 100644 (file)
index 0000000..af606b3
--- /dev/null
@@ -0,0 +1,74 @@
+#
+# rustlibd -- RUSTLIBD Daemon
+#
+
+if RUSTLIBD
+noinst_LIBRARIES += rustlibd/librustlibd.a
+sbin_PROGRAMS += rustlibd/rustlibd
+# vtysh_daeons += rustlibd
+
+
+#
+# Files for copying as template:
+#
+# - xxx_lib.c :: Main rust functions for the daemon - replace these with your own
+# - xxx_nb.c :: Create from `tools/gen_northbound_calllbacks` function/struct defines
+# - xxx_nb.h :: Create from `tools/gen_northbound_calllbacks` prototype declares
+# - xxx_nb_{config,state}.rs :: Use `define_nb_*_shim()` rust macros to bridge
+#       your rust northbound callbacks from the C prototypes created by
+#       gen_northbound_calllbacks - remove the C versions of these functions
+#       from xxx_nb.c
+#
+# - build.rs :: Build script the rust daemon, only modify if you have build
+#       issues with C symbols.
+# - c_shim.rs :: no modify needed - exports FRR/C bindings and calls your rust
+#      functions.
+# - restonf_main.c :: C main(), requires minimal modifications (name, yang modules)
+# - wrapper.h.in :: Include C header files to create bindings under c_shim for your
+#       rust use, add/remove as needed.
+
+#
+# C build rules
+#
+rustlibd_rustlibd_SOURCES = rustlibd/rustlib_main.c # rustlibd/rustlib_nb.c
+rustlibd_rustlibd_LDADD = rustlibd/librustlibd.a lib/libfrr.la $(LIBCAP) -lm
+# noinst_HEADERS += rustlibd/rustlib_nb.h
+
+# Add any used YANG modules here to embed.
+# nodist_restconfd_restconfd_SOURCES = \
+#       yang/frr-rustlib.yang.c
+
+# clippy_scan += rustlibd/rustlib_cli.c
+
+#
+# rustlibd rust library build rules
+#
+EXTRA_DIST += rustlibd/Cargo.toml
+rustlibd_librustlibd_a_SOURCES = rustlibd/c_shim.rs \
+       lib/frrutil.rs \
+       rustlibd/rustlib_lib.rs
+
+if DEV_BUILD
+RUSTLIBD_BUILD_FLAG =
+RUSTLIBD_TARGET_DIR = debug
+else
+RUSTLIBD_BUILD_FLAG = --release
+RUSTLIBD_TARGET_DIR = release
+endif
+
+rustlibd/Cargo.lock: rustlibd/Cargo.toml
+       (cd rustlibd && cargo update $(RUSTLIBD_BUILD_FLAG))
+
+rustlibd/librustlibd.a: rustlibd/target/$(RUSTLIBD_TARGET_DIR)/librustlibd.a
+       @printf "  CP       rustlib/librustlibd.a\n"
+       @cp $< $@
+
+rustlibd/target/$(RUSTLIBD_TARGET_DIR)/librustlibd.a: $(rustlibd_librustlibd_a_SOURCES) rustlibd/Cargo.toml
+       @printf "  CARGO    $@\n"
+       (cd rustlibd && cargo +stable -q build --lib $(RUSTLIBD_BUILD_FLAG))
+
+clean-rustlibd:
+       (cd rustlibd && cargo clean)
+else
+clean-rustlibd:
+endif
diff --git a/rustlibd/wrapper.h.in b/rustlibd/wrapper.h.in
new file mode 100644 (file)
index 0000000..1aaca7f
--- /dev/null
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * September 9 2024, Christian Hopps <chopps@labn.net>
+ *
+ * Copyright (C) 2024 LabN Consulting, L.L.C.
+ */
+#include "config.h"
+/* #include "typesafe.h" */
+/* #include "frratomic.h" */
+/* #include "sigevent.h" */
+/* #include "privs.h" */
+
+/* #include "log.h" */
+/* #include "getopt.h" */
+/* #include "module.h" */
+/* #include "hook.h" */
+/* #include "northbound.h" */
+
+#include <lib/zebra.h>
+
+#include <lib/frr_pthread.h>
+#include <lib/libfrr.h>
+#include <lib/log.h>
+#include <lib/mgmt_msg_native.h>