use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use zerodds_ast::Ast;
use zerodds_dcps::runtime::DcpsRuntime;
use zerodds_dcps::serialized_type::SerializedTypeSupport;
use zerodds_types::EquivalenceHash;
use zerodds_types::dynamic::DynamicType;
use zerodds_types::type_object::TypeObject;
#[derive(Clone)]
pub struct ResolvedType {
pub support: Arc<SerializedTypeSupport>,
pub dynamic_type: DynamicType,
pub keyed: bool,
}
impl std::fmt::Debug for ResolvedType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResolvedType")
.field("type_name", &self.dynamic_type.name())
.field("keyed", &self.keyed)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub enum TypeResolveError {
Io(std::io::Error),
Idl(String),
Model(String),
NotFound(String),
NotYetResolved,
NoDiscoveredEndpoint(String),
TimedOut(String),
}
impl std::fmt::Display for TypeResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "io: {e}"),
Self::Idl(m) => write!(f, "idl: {m}"),
Self::Model(m) => write!(f, "type model: {m}"),
Self::NotFound(name) => write!(f, "type `{name}` not found"),
Self::NotYetResolved => f.write_str("type not yet resolved via TypeLookup"),
Self::NoDiscoveredEndpoint(topic) => {
write!(
f,
"no discovered publication or subscription names topic `{topic}`"
)
}
Self::TimedOut(name) => write!(
f,
"type `{name}` never resolved via TypeLookup within the bound"
),
}
}
}
impl std::error::Error for TypeResolveError {}
#[derive(Default)]
pub struct TypeRegistry {
by_name: Mutex<HashMap<String, ResolvedType>>,
}
impl TypeRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get(&self, type_name: &str) -> Option<ResolvedType> {
self.by_name
.lock()
.ok()
.and_then(|m| m.get(type_name).cloned())
}
pub fn insert(&self, type_name: &str, resolved: ResolvedType) {
if let Ok(mut m) = self.by_name.lock() {
m.insert(type_name.to_string(), resolved);
}
}
pub fn load_idl_file(
&self,
idl_path: &std::path::Path,
type_name: &str,
) -> Result<ResolvedType, TypeResolveError> {
let source = std::fs::read_to_string(idl_path).map_err(TypeResolveError::Io)?;
let ast = Arc::new(
zerodds_wire_probe::encode::model_from_idl(&source, &idl_path.to_string_lossy())
.map_err(|e| TypeResolveError::Idl(format!("{e}")))?,
);
let resolved = resolve_from_ast(&ast, type_name)?;
self.insert(type_name, resolved.clone());
Ok(resolved)
}
pub fn load_xml_file(
&self,
xml_path: &std::path::Path,
type_name: &str,
) -> Result<ResolvedType, TypeResolveError> {
let source = std::fs::read_to_string(xml_path).map_err(TypeResolveError::Io)?;
let libraries = zerodds_xml::parse_type_libraries(&source)
.map_err(|e| TypeResolveError::Idl(format!("{e}")))?;
let mut objects: HashMap<String, zerodds_types::CompleteTypeObject> = HashMap::new();
for lib in &libraries {
let bridged = zerodds_xml::bridge_library_complete(lib)
.map_err(|e| TypeResolveError::Model(format!("{e}")))?;
objects.extend(bridged);
}
let Some(root) = objects.remove(type_name) else {
return Err(TypeResolveError::NotFound(format!(
"{type_name} (not declared in {})",
xml_path.display()
)));
};
let dependencies: Vec<zerodds_types::CompleteTypeObject> = objects.into_values().collect();
let resolved = resolve_from_complete(type_name, root, dependencies)?;
self.insert(type_name, resolved.clone());
Ok(resolved)
}
pub fn resolve_discovered(
&self,
runtime: &DcpsRuntime,
type_name: &str,
hash: EquivalenceHash,
) -> Result<ResolvedType, TypeResolveError> {
let (root, dependencies) = {
let server = runtime
.type_lookup_server
.lock()
.map_err(|_| TypeResolveError::Model("type_lookup_server poisoned".into()))?;
let Some(root) = server.registry.get_complete(&hash).cloned() else {
return Err(TypeResolveError::NotYetResolved);
};
let dep_hashes = server
.registry
.transitive_dependencies(&hash, zerodds_types::resolve::DEFAULT_MAX_RESOLVE_NODES);
let mut deps = Vec::with_capacity(dep_hashes.len());
for dep_hash in &dep_hashes {
let Some(dep) = server.registry.get_complete(dep_hash).cloned() else {
return Err(TypeResolveError::NotYetResolved);
};
deps.push(dep);
}
(root, deps)
};
let resolved = resolve_from_complete(type_name, root, dependencies)?;
self.insert(type_name, resolved.clone());
Ok(resolved)
}
pub fn resolve_named(
&self,
runtime: &DcpsRuntime,
topic_name: &str,
type_name: &str,
timeout: core::time::Duration,
) -> Result<ResolvedType, TypeResolveError> {
let endpoint = runtime
.discovered_publication_endpoints()
.into_iter()
.chain(runtime.discovered_subscription_endpoints())
.find(|e| e.topic_name == topic_name)
.ok_or_else(|| TypeResolveError::NoDiscoveredEndpoint(topic_name.to_string()))?;
let zerodds_types::TypeIdentifier::EquivalenceHashComplete(hash) =
endpoint.type_identifier.clone()
else {
return Err(TypeResolveError::Model(format!(
"topic `{topic_name}`'s discovered TypeIdentifier is not the \
EquivalenceHashComplete form this daemon can resolve"
)));
};
match self.resolve_discovered(runtime, type_name, hash) {
Ok(r) => return Ok(r),
Err(TypeResolveError::NotYetResolved) => {}
Err(e) => return Err(e),
}
let mut peer_prefix = [0u8; 12];
peer_prefix.copy_from_slice(&endpoint.endpoint_guid[..12]);
let peer = zerodds_rtps::wire_types::GuidPrefix(peer_prefix);
let _ = runtime.send_type_lookup_request(peer, &[endpoint.type_identifier.clone()]);
let deadline = std::time::Instant::now() + timeout;
let mut last_generation = current_generation(runtime);
loop {
if std::time::Instant::now() >= deadline {
return Err(TypeResolveError::TimedOut(type_name.to_string()));
}
std::thread::sleep(core::time::Duration::from_millis(10));
let generation = current_generation(runtime);
if generation == last_generation {
continue;
}
last_generation = generation;
match self.resolve_discovered(runtime, type_name, hash) {
Ok(r) => return Ok(r),
Err(TypeResolveError::NotYetResolved) => {}
Err(e) => return Err(e),
}
}
}
pub fn resolve_synthetic(
&self,
type_name: &str,
root: zerodds_types::CompleteTypeObject,
dependencies: Vec<zerodds_types::CompleteTypeObject>,
) -> Result<ResolvedType, TypeResolveError> {
let resolved = resolve_from_complete(type_name, root, dependencies)?;
self.insert(type_name, resolved.clone());
Ok(resolved)
}
}
fn current_generation(runtime: &DcpsRuntime) -> u64 {
runtime
.type_lookup_server
.lock()
.map(|s| s.registry.generation())
.unwrap_or(0)
}
fn resolve_from_ast(ast: &Arc<Ast>, type_name: &str) -> Result<ResolvedType, TypeResolveError> {
let dynamic_type = DynamicType::find(ast, type_name)
.map_err(|e| TypeResolveError::NotFound(format!("{type_name}: {e}")))?;
let root_id = dynamic_type.id();
let complete = zerodds_types::from_ast::complete_type_object(ast, root_id)
.map_err(|e| TypeResolveError::Model(format!("{e:?}")))?;
let mut dependencies = Vec::new();
for (id, _) in ast.types() {
if id == root_id {
continue;
}
if let Ok(dep) = zerodds_types::from_ast::complete_type_object(ast, id) {
dependencies.push(dep);
}
}
resolve_from_complete(type_name, complete, dependencies)
}
fn resolve_from_complete(
type_name: &str,
root: zerodds_types::CompleteTypeObject,
dependencies: Vec<zerodds_types::CompleteTypeObject>,
) -> Result<ResolvedType, TypeResolveError> {
let root_bytes = TypeObject::Complete(root.clone())
.to_bytes_le()
.map_err(|e| TypeResolveError::Model(format!("re-encoding root TypeObject: {e:?}")))?;
let dep_bytes: Vec<Vec<u8>> = dependencies
.iter()
.map(|d| {
TypeObject::Complete(d.clone())
.to_bytes_le()
.map_err(|e| TypeResolveError::Model(format!("re-encoding dependency: {e:?}")))
})
.collect::<Result<_, _>>()?;
let mut registry = zerodds_types::resolve::TypeRegistry::new();
for dep in &dependencies {
registry
.insert_object(TypeObject::Complete(dep.clone()))
.map_err(|e| TypeResolveError::Model(format!("dependency hash: {e:?}")))?;
}
registry
.insert_object(TypeObject::Complete(root.clone()))
.map_err(|e| TypeResolveError::Model(format!("root hash: {e:?}")))?;
let mut model = zerodds_types::dynamic::ModelFromTypeObjects::new(®istry);
let root_id = model
.add_type_object(&TypeObject::Complete(root))
.map_err(|e| TypeResolveError::Model(format!("{e:?}")))?;
let ast = model.finish();
let dynamic_type = DynamicType::new(Arc::clone(&ast), root_id);
let keyed = zerodds_ast_layout::keyhash::holder_keeps_only_keys(&ast, root_id)
|| zerodds_ast_layout::keyhash::union_key_discriminator(&ast, root_id).is_some();
let extensibility = match dynamic_type.extensibility() {
Some(zerodds_types::dynamic::ExtensibilityKind::Final) => 0,
Some(zerodds_types::dynamic::ExtensibilityKind::Appendable) => 1,
Some(zerodds_types::dynamic::ExtensibilityKind::Mutable) => 2,
None => {
return Err(TypeResolveError::Model(format!(
"`{type_name}`'s root has no aggregate (struct/union) extensibility"
)));
}
};
let support = SerializedTypeSupport::from_type_objects(
type_name,
&root_bytes,
&dep_bytes,
keyed,
extensibility,
)
.map_err(TypeResolveError::Model)?;
Ok(ResolvedType {
support,
dynamic_type,
keyed,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn write_idl(dir: &std::path::Path, text: &str) -> std::path::PathBuf {
let path = dir.join("t.idl");
std::fs::write(&path, text).unwrap();
path
}
fn write_xml(dir: &std::path::Path, text: &str) -> std::path::PathBuf {
let path = dir.join("t.xml");
std::fs::write(&path, text).unwrap();
path
}
#[test]
fn loads_a_keyed_struct_from_a_dds_xml_types_file() {
let dir = std::env::temp_dir().join(format!("zd-webdds-xmltypes-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = write_xml(
&dir,
r#"<types>
<struct name="Sample">
<member name="id" type="int32" key="true"/>
<member name="value" type="float64"/>
</struct>
</types>"#,
);
let reg = TypeRegistry::new();
let resolved = reg.load_xml_file(&path, "Sample").expect("resolved");
assert!(resolved.keyed);
assert_eq!(resolved.dynamic_type.member_count(), 2);
assert!(reg.get("Sample").is_some());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn dds_xml_types_file_with_an_enum_dependency_resolves() {
let dir = std::env::temp_dir().join(format!("zd-webdds-xmltypes2-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = write_xml(
&dir,
r#"<types>
<enum name="Color">
<enumerator name="RED"/>
<enumerator name="GREEN"/>
</enum>
<struct name="Pixel">
<member name="id" type="int32" key="true"/>
<member name="color" type="nonBasic" nonBasicTypeName="Color"/>
</struct>
</types>"#,
);
let reg = TypeRegistry::new();
let resolved = reg.load_xml_file(&path, "Pixel").expect("resolved");
assert!(resolved.keyed);
assert_eq!(resolved.dynamic_type.member_count(), 2);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn unknown_type_in_xml_file_is_reported() {
let dir = std::env::temp_dir().join(format!("zd-webdds-xmltypes3-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = write_xml(
&dir,
r#"<types><struct name="A"><member name="x" type="int32"/></struct></types>"#,
);
let reg = TypeRegistry::new();
let err = reg.load_xml_file(&path, "NoSuchType").unwrap_err();
assert!(matches!(err, TypeResolveError::NotFound(_)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn loads_a_keyed_struct_from_an_idl_file() {
let dir = std::env::temp_dir().join(format!("zd-webdds-types-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = write_idl(
&dir,
"module M { @final struct Sample { @key long id; string name; }; };",
);
let reg = TypeRegistry::new();
let resolved = reg.load_idl_file(&path, "M::Sample").expect("resolved");
assert!(resolved.keyed);
assert_eq!(resolved.dynamic_type.member_count(), 2);
assert!(reg.get("M::Sample").is_some());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn unknown_type_name_is_reported() {
let dir = std::env::temp_dir().join(format!("zd-webdds-types2-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = write_idl(&dir, "module M { @final struct A { long x; }; };");
let reg = TypeRegistry::new();
let err = reg.load_idl_file(&path, "M::NoSuchType").unwrap_err();
assert!(matches!(err, TypeResolveError::NotFound(_)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn unkeyed_struct_roundtrip() {
let dir = std::env::temp_dir().join(format!("zd-webdds-types3-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = write_idl(
&dir,
"module M { @final struct Plain { long x; long y; }; };",
);
let reg = TypeRegistry::new();
let resolved = reg.load_idl_file(&path, "M::Plain").expect("resolved");
assert!(!resolved.keyed);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn resolve_named_discovers_a_type_never_loaded_locally() {
let dir = std::env::temp_dir().join(format!("zd-webdds-types-tl-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = write_idl(
&dir,
"module M { @final struct Tlq { @key long id; long value; }; };",
);
let lease = zerodds_test_domains::lease_domain();
let domain = lease.id();
let reg_a = TypeRegistry::new();
let resolved_a = reg_a.load_idl_file(&path, "M::Tlq").expect("resolve A");
let participant_a = zerodds_dcps::DomainParticipantFactory::instance()
.create_participant(domain, zerodds_dcps::DomainParticipantQos::default())
.expect("participant A");
let topic_a = participant_a
.create_serialized_topic("Tlq", zerodds_dcps::TopicQos::default(), resolved_a.support)
.expect("topic A");
let publisher_a = participant_a.create_publisher(zerodds_dcps::PublisherQos::default());
let _writer_a = publisher_a
.create_datawriter(&topic_a, zerodds_dcps::DataWriterQos::default())
.expect("writer A");
let participant_b = zerodds_dcps::DomainParticipantFactory::instance()
.create_participant(domain, zerodds_dcps::DomainParticipantQos::default())
.expect("participant B");
let subscriber_b = participant_b.create_subscriber(zerodds_dcps::SubscriberQos::default());
let _ = &subscriber_b;
let reg_b = TypeRegistry::new();
let runtime_b = participant_b.runtime().expect("live runtime B").clone();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
let mut resolved_b = None;
while resolved_b.is_none() && std::time::Instant::now() < deadline {
match reg_b.resolve_named(
&runtime_b,
"Tlq",
"M::Tlq",
std::time::Duration::from_secs(2),
) {
Ok(r) => resolved_b = Some(r),
Err(TypeResolveError::NoDiscoveredEndpoint(_)) => {
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(e) => panic!("resolve_named failed: {e}"),
}
}
let resolved_b = resolved_b.expect("type resolved via TypeLookup within 20s");
assert!(resolved_b.keyed);
assert_eq!(resolved_b.dynamic_type.member_count(), 2);
let _ = std::fs::remove_dir_all(&dir);
}
}