Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define a trait for Plugins and allow them to be dynamically loaded or statically linked #114

Merged
merged 19 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"zenoh",
"zenoh-util",
"zenoh-ext",
"plugins/zenoh-plugin-trait",
"plugins/example-plugin",
"plugins/zenoh-plugin-rest",
"plugins/zenoh-plugin-storages",
Expand Down
14 changes: 9 additions & 5 deletions plugins/example-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
[package]
name = "zplugin-example"
version = "0.5.0-dev"
authors = ["kydos <angelo@icorsaro.net>",
"Julien Enoch <julien@enoch.fr>",
"Olivier Hécart <olivier.hecart@adlinktech.com>",
"Luca Cominardi <luca.cominardi@adlinktech.com>"]
authors = [
"kydos <angelo@icorsaro.net>",
"Julien Enoch <julien@enoch.fr>",
"Olivier Hécart <olivier.hecart@adlinktech.com>",
"Luca Cominardi <luca.cominardi@adlinktech.com>",
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to add yourself! 😉

edition = "2018"

# NOTE: as this library name doesn't start with 'zplugin_' prefix
Expand All @@ -29,7 +31,9 @@ crate-type = ["cdylib"]


[dependencies]
zenoh = { path = "../../zenoh" }
zenoh = { path="../../zenoh" }
zenoh-util = { path="../../zenoh-util" }
zenoh-plugin-trait = { path="../zenoh-plugin-trait" }
futures = "0.3.12"
clap = "2"
log = "0.4"
Expand Down
68 changes: 56 additions & 12 deletions plugins/example-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,69 @@ use futures::select;
use log::{debug, info};
use runtime::Runtime;
use std::collections::HashMap;
use std::sync::{
atomic::{AtomicBool, Ordering::Relaxed},
Arc,
};
use zenoh::net::queryable::STORAGE;
use zenoh::net::utils::resource_name;
use zenoh::net::*;
use zenoh_plugin_trait::prelude::*;
use zenoh_util::zerror2;

#[no_mangle]
pub fn get_expected_args<'a, 'b>() -> Vec<Arg<'a, 'b>> {
vec![
Arg::from_usage("--storage-selector 'The selection of resources to be stored'")
.default_value("/demo/example/**"),
]
pub struct ExamplePlugin {
selector: ResKey,
}

#[no_mangle]
pub fn start(runtime: Runtime, args: &'static ArgMatches<'_>) {
async_std::task::spawn(run(runtime, args));
zenoh_plugin_trait::declare_plugin!(ExamplePlugin);

pub struct ExamplePluginStopper {
flag: Arc<AtomicBool>,
}

impl PluginStopper for ExamplePluginStopper {
fn stop(self) {
self.flag.store(false, Relaxed);
}
}

impl Plugin for ExamplePlugin {
fn compatibility() -> zenoh_plugin_trait::Compatibility {
zenoh_plugin_trait::Compatibility {
uid: "zenoh-example-plugin",
}
}

fn get_expected_args() -> Vec<Arg<'static, 'static>> {
vec![
Arg::from_usage("--storage-selector 'The selection of resources to be stored'")
.default_value("/demo/example/**"),
]
}

fn init(args: &ArgMatches) -> Result<Self, Box<dyn std::error::Error>> {
if let Some(selector) = args.value_of("storage-selector") {
Ok(ExamplePlugin {
selector: selector.into(),
})
} else {
Err(Box::new(zerror2!(ZErrorKind::Other {
descr: "storage-selector is a mandatory option for ExamplePlugin".into()
})))
}
}
}

impl PluginLaunch for ExamplePlugin {
fn start(self, runtime: Runtime) -> Box<dyn PluginStopper> {
let flag = Arc::new(AtomicBool::new(true));
let stopper = ExamplePluginStopper { flag: flag.clone() };
async_std::task::spawn(run(runtime, self.selector, flag));
Box::new(stopper)
}
}

async fn run(runtime: Runtime, args: &'static ArgMatches<'_>) {
async fn run(runtime: Runtime, selector: ResKey, flag: Arc<AtomicBool>) {
env_logger::init();

let session = Session::init(runtime, true, vec![], vec![]).await;
Expand All @@ -49,7 +94,6 @@ async fn run(runtime: Runtime, args: &'static ArgMatches<'_>) {
period: None,
};

let selector: ResKey = args.value_of("storage-selector").unwrap().into();
debug!("Run example-plugin with storage-selector={}", selector);

debug!("Declaring Subscriber on {}", selector);
Expand All @@ -61,7 +105,7 @@ async fn run(runtime: Runtime, args: &'static ArgMatches<'_>) {
debug!("Declaring Queryable on {}", selector);
let mut queryable = session.declare_queryable(&selector, STORAGE).await.unwrap();

loop {
while flag.load(Relaxed) {
select!(
sample = sub.receiver().next().fuse() => {
let sample = sample.unwrap();
Expand Down
35 changes: 35 additions & 0 deletions plugins/zenoh-plugin-trait/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#
# Copyright (c) 2017, 2020 ADLINK Technology Inc.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# ADLINK zenoh team, <zenoh@adlink-labs.tech>
#
[package]
name = "zenoh-plugin-trait"
version = "0.1.0-dev"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version should be the same as zenoh (0.5.0-dev) since we will release all together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, although I've removed zenoh when I realized it was causing a dependency cycle when I started working on integrating it into zenoh itself

authors = [
"kydos <angelo@icorsaro.net>",
"Julien Enoch <julien@enoch.fr>",
"Olivier Hécart <olivier.hecart@adlinktech.com>",
"Luca Cominardi <luca.cominardi@adlinktech.com>",
"Pierre Avital <pierre.avital@adlinktech.com>",
]
edition = "2018"

[lib]
name = "zenoh_plugin_trait"

[features]
no_mangle = []
default = ["no_mangle"]

[dependencies]
zenoh = { path="../../zenoh" }
clap = "2"
205 changes: 205 additions & 0 deletions plugins/zenoh-plugin-trait/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//
// Copyright (c) 2017, 2020 ADLINK Technology Inc.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
//
// Contributors:
// ADLINK zenoh team, <zenoh@adlink-labs.tech>
//

//! # The plugin infrastructure for Zenoh.
//!
//! To build a plugin, up to 2 types may be constructed :
//! * A [Plugin] type.
//! * [PluginLaunch::start] should be non-blocking, and return a boxed instance of your stoppage type, which should implement [PluginStopper].

use clap::{Arg, ArgMatches};
use std::error::Error;
use zenoh::net::runtime::Runtime;

pub mod prelude {
pub use crate::{dynamic_loading::*, Plugin, PluginLaunch, PluginStopper};
}

/// Your plugin's compatibility.
/// Currently, this should simply be the plugin crate's name.
/// This structure may evolve to include more detailed information.
#[derive(Clone, Debug, PartialEq)]
pub struct Compatibility {
pub uid: &'static str,
}

#[derive(Clone)]
pub struct Incompatibility {
pub own_compatibility: Compatibility,
pub conflicting_with: Compatibility,
pub details: Option<String>,
}

/// Zenoh plugins must implement [Plugin] and [PluginLaunch]
pub trait Plugin: PluginLaunch + Sized + 'static {
/// Returns this plugin's [Compatibility].
fn compatibility() -> Compatibility;

/// As Zenoh instanciates plugins, it will append their [Compatibility] to an array.
/// This array's current state will be shown to the next plugin.
///
/// To signal that your plugin is incompatible with a previously instanciated plugin, return `Err`,
/// Otherwise, return `Ok(Self::compatibility())`.
///
/// By default, a plugin is non-reentrant to avoir reinstanciation if its dlib is accessible despite it already being statically linked.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"avoir" => "avoid"

fn is_compatible_with(others: &[Compatibility]) -> Result<Compatibility, Incompatibility> {
let own_compatibility = Self::compatibility();
if others.iter().any(|c| c == &own_compatibility) {
let conflicting_with = own_compatibility.clone();
Err(Incompatibility {
own_compatibility,
conflicting_with,
details: None,
})
} else {
Ok(own_compatibility)
}
}

/// Returns the arguments that are required for the plugin's construction
fn get_expected_args() -> Vec<Arg<'static, 'static>>;

/// Constructs an instance of the plugin, which will be launched with [PluginLaunch::start]
fn init(args: &ArgMatches) -> Result<Self, Box<dyn Error>>;

fn box_init(args: &ArgMatches) -> Result<Box<dyn PluginLaunch>, Box<dyn Error>> {
match Self::init(args) {
Ok(v) => Ok(Box::new(v)),
Err(e) => Err(e),
}
}
}

/// Allows a [Plugin] instance to be started.
pub trait PluginLaunch {
fn start(self, runtime: Runtime) -> Box<dyn PluginStopper>;
}

/// Allows a [Plugin] instance to be stopped.
/// Typically, you can achieve this using a one-shot channel or an [AtomicBool](std::sync::atomic::AtomicBool).
/// If you don't want a stopping mechanism, you can use `()` as your [PluginStopper].
pub trait PluginStopper {
fn stop(self);
}

impl PluginStopper for () {
fn stop(self) {}
}

pub mod dynamic_loading {
use super::*;
pub use no_mangle::*;

type InitFn = fn(&ArgMatches) -> Result<Box<dyn PluginLaunch>, Box<dyn Error>>;
pub type PluginVTableVersion = u16;

/// This number should change any time the internal structure of [PluginVTable] changes
pub const PLUGIN_VTABLE_VERSION: PluginVTableVersion = 0;

#[repr(C)]
struct PluginVTableInner {
init: InitFn,
is_compatible_with: fn(&[Compatibility]) -> Result<Compatibility, Incompatibility>,
get_expected_args: fn() -> Vec<Arg<'static, 'static>>,
}

/// Automagical padding such that [PluginVTable::init]'s result is the size of a cache line
#[repr(C)]
struct PluginVTablePadding {
__padding: [u8; PADDING_LENGTH],
}
const PADDING_LENGTH: usize =
64 - std::mem::size_of::<Result<PluginVTableInner, PluginVTableVersion>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the PluginVTablePadding is used along PluginVTableInner within PluginVTable, I don't understand why its length depends on Result<PluginVTableInner, PluginVTableVersion> rather than just PluginVTableInner. Can you explain ?

Copy link
Contributor Author

@p-avital p-avital Jul 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit less "magic" to set the padding length as "a cache line size minus the size of the type that actually interests us".

Result should use an 8 bytes marker due to align_of::<PluginVTable>() being 8 (and the same as align_of::<PluginVTableInner>()), but that might change (for example if it finds a niche to exploit or realizes that the first bytes are non-null for Ok and null of Err in a few compiler updates), so I'd rather use the Result type to ensure that type has consistent size.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a crate that does that: https://docs.rs/cache-padded/1.1.1/cache_padded/. I suggest to use this crate that is already widely used.

impl PluginVTablePadding {
fn new() -> Self {
PluginVTablePadding {
__padding: [0; PADDING_LENGTH],
}
}
}

/// For use with dynamically loaded plugins. Its size will not change accross versions, but its internal structure might.
///
/// To ensure compatibility, its size and alignment must allow `size_of::<Result<PluginVTable, PluginVTableVersion>>() == 64` (one cache line).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size_of::<Result<PluginVTable, PluginVTableVersion>> or size_of::<Result<PluginVTableInner, PluginVTableVersion>> ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PluginVTable, the goal is for the size of the outer type to be fixed even if we change the size of the inner one (by adding more functions to the vtable for example).

The constant should let us know that the pointers may not point to the same things, but I'm afraid of what would happen if the load_plugin function returns something bigger (or smaller) than what is expected by the caller.

#[repr(C)]
pub struct PluginVTable {
inner: PluginVTableInner,
padding: PluginVTablePadding,
}

impl PluginVTable {
pub fn new<ConcretePlugin: Plugin + 'static>() -> Self {
PluginVTable {
inner: PluginVTableInner {
is_compatible_with: ConcretePlugin::is_compatible_with,
get_expected_args: ConcretePlugin::get_expected_args,
init: ConcretePlugin::box_init,
},
padding: PluginVTablePadding::new(),
}
}

/// Ensures [PluginVTable]'s size stays the same between versions
fn __size_check() {
unsafe {
std::mem::transmute::<_, [u8; 64]>(std::mem::MaybeUninit::<
Result<Self, PluginVTableVersion>,
>::uninit())
};
}

pub fn is_compatible_with(
&self,
others: &[Compatibility],
) -> Result<Compatibility, Incompatibility> {
(self.inner.is_compatible_with)(others)
}

pub fn get_expected_args(&self) -> Vec<Arg<'static, 'static>> {
(self.inner.get_expected_args)()
}

pub fn init(&self, args: &ArgMatches) -> Result<Box<dyn PluginLaunch>, Box<dyn Error>> {
(self.inner.init)(args)
}
}

pub use no_mangle::*;
#[cfg(feature = "no_mangle")]
pub mod no_mangle {
/// This macro will add a non-mangled `load_plugin` function to the library if feature `no_mangle` is enabled (which it is by default).
#[macro_export]
macro_rules! declare_plugin {
($ty: path) => {
#[no_mangle]
fn load_plugin(
version: PluginVTableVersion,
) -> Result<PluginVTable, PluginVTableVersion> {
if version == PLUGIN_VTABLE_VERSION {
Ok(PluginVTable::new::<$ty>())
} else {
Err(PLUGIN_VTABLE_VERSION)
}
}
};
}
}
#[cfg(not(feature = "no_mangle"))]
pub mod no_mangle {
#[macro_export]
macro_rules! declare_plugin {
($ty: path) => {};
}
}
}