From: Christian Hopps Date: Wed, 26 Feb 2025 21:13:47 +0000 (+0000) Subject: rustlibd: rust daemon template X-Git-Url: https://git.puffer.fish/?a=commitdiff_plain;h=b4000e0ad7db49275cf3f592df3ceeb35021c296;p=matthieu%2Ffrr.git rustlibd: rust daemon template Signed-off-by: Christian Hopps --- 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 +.. +.. 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>>, +} + +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 +// +// 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 +// +// 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> = OnceLock::new(); +static RX: OnceLock>>> = 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::(10); + let (result_tx, result_rx) = channel::>(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, result_tx: Sender>) { + 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 + * + * Copyright (c) 2024, LabN Consulting, L.L.C. + */ + +#include +#include +#include +#include +#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 +// +// 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 + * + * 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 + +#include +#include +#include +#include