From a0b884eab6eb55e2d267f75954298852d753e971 Mon Sep 17 00:00:00 2001 From: Nico Chatzi Date: Sat, 13 Mar 2021 09:46:19 +0000 Subject: [PATCH 1/3] Move macros and plugin_main to other files --- src/api.rs | 11 +- src/init.rs | 286 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 316 +------------------------------------------------- src/macros.rs | 27 +++++ src/plugin.rs | 2 +- 5 files changed, 325 insertions(+), 317 deletions(-) create mode 100644 src/init.rs create mode 100644 src/macros.rs diff --git a/src/api.rs b/src/api.rs index dc448b29..b1f94884 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,6 +4,7 @@ use std::os::raw::c_void; use std::sync::Arc; use self::consts::*; +use cache::PluginCache; use editor::Editor; use plugin::{Info, Plugin, PluginParameters}; @@ -134,7 +135,7 @@ pub struct AEffect { impl AEffect { /// Return handle to Plugin object. Only works for plugins created using this library. /// Caller is responsible for not calling this function concurrently. - // Supresses warning about returning a reference to a box + // Suppresses warning about returning a reference to a box #[allow(clippy::borrowed_box)] pub unsafe fn get_plugin(&self) -> &mut Box { //FIXME: find a way to do this without resorting to transmuting via a box @@ -143,24 +144,24 @@ impl AEffect { /// Return handle to Info object. Only works for plugins created using this library. pub unsafe fn get_info(&self) -> &Info { - &(*(self.user as *mut super::PluginCache)).info + &(*(self.user as *mut PluginCache)).info } /// Return handle to PluginParameters object. Only works for plugins created using this library. pub unsafe fn get_params(&self) -> &Arc { - &(*(self.user as *mut super::PluginCache)).params + &(*(self.user as *mut PluginCache)).params } /// Return handle to Editor object. Only works for plugins created using this library. /// Caller is responsible for not calling this function concurrently. pub unsafe fn get_editor(&self) -> &mut Option> { - &mut (*(self.user as *mut super::PluginCache)).editor + &mut (*(self.user as *mut PluginCache)).editor } /// Drop the Plugin object. Only works for plugins created using this library. pub unsafe fn drop_plugin(&mut self) { drop(Box::from_raw(self.object as *mut Box)); - drop(Box::from_raw(self.user as *mut super::PluginCache)); + drop(Box::from_raw(self.user as *mut PluginCache)); } } diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 00000000..5713a041 --- /dev/null +++ b/src/init.rs @@ -0,0 +1,286 @@ +//! Entry point for initializing a VST plugin + +use api::consts::VST_MAGIC; +use api::{AEffect, HostCallbackProc}; +use cache::PluginCache; +use interfaces; +use plugin::{self, HostCallback, Plugin}; +use std::ptr; + +/// Exports the necessary symbols for the plugin to be used by a VST host. +/// +/// This macro takes a type which must implement the `Plugin` trait. +#[macro_export] +macro_rules! plugin_main { + ($t:ty) => { + #[cfg(target_os = "macos")] + #[no_mangle] + pub extern "system" fn main_macho(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { + VSTPluginMain(callback) + } + + #[cfg(target_os = "windows")] + #[allow(non_snake_case)] + #[no_mangle] + pub extern "system" fn MAIN(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { + VSTPluginMain(callback) + } + + #[allow(non_snake_case)] + #[no_mangle] + pub extern "C" fn VSTPluginMain(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { + $crate::init::main::<$t>(callback) + } + }; +} + +/// Initializes a VST plugin and returns a raw pointer to an AEffect struct. +#[doc(hidden)] +pub fn main(callback: HostCallbackProc) -> *mut AEffect { + // Initialize as much of the AEffect as we can before creating the plugin. + // In particular, initialize all the function pointers, since initializing + // these to zero is undefined behavior. + let boxed_effect = Box::new(AEffect { + magic: VST_MAGIC, + dispatcher: interfaces::dispatch, // fn pointer + + _process: interfaces::process_deprecated, // fn pointer + + setParameter: interfaces::set_parameter, // fn pointer + getParameter: interfaces::get_parameter, // fn pointer + + numPrograms: 0, // To be updated with plugin specific value. + numParams: 0, // To be updated with plugin specific value. + numInputs: 0, // To be updated with plugin specific value. + numOutputs: 0, // To be updated with plugin specific value. + + flags: 0, // To be updated with plugin specific value. + + reserved1: 0, + reserved2: 0, + + initialDelay: 0, // To be updated with plugin specific value. + + _realQualities: 0, + _offQualities: 0, + _ioRatio: 0.0, + + object: ptr::null_mut(), + user: ptr::null_mut(), + + uniqueId: 0, // To be updated with plugin specific value. + version: 0, // To be updated with plugin specific value. + + processReplacing: interfaces::process_replacing, // fn pointer + processReplacingF64: interfaces::process_replacing_f64, //fn pointer + + future: [0u8; 56], + }); + let raw_effect = Box::into_raw(boxed_effect); + + let host = HostCallback::wrap(callback, raw_effect); + if host.vst_version() == 0 { + // TODO: Better criteria would probably be useful here... + return ptr::null_mut(); + } + + trace!("Creating VST plugin instance..."); + let mut plugin = T::new(host); + let info = plugin.get_info(); + let params = plugin.get_parameter_object(); + let editor = plugin.get_editor(); + + // Update AEffect in place + let effect = unsafe { &mut *raw_effect }; + effect.numPrograms = info.presets; + effect.numParams = info.parameters; + effect.numInputs = info.inputs; + effect.numOutputs = info.outputs; + effect.flags = { + use api::PluginFlags; + + let mut flag = PluginFlags::CAN_REPLACING; + + if info.f64_precision { + flag |= PluginFlags::CAN_DOUBLE_REPLACING; + } + + if editor.is_some() { + flag |= PluginFlags::HAS_EDITOR; + } + + if info.preset_chunks { + flag |= PluginFlags::PROGRAM_CHUNKS; + } + + if let plugin::Category::Synth = info.category { + flag |= PluginFlags::IS_SYNTH; + } + + if info.silent_when_stopped { + flag |= PluginFlags::NO_SOUND_IN_STOP; + } + + flag.bits() + }; + effect.initialDelay = info.initial_delay; + effect.object = Box::into_raw(Box::new(Box::new(plugin) as Box)) as *mut _; + effect.user = Box::into_raw(Box::new(PluginCache::new(&info, params, editor))) as *mut _; + effect.uniqueId = info.unique_id; + effect.version = info.version; + + effect +} + +#[cfg(test)] +mod tests { + use std::ptr; + + use std::os::raw::c_void; + + use api::consts::VST_MAGIC; + use api::AEffect; + use interfaces; + use plugin::{HostCallback, Info, Plugin}; + + struct TestPlugin; + + impl Plugin for TestPlugin { + fn new(_host: HostCallback) -> Self { + TestPlugin + } + + fn get_info(&self) -> Info { + Info { + name: "Test Plugin".to_string(), + vendor: "overdrivenpotato".to_string(), + + presets: 1, + parameters: 1, + + unique_id: 5678, + version: 1234, + + initial_delay: 123, + + ..Default::default() + } + } + } + + plugin_main!(TestPlugin); + + extern "C" fn pass_callback( + _effect: *mut AEffect, + _opcode: i32, + _index: i32, + _value: isize, + _ptr: *mut c_void, + _opt: f32, + ) -> isize { + 1 + } + + extern "C" fn fail_callback( + _effect: *mut AEffect, + _opcode: i32, + _index: i32, + _value: isize, + _ptr: *mut c_void, + _opt: f32, + ) -> isize { + 0 + } + + #[cfg(target_os = "windows")] + #[test] + fn old_hosts() { + assert_eq!(MAIN(fail_callback), ptr::null_mut()); + } + + #[cfg(target_os = "macos")] + #[test] + fn old_hosts() { + assert_eq!(main_macho(fail_callback), ptr::null_mut()); + } + + #[test] + fn host_callback() { + assert_eq!(VSTPluginMain(fail_callback), ptr::null_mut()); + } + + #[test] + fn aeffect_created() { + let aeffect = VSTPluginMain(pass_callback); + assert!(!aeffect.is_null()); + } + + #[test] + fn plugin_drop() { + static mut DROP_TEST: bool = false; + + impl Drop for TestPlugin { + fn drop(&mut self) { + unsafe { + DROP_TEST = true; + } + } + } + + let aeffect = VSTPluginMain(pass_callback); + assert!(!aeffect.is_null()); + + unsafe { (*aeffect).drop_plugin() }; + + // Assert that the VST is shut down and dropped. + assert!(unsafe { DROP_TEST }); + } + + #[test] + fn plugin_no_drop() { + let aeffect = VSTPluginMain(pass_callback); + assert!(!aeffect.is_null()); + + // Make sure this doesn't crash. + unsafe { (*aeffect).drop_plugin() }; + } + + #[test] + fn plugin_deref() { + let aeffect = VSTPluginMain(pass_callback); + assert!(!aeffect.is_null()); + + let plugin = unsafe { (*aeffect).get_plugin() }; + // Assert that deref works correctly. + assert!(plugin.get_info().name == "Test Plugin"); + } + + #[test] + fn aeffect_params() { + // Assert that 2 function pointers are equal. + macro_rules! assert_fn_eq { + ($a:expr, $b:expr) => { + assert_eq!($a as usize, $b as usize); + }; + } + + let aeffect = unsafe { &mut *VSTPluginMain(pass_callback) }; + + assert_eq!(aeffect.magic, VST_MAGIC); + assert_fn_eq!(aeffect.dispatcher, interfaces::dispatch); + assert_fn_eq!(aeffect._process, interfaces::process_deprecated); + assert_fn_eq!(aeffect.setParameter, interfaces::set_parameter); + assert_fn_eq!(aeffect.getParameter, interfaces::get_parameter); + assert_eq!(aeffect.numPrograms, 1); + assert_eq!(aeffect.numParams, 1); + assert_eq!(aeffect.numInputs, 2); + assert_eq!(aeffect.numOutputs, 2); + assert_eq!(aeffect.reserved1, 0); + assert_eq!(aeffect.reserved2, 0); + assert_eq!(aeffect.initialDelay, 123); + assert_eq!(aeffect.uniqueId, 5678); + assert_eq!(aeffect.version, 1234); + assert_fn_eq!(aeffect.processReplacing, interfaces::process_replacing); + assert_fn_eq!(aeffect.processReplacingF64, interfaces::process_replacing_f64); + } +} diff --git a/src/lib.rs b/src/lib.rs index cf37ac44..dd5c0015 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,324 +116,18 @@ extern crate log; #[macro_use] extern crate bitflags; -use std::ptr; - -/// Implements `From` and `Into` for enums with `#[repr(usize)]`. Useful for interfacing with C -/// enums. -macro_rules! impl_clike { - ($t:ty, $($c:ty) +) => { - $( - impl From<$c> for $t { - fn from(v: $c) -> $t { - use std::mem; - unsafe { mem::transmute(v as usize) } - } - } - - impl Into<$c> for $t { - fn into(self) -> $c { - self as $c - } - } - )* - }; - - ($t:ty) => { - impl_clike!($t, i8 i16 i32 i64 isize u8 u16 u32 u64 usize); - } -} +#[macro_use] +mod macros; +mod cache; +mod interfaces; pub mod api; pub mod buffer; -mod cache; pub mod channels; pub mod editor; pub mod event; pub mod host; -mod interfaces; +pub mod init; pub mod plugin; pub mod util; - -use api::consts::VST_MAGIC; -use api::{AEffect, HostCallbackProc}; -use cache::PluginCache; -use plugin::{HostCallback, Plugin}; - -/// Exports the necessary symbols for the plugin to be used by a VST host. -/// -/// This macro takes a type which must implement the `Plugin` trait. -#[macro_export] -macro_rules! plugin_main { - ($t:ty) => { - #[cfg(target_os = "macos")] - #[no_mangle] - pub extern "system" fn main_macho(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { - VSTPluginMain(callback) - } - - #[cfg(target_os = "windows")] - #[allow(non_snake_case)] - #[no_mangle] - pub extern "system" fn MAIN(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { - VSTPluginMain(callback) - } - - #[allow(non_snake_case)] - #[no_mangle] - pub extern "C" fn VSTPluginMain(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { - $crate::main::<$t>(callback) - } - }; -} - -/// Initializes a VST plugin and returns a raw pointer to an AEffect struct. -#[doc(hidden)] -pub fn main(callback: HostCallbackProc) -> *mut AEffect { - // Initialize as much of the AEffect as we can before creating the plugin. - // In particular, initialize all the function pointers, since initializing - // these to zero is undefined behavior. - let boxed_effect = Box::new(AEffect { - magic: VST_MAGIC, - dispatcher: interfaces::dispatch, // fn pointer - - _process: interfaces::process_deprecated, // fn pointer - - setParameter: interfaces::set_parameter, // fn pointer - getParameter: interfaces::get_parameter, // fn pointer - - numPrograms: 0, // To be updated with plugin specific value. - numParams: 0, // To be updated with plugin specific value. - numInputs: 0, // To be updated with plugin specific value. - numOutputs: 0, // To be updated with plugin specific value. - - flags: 0, // To be updated with plugin specific value. - - reserved1: 0, - reserved2: 0, - - initialDelay: 0, // To be updated with plugin specific value. - - _realQualities: 0, - _offQualities: 0, - _ioRatio: 0.0, - - object: ptr::null_mut(), - user: ptr::null_mut(), - - uniqueId: 0, // To be updated with plugin specific value. - version: 0, // To be updated with plugin specific value. - - processReplacing: interfaces::process_replacing, // fn pointer - processReplacingF64: interfaces::process_replacing_f64, //fn pointer - - future: [0u8; 56], - }); - let raw_effect = Box::into_raw(boxed_effect); - - let host = HostCallback::wrap(callback, raw_effect); - if host.vst_version() == 0 { - // TODO: Better criteria would probably be useful here... - return ptr::null_mut(); - } - - trace!("Creating VST plugin instance..."); - let mut plugin = T::new(host); - let info = plugin.get_info(); - let params = plugin.get_parameter_object(); - let editor = plugin.get_editor(); - - // Update AEffect in place - let effect = unsafe { &mut *raw_effect }; - effect.numPrograms = info.presets; - effect.numParams = info.parameters; - effect.numInputs = info.inputs; - effect.numOutputs = info.outputs; - effect.flags = { - use api::PluginFlags; - - let mut flag = PluginFlags::CAN_REPLACING; - - if info.f64_precision { - flag |= PluginFlags::CAN_DOUBLE_REPLACING; - } - - if editor.is_some() { - flag |= PluginFlags::HAS_EDITOR; - } - - if info.preset_chunks { - flag |= PluginFlags::PROGRAM_CHUNKS; - } - - if let plugin::Category::Synth = info.category { - flag |= PluginFlags::IS_SYNTH; - } - - if info.silent_when_stopped { - flag |= PluginFlags::NO_SOUND_IN_STOP; - } - - flag.bits() - }; - effect.initialDelay = info.initial_delay; - effect.object = Box::into_raw(Box::new(Box::new(plugin) as Box)) as *mut _; - effect.user = Box::into_raw(Box::new(PluginCache::new(&info, params, editor))) as *mut _; - effect.uniqueId = info.unique_id; - effect.version = info.version; - - effect -} - -#[cfg(test)] -mod tests { - use std::ptr; - - use std::os::raw::c_void; - - use api::consts::VST_MAGIC; - use api::AEffect; - use interfaces; - use plugin::{HostCallback, Info, Plugin}; - - struct TestPlugin; - - impl Plugin for TestPlugin { - fn new(_host: HostCallback) -> Self { - TestPlugin - } - - fn get_info(&self) -> Info { - Info { - name: "Test Plugin".to_string(), - vendor: "overdrivenpotato".to_string(), - - presets: 1, - parameters: 1, - - unique_id: 5678, - version: 1234, - - initial_delay: 123, - - ..Default::default() - } - } - } - - plugin_main!(TestPlugin); - - extern "C" fn pass_callback( - _effect: *mut AEffect, - _opcode: i32, - _index: i32, - _value: isize, - _ptr: *mut c_void, - _opt: f32, - ) -> isize { - 1 - } - - extern "C" fn fail_callback( - _effect: *mut AEffect, - _opcode: i32, - _index: i32, - _value: isize, - _ptr: *mut c_void, - _opt: f32, - ) -> isize { - 0 - } - - #[cfg(target_os = "windows")] - #[test] - fn old_hosts() { - assert_eq!(MAIN(fail_callback), ptr::null_mut()); - } - - #[cfg(target_os = "macos")] - #[test] - fn old_hosts() { - assert_eq!(main_macho(fail_callback), ptr::null_mut()); - } - - #[test] - fn host_callback() { - assert_eq!(VSTPluginMain(fail_callback), ptr::null_mut()); - } - - #[test] - fn aeffect_created() { - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - } - - #[test] - fn plugin_drop() { - static mut DROP_TEST: bool = false; - - impl Drop for TestPlugin { - fn drop(&mut self) { - unsafe { - DROP_TEST = true; - } - } - } - - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - - unsafe { (*aeffect).drop_plugin() }; - - // Assert that the VST is shut down and dropped. - assert!(unsafe { DROP_TEST }); - } - - #[test] - fn plugin_no_drop() { - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - - // Make sure this doesn't crash. - unsafe { (*aeffect).drop_plugin() }; - } - - #[test] - fn plugin_deref() { - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - - let plugin = unsafe { (*aeffect).get_plugin() }; - // Assert that deref works correctly. - assert!(plugin.get_info().name == "Test Plugin"); - } - - #[test] - fn aeffect_params() { - // Assert that 2 function pointers are equal. - macro_rules! assert_fn_eq { - ($a:expr, $b:expr) => { - assert_eq!($a as usize, $b as usize); - }; - } - - let aeffect = unsafe { &mut *VSTPluginMain(pass_callback) }; - - assert_eq!(aeffect.magic, VST_MAGIC); - assert_fn_eq!(aeffect.dispatcher, interfaces::dispatch); - assert_fn_eq!(aeffect._process, interfaces::process_deprecated); - assert_fn_eq!(aeffect.setParameter, interfaces::set_parameter); - assert_fn_eq!(aeffect.getParameter, interfaces::get_parameter); - assert_eq!(aeffect.numPrograms, 1); - assert_eq!(aeffect.numParams, 1); - assert_eq!(aeffect.numInputs, 2); - assert_eq!(aeffect.numOutputs, 2); - assert_eq!(aeffect.reserved1, 0); - assert_eq!(aeffect.reserved2, 0); - assert_eq!(aeffect.initialDelay, 123); - assert_eq!(aeffect.uniqueId, 5678); - assert_eq!(aeffect.version, 1234); - assert_fn_eq!(aeffect.processReplacing, interfaces::process_replacing); - assert_fn_eq!(aeffect.processReplacingF64, interfaces::process_replacing_f64); - } -} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 00000000..a8cc05d6 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,27 @@ +//! Internal utility macros + +/// Implements `From` and `Into` for enums with `#[repr(usize)]`. Useful for interfacing with C +/// enums. +#[macro_export] +macro_rules! impl_clike { + ($t:ty, $($c:ty) +) => { + $( + impl From<$c> for $t { + fn from(v: $c) -> $t { + use std::mem; + unsafe { mem::transmute(v as usize) } + } + } + + impl Into<$c> for $t { + fn into(self) -> $c { + self as $c + } + } + )* + }; + + ($t:ty) => { + impl_clike!($t, i8 i16 i32 i64 isize u8 u16 u32 u64 usize); + } +} diff --git a/src/plugin.rs b/src/plugin.rs index 1e9b062a..559880b4 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -977,7 +977,7 @@ mod tests { ($($attr:meta) *) => { use std::os::raw::c_void; - use main; + use init::main; use api::AEffect; use host::{Host, OpCode}; use plugin::{HostCallback, Info, Plugin}; From 6e8bc0f73ba63090280c6f1bf2e9b7983b334613 Mon Sep 17 00:00:00 2001 From: Nico Chatzi Date: Sat, 13 Mar 2021 09:59:53 +0000 Subject: [PATCH 2/3] Fix some clippy lints --- src/channels.rs | 12 ++---------- src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index a4e79a25..f78367b8 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -201,20 +201,12 @@ impl Default for SpeakerArrangementType { impl SpeakerArrangementType { /// Determine whether this channel is part of a surround speaker arrangement. pub fn is_speaker_type(&self) -> bool { - if let SpeakerArrangementType::Surround(..) = *self { - true - } else { - false - } + matches!(*self, SpeakerArrangementType::Surround(..)) } /// Determine whether this channel is the left speaker in a stereo pair. pub fn is_left_stereo(&self) -> bool { - if let SpeakerArrangementType::Stereo(_, StereoChannel::Left) = *self { - true - } else { - false - } + matches!(*self, SpeakerArrangementType::Stereo(_, StereoChannel::Left)) } } diff --git a/src/lib.rs b/src/lib.rs index dd5c0015..fd53580a 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -#![warn(missing_docs)] +#![allow(clippy::mut_from_ref)] +#![deny(missing_docs, unused_imports)] //! A rust implementation of the VST2.4 API. //! From 40a41a4f47687d8f87ad232f4a4f9452099d5f60 Mon Sep 17 00:00:00 2001 From: Nico Chatzi Date: Sat, 13 Mar 2021 15:15:43 +0000 Subject: [PATCH 3/3] Add doc_comment --- Cargo.toml | 1 + README.md | 9 +++------ src/lib.rs | 10 +++++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4fcfa3b1..a96c147c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ libloading = "0.5" [dev-dependencies] time = "0.1" rand = "0.7" +doc-comment = "0.3.3" [[example]] name = "dimension_expander" diff --git a/README.md b/README.md index a4ee1030..d5345922 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,6 @@ A simple plugin that bears no functionality. The provided `Cargo.toml` has a `src/lib.rs` ```rust -#[macro_use] -extern crate vst; - use vst::plugin::{HostCallback, Info, Plugin}; struct BasicPlugin; @@ -64,7 +61,7 @@ impl Plugin for BasicPlugin { } } -plugin_main!(BasicPlugin); // Important! +vst::plugin_main!(BasicPlugin); // Important! ``` `Cargo.toml` @@ -97,8 +94,8 @@ To package your VST as a loadable bundle you may use the `osx_vst_bundler.sh` sc Example:  -``` -./osx_vst_bundler.sh Plugin target/release/plugin.dylib +```sh +$ ./osx_vst_bundler.sh Plugin target/release/plugin.dylib Creates a Plugin.vst bundle ``` diff --git a/src/lib.rs b/src/lib.rs index fd53580a..55563c59 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ -#![allow(clippy::mut_from_ref)] -#![deny(missing_docs, unused_imports)] +#![warn(missing_docs)] //! A rust implementation of the VST2.4 API. //! @@ -116,6 +115,12 @@ extern crate num_traits; extern crate log; #[macro_use] extern crate bitflags; +#[cfg(doctest)] +#[macro_use] +extern crate doc_comment; + +#[cfg(doctest)] +doctest!("../README.md"); #[macro_use] mod macros; @@ -130,5 +135,4 @@ pub mod event; pub mod host; pub mod init; pub mod plugin; - pub mod util;