-
Notifications
You must be signed in to change notification settings - Fork 23
Description
Hi,
For a prototype, Im trying to implement a modular OpcUa server using libloading. Im currently working with three crates: 1. main crate (opcua_server), 2. common (utils) crate (opcua_common), 3. device communication module crate (dcm_test). Both the main crate and the dcm crate depend on the common crate to define intefacing traits and utils. DCMs are how I want to scale the server, allowing me to develop isolated libraries, compile them to DLLs and plug any combination of them on servers connected to different machines. Im currently using libloading to load the compiled dlls. I also previously prototyped this using the open62541 wrapper crate, which worked well up until the MethodCallbacks (specifically the obtaining of input arguments inside a libloaded library).
I defined a ServerAccess type that I can pass from the main to the DCM. When I use my access utils from the main crate, I can add nodes to the address_space without issues, but when trying the same from the DCM, I get the following error:
thread '<unnamed>' panicked at <...>\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\async-opcua-server-0.15.1\src\address_space\mod.rs:219:13:
Namespace index 2 not in address space
According to my console output, the namespace 2 does exist tho at every point I check the namespaces (including right before the error is thrown:
Address from init: 0x2f078b69898
-- {2: "urn:PrototypeServer"}
-- NS Some(2)
Address from access: 0x2f078b69898
-- {2: "urn:PrototypeServer"} // address_space.namespaces()
-- NS Some(2) // address_space.namespace_index(SERVER_NAMESPACE_URN)
Enter libloaded module
Address from access: 0x2f078b69898
-- {2: "urn:PrototypeServer"} // address_space.namespaces()
-- NS Some(2) // address_space.namespace_index(SERVER_NAMESPACE_URN)
Address from Inside Access: 0x2f078b69898
-- {2: "urn:PrototypeServer"} // address_space.namespaces()
-- NS Some(2) // address_space.namespace_index(SERVER_NAMESPACE_URN)
thread '<unnamed>' panicked at <...>\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\async-opcua-server-0.15.1\src\address_space\mod.rs:219:13:
Namespace index 2 not in address space
Im running on Windows 11, this is the relevant part of the config.toml for the dcm (same dependency config for common and main):
[lib]
crate-type = ["dylib"]
[dependencies.async-opcua]
version = "0.15.1"
features = ["server"]
default-features = falseIm still new to Rust, so Im not sure if my approach to this is actually feasible/advisable. Sorry for the code-dump. Just wondering if I am doing something terribly wrong or there is actually a bug with the address space. Please let me know if you need any additional information.
opcua_server/src/main.rs
#[tokio::main]
async fn main() -> Result<(), GlobalServerError> {
let server = server::init("./server.conf".into())?;
let _ = server_util::access(
&SERVER,
|address_space, ns, server_api| {
let mut address_space = address_space.write();
let line_id = NodeId::new(ns, "line");
ObjectBuilder::new(&line_id, "Line", "Line")
.event_notifier(EventNotifier::SUBSCRIBE_TO_EVENTS)
.organized_by(ObjectId::ObjectsFolder)
.insert(address_space.deref_mut());
let object_id = NodeId::new(ns, "main-object");
ObjectBuilder::new(&object_id, "MainObject", "Main Object")
.event_notifier(EventNotifier::SUBSCRIBE_TO_EVENTS)
.organized_by(line_id)
.insert(address_space.deref_mut());
let field_id = NodeId::new(ns, "main-object-field-1");
VariableBuilder::new(&field_id, "Var1", "Var 1")
.organized_by(&object_id)
.data_type(DataTypeId::String)
.value("Initial Value")
.insert(address_space.deref_mut());
let fn_node_id = NodeId::new(ns, "main-object-method-1");
MethodBuilder::new(&fn_node_id, "Method1", "Method 1")
.component_of(&object_id)
.executable(true)
.user_executable(true)
.input_args(
&mut *address_space,
&NodeId::new(ns, "main-object-method-1-input"),
&[("InputData", DataTypeId::String).into()],
)
.output_args(
&mut *address_space,
&NodeId::new(ns, "main-object-method-1-output"),
&[("ResultData", DataTypeId::String).into()]
)
.insert(&mut *address_space);
server_api.get_node_manager().inner().add_method_callback(fn_node_id, |args| {
println!("Blub method called");
let Some(Variant::String(s)) = args.first() else {
return Err(StatusCode::BadTypeMismatch);
};
Ok(vec![Variant::from(format!("Input was {s}!").to_owned())])
});
Ok(true)
}
);
let _dcm_list = meta::dcms::load_and_init_dcms(
DCM_DIRECTORY,
);
let handle_c = server_util::handle(&SERVER)?;
tokio::spawn(async move {
if let Err(_) = tokio::signal::ctrl_c().await {
return;
}
handle_c.cancel();
});
server.run().await.unwrap();
Ok(())
}opcua_server/src/dcms.rs (dcm loading)
pub fn load_and_init_dcms(dir: &str) -> Vec<(Library, &mut dyn DCM)> {
let mut dcm_list: Vec<(Library, &mut dyn DCM)> = vec![];
match ScanDir::files().read(dir, |iter| {
for (entry, name) in iter {
if !name.ends_with(".dll") {
continue;
}
match load_dcm(entry.path()) {
Ok(dcm) => dcm_list.push((dcm.0, Box::leak(dcm.1))),
Err(err) => println!("An error occured while trying to load {name}: {err}")
}
}
}) {
Ok(_) => println!("Finished reading DCM folder successfully."),
Err(err) => println!("{err}"),
}
dcm_list
}
fn load_dcm(dcm_file: PathBuf) -> Result<(Library, Box<dyn DCM>), DCMLoadingError> {
let dcm_lib = match unsafe { libloading::Library::new(&dcm_file) } {
Ok(lib) => lib,
Err(_) => {
return Err(DCMLoadingError { message: String::from("DCMLoadingFailed") });
},
};
let dcm_loader: libloading::Symbol<fn() -> Box<dyn DCM>> =
match unsafe { dcm_lib.get(b"load_dcm") } {
Ok(loader) => loader,
Err(_) => {
return Err(DCMLoadingError { message: String::from("DCMLoaderAccessError") });
}
};
let mut dcm = dcm_loader();
dcm.set_server_access(&SERVER);
let init_result = dcm.init();
if let Err(_) = init_result {
return Err(DCMLoadingError { message: String::from("DCMInitError") });
}
Ok((dcm_lib, dcm))
}opcua_server/src/server.rs
pub static SERVER: ServerAccess = Mutex::new(None);
pub struct OpcuaServer {
handle: ServerHandle,
ns: u16,
node_manager: Arc<InMemoryNodeManager<SimpleNodeManagerImpl>>
}
impl ServerAPI for OpcuaServer {
fn get_handle(&self) -> &ServerHandle {
&self.handle
}
fn get_ns(&self) -> u16 {
self.ns
}
fn get_node_manager(&self)
-> &Arc<InMemoryNodeManager<SimpleNodeManagerImpl>>
{
&self.node_manager
}
}
pub fn init(server_config: PathBuf) -> Result<Server, GlobalServerError> {
let (server, handle) = ServerBuilder::new()
.with_config_from(server_config)
.build_info(BuildInfo {
product_uri: "https://prototype.co.jp/".into(),
manufacturer_name: "Prototype".into(),
product_name: "OPCUA Prototype".into(),
software_version: "0.1.0".into(),
build_number: "1".into(),
build_date: DateTime::now(),
})
.with_node_manager(simple_node_manager(
NamespaceMetadata {
namespace_uri: SERVER_NAMESPACE_URN.to_owned(),
..Default::default()
},
"prototype"
))
.trust_client_certs(true)
.diagnostics_enabled(true)
.build()
.unwrap();
let node_manager: Arc<InMemoryNodeManager<SimpleNodeManagerImpl>> = handle
.node_managers()
.get_of_type::<SimpleNodeManager>()
.ok_or(GlobalServerError::NodeManagerInitError)?;
println!("Address from init: {:?}", node_manager.address_space().data_ptr());
println!("-- {:?}", node_manager.address_space().read().namespaces());
println!("-- NS {:?}", node_manager.address_space().read().namespace_index(SERVER_NAMESPACE_URN));
let ns = handle.get_namespace_index(SERVER_NAMESPACE_URN).unwrap();
*SERVER.lock().unwrap() = Some(Box::new(OpcuaServer {
handle,
ns,
node_manager,
}));
Ok(server)
}opcua_common/src/server_util.rs
use std::sync::{Arc, Mutex};
use opcua::sync::RwLock;
use opcua::server::ServerHandle;
use opcua::server::address_space::AddressSpace;
use opcua::server::node_manager::memory::InMemoryNodeManager;
use opcua::server::node_manager::memory::SimpleNodeManagerImpl;
use opcua::types::NodeId;
pub const SERVER_NAMESPACE_URN: &str = "urn:PrototypeServer";
pub type ServerAccess = Mutex<Option<Box<dyn ServerAPI>>>;
pub const fn empty_server_access() -> ServerAccess {
Mutex::new(None)
}
pub trait ServerAPI : Send + Sync {
fn get_handle(&self) -> &ServerHandle;
fn get_ns(&self) -> u16;
fn get_node_manager(&self) -> &Arc<InMemoryNodeManager<SimpleNodeManagerImpl>>;
}
#[derive(Debug)]
pub enum GlobalServerError {
NodeManagerInitError,
NamespaceNotFoundError,
AccessError
}
pub fn line_id(ns: u16) -> NodeId {
NodeId:: new(ns, "line")
}
pub fn handle(server_access: &ServerAccess) -> Result<ServerHandle, GlobalServerError> {
let unlocked = match server_access.lock() {
Ok(guard) => guard,
Err(_) => return Err(GlobalServerError::AccessError),
};
let server_api = unlocked
.as_ref()
.ok_or(GlobalServerError::AccessError)?;
Ok(server_api.get_handle().clone())
}
pub fn access(
server_access: &ServerAccess,
action: fn(
address_space: &Arc<RwLock<AddressSpace>>,
ns: u16,
server_api: &Box<dyn ServerAPI>
) -> Result<bool, GlobalServerError>
) -> Result<bool, GlobalServerError> {
let unlocked = match server_access.lock() {
Ok(guard) => guard,
Err(_) => return Err(GlobalServerError::AccessError),
};
let server_api = unlocked
.as_ref()
.ok_or(GlobalServerError::AccessError)?;
let node_manager = server_api.get_node_manager();
let address_space = node_manager.address_space();
println!("Address from access: {:?}", address_space.data_ptr());
println!("-- {:?} // address_space.namespaces()", address_space.read().namespaces());
println!("-- NS {:?} // address_space.namespace_index(SERVER_NAMESPACE_URN)", address_space.read().namespace_index(SERVER_NAMESPACE_URN));
// let ns = server_api
// .get_handle()
// .get_namespace_index(SERVER_NAMESPACE_URN)
// .ok_or(GlobalServerError::NamespaceNotFoundError)?;
action(address_space, server_api.get_ns(), server_api)
}opcua_common/src/dcm.rs
pub trait DCM {
fn init(&mut self) -> Result<(), Error>;
fn set_server_access(&mut self, server_access: &'static ServerAccess);
fn get_dcm_name(&self) -> &str;
fn exit(&self) -> ();
}dcm_test/src/lib.rs
static NON_HANDLER: ServerAccess = server_util::empty_server_access();
#[unsafe(no_mangle)]
pub fn load_dcm() -> Box<dyn DCM> {
Box::new(DCMTest { server_access: &NON_HANDLER })
}
struct DCMTest<'a> {
server_access: &'a ServerAccess,
}
impl DCM for DCMTest<'_> {
fn set_server_access(&mut self, server_access: &'static ServerAccess) {
self.server_access = server_access;
}
fn init(&mut self) -> Result<(), Error> {
println!("Enter libloaded module");
self.add_test_nodes();
Ok(())
}
fn get_dcm_name(&self) -> &str { "DCMTest" }
fn exit(&self) -> () { }
}
impl DCMTest<'_> {
fn add_test_nodes(&self) {
match server_util::access(
self.server_access,
|address_space, ns, server_api| {
println!("Address from Inside Access: {:?}", address_space.data_ptr());
let mut address_space = address_space.write();
println!("-- {:?} // address_space.namespaces()", address_space.namespaces());
println!("-- NS {:?} // address_space.namespace_index(SERVER_NAMESPACE_URN)", address_space.namespace_index(SERVER_NAMESPACE_URN));
let object_id = NodeId::new(ns, "dcm-object");
ObjectBuilder::new(&object_id, "DCMObject", "DCM Object")
.event_notifier(EventNotifier::SUBSCRIBE_TO_EVENTS)
.organized_by(server_util::line_id(ns))
.insert(&mut *address_space);
Ok(true)
}
) {
Ok(_) => println!("Executed server access in dcm."),
Err(err) => println!("Failed server access in dcm: {:?}", err),
}
println!("Finish");
}
}