diff options
| author | Christian Hopps <chopps@labn.net> | 2025-02-26 21:13:47 +0000 | 
|---|---|---|
| committer | Christian Hopps <chopps@labn.net> | 2025-03-03 12:14:22 +0000 | 
| commit | b4000e0ad7db49275cf3f592df3ceeb35021c296 (patch) | |
| tree | fd878567ad4e49aab9483c136ead4a77363ed8a2 | |
| parent | 38c8603ebe694e92ef223df3a2c0c4c7fef43c13 (diff) | |
rustlibd: rust daemon templatedev/rust-skel
Signed-off-by: Christian Hopps <chopps@labn.net>
| -rw-r--r-- | Makefile.am | 2 | ||||
| -rw-r--r-- | configure.ac | 41 | ||||
| -rw-r--r-- | doc/developer/index.rst | 1 | ||||
| -rw-r--r-- | doc/developer/rust-dev.rst | 177 | ||||
| -rw-r--r-- | lib/libfrr.h | 1 | ||||
| -rw-r--r-- | rustlibd/.gitignore | 6 | ||||
| -rw-r--r-- | rustlibd/Cargo.toml.in | 30 | ||||
| -rw-r--r-- | rustlibd/Makefile | 10 | ||||
| -rw-r--r-- | rustlibd/README.org | 21 | ||||
| -rw-r--r-- | rustlibd/build.rs.in | 94 | ||||
| -rw-r--r-- | rustlibd/c_shim.rs | 52 | ||||
| l--------- | rustlibd/frrutil.rs | 1 | ||||
| -rw-r--r-- | rustlibd/rustfmt.toml.default | 80 | ||||
| -rw-r--r-- | rustlibd/rustlib_lib.rs | 131 | ||||
| -rw-r--r-- | rustlibd/rustlib_main.c | 156 | ||||
| -rw-r--r-- | rustlibd/sandbox.rs | 29 | ||||
| -rw-r--r-- | rustlibd/subdir.am | 74 | ||||
| -rw-r--r-- | rustlibd/wrapper.h.in | 24 | 
18 files changed, 930 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am index 0ce716e345..c2d35fcb0d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/configure.ac b/configure.ac index 09e2d20c3a..cedfc8c0af 100644 --- a/configure.ac +++ b/configure.ac @@ -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"]) diff --git a/doc/developer/index.rst b/doc/developer/index.rst index bd794b11a8..2443633859 100644 --- a/doc/developer/index.rst +++ b/doc/developer/index.rst @@ -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 index 0000000000..f06f4ba54f --- /dev/null +++ b/doc/developer/rust-dev.rst @@ -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. diff --git a/lib/libfrr.h b/lib/libfrr.h index df537e2e3b..02c57859b8 100644 --- a/lib/libfrr.h +++ b/lib/libfrr.h @@ -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 index 0000000000..3185cdb06b --- /dev/null +++ b/rustlibd/.gitignore @@ -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 index 0000000000..16baf26d2f --- /dev/null +++ b/rustlibd/Cargo.toml.in @@ -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 index 0000000000..631b7e7b72 --- /dev/null +++ b/rustlibd/Makefile @@ -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 index 0000000000..d30991b7f3 --- /dev/null +++ b/rustlibd/README.org @@ -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 index 0000000000..9d5741e461 --- /dev/null +++ b/rustlibd/build.rs.in @@ -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 index 0000000000..cd59c76c04 --- /dev/null +++ b/rustlibd/c_shim.rs @@ -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 index 0000000000..55797a75ff --- /dev/null +++ b/rustlibd/frrutil.rs @@ -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 index 0000000000..58fba6a6e8 --- /dev/null +++ b/rustlibd/rustfmt.toml.default @@ -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 index 0000000000..363d801c20 --- /dev/null +++ b/rustlibd/rustlib_lib.rs @@ -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 index 0000000000..eb129a606a --- /dev/null +++ b/rustlibd/rustlib_main.c @@ -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 index 0000000000..ca8a3c5ae4 --- /dev/null +++ b/rustlibd/sandbox.rs @@ -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 index 0000000000..af606b3133 --- /dev/null +++ b/rustlibd/subdir.am @@ -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 index 0000000000..1aaca7f509 --- /dev/null +++ b/rustlibd/wrapper.h.in @@ -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>  | 
