1use crate::{
2 ArtifactCache, COMPONENT_ABI_VERSION, COMPONENT_MANIFEST_SCHEMA, ComponentError, ComponentLockEntry,
3 ComponentManifest,
4};
5use alef_component_abi::{
6 ABI_MAJOR_V1, ABI_MINOR_V1, AlefComponentEntryV1, AlefComponentV1, AlefContract, AlefHostApiV1, AlefOwnedBuffer,
7};
8use libloading::Library;
9use std::ffi::c_void;
10use std::marker::PhantomData;
11use std::ptr::NonNull;
12use std::sync::{Arc, Mutex, OnceLock};
13
14const MAX_DESCRIPTOR_STRING: usize = 16 * 1024;
15
16#[derive(Clone, Debug)]
25pub struct ComponentRequirements {
26 pub component_id: String,
27 pub contract_name: String,
28 pub contract_hash: [u8; 32],
29 pub feature_set_hash: Option<[u8; 32]>,
30}
31
32#[derive(Clone, Debug)]
33pub struct Runtime {
34 cache: ArtifactCache,
35}
36
37impl Runtime {
38 pub fn new(cache: ArtifactCache) -> Self {
39 Self { cache }
40 }
41
42 pub fn install_and_load(
43 &self,
44 entry: &ComponentLockEntry,
45 entry_symbol: &[u8],
46 requirements: &ComponentRequirements,
47 host: AlefHostApiV1,
48 ) -> Result<LoadedComponent, ComponentError> {
49 let cached = self.cache.install(entry)?;
50 validate_manifest(&cached.manifest, requirements)?;
51 LoadedComponent::load(cached.library, entry_symbol, requirements, host)
52 }
53}
54
55pub struct LoadedComponent {
56 library: Arc<Library>,
57 descriptor: NonNull<AlefComponentV1>,
58 host: Box<AlefHostApiV1>,
59 component_id: String,
60 component_version: String,
61}
62
63pub struct ComponentInstance<T: AlefContract> {
68 component: Arc<LoadedComponent>,
69 handle: NonNull<c_void>,
70 _contract: PhantomData<T>,
71}
72
73unsafe impl Send for LoadedComponent {}
77unsafe impl Sync for LoadedComponent {}
78
79unsafe impl<T: AlefContract> Send for ComponentInstance<T> {}
87unsafe impl<T: AlefContract> Sync for ComponentInstance<T> {}
88
89impl LoadedComponent {
90 pub fn load(
99 path: impl Into<std::path::PathBuf>,
100 entry_symbol: &[u8],
101 requirements: &ComponentRequirements,
102 mut host: AlefHostApiV1,
103 ) -> Result<Self, ComponentError> {
104 let path = path.into();
105 host.struct_size = core::mem::size_of::<AlefHostApiV1>();
106 host.abi_major = ABI_MAJOR_V1;
107 host.abi_minor = ABI_MINOR_V1;
108 let host = Box::new(host);
109
110 let library = Arc::new(
111 unsafe { Library::new(&path) }.map_err(|error| ComponentError::LibraryLoad {
112 path: path.clone(),
113 message: error.to_string(),
114 })?,
115 );
116 let descriptor = unsafe {
117 let entry: libloading::Symbol<'_, AlefComponentEntryV1> = library
118 .get(entry_symbol)
119 .map_err(|error| ComponentError::MissingEntrypoint(error.to_string()))?;
120 let mut raw = core::ptr::null();
121 let status = entry(ABI_MAJOR_V1, host.as_ref(), &mut raw);
122 if !status.is_ok() {
123 return Err(ComponentError::EntrypointFailed(status.0));
124 }
125 NonNull::new(raw.cast_mut()).ok_or(ComponentError::InvalidDescriptor("null descriptor"))?
126 };
127
128 unsafe { reject_undersized_descriptor(descriptor)? };
133 let validated = unsafe { validate_descriptor(descriptor.as_ref(), requirements)? };
134 pin_for_process(Arc::clone(&library));
135 Ok(Self {
136 library,
137 descriptor,
138 host,
139 component_id: validated.0,
140 component_version: validated.1,
141 })
142 }
143
144 #[must_use]
145 pub fn component_id(&self) -> &str {
146 &self.component_id
147 }
148
149 #[must_use]
150 pub fn component_version(&self) -> &str {
151 &self.component_version
152 }
153
154 #[must_use]
155 pub fn descriptor(&self) -> &AlefComponentV1 {
156 unsafe { self.descriptor.as_ref() }
157 }
158
159 #[must_use]
160 pub fn contract(&self) -> *const c_void {
161 self.descriptor().contract
162 }
163
164 #[must_use]
165 pub fn contract_size(&self) -> usize {
166 self.descriptor().contract_size
167 }
168
169 pub fn contract_table<T: AlefContract>(&self) -> Result<&T, ComponentError> {
170 contract_table_from_descriptor(self.descriptor())
171 }
172
173 pub fn instantiate<T: AlefContract>(self: &Arc<Self>) -> Result<ComponentInstance<T>, ComponentError> {
175 self.contract_table::<T>()?;
176 let create = self.descriptor().create.ok_or(ComponentError::MissingInstanceFactory)?;
177 self.descriptor()
178 .destroy
179 .ok_or(ComponentError::MissingInstanceDestructor)?;
180 let mut raw = core::ptr::null_mut();
181 let mut error = AlefOwnedBuffer::EMPTY;
182 let status = unsafe { create(self.host_api(), &mut raw, &mut error) };
183 if !status.is_ok() {
184 return Err(ComponentError::InstanceCreation {
185 status: status.0,
186 message: unsafe { take_owned_buffer(error) },
187 });
188 }
189 let handle = NonNull::new(raw).ok_or(ComponentError::NullInstance)?;
190 Ok(ComponentInstance {
191 component: Arc::clone(self),
192 handle,
193 _contract: PhantomData,
194 })
195 }
196
197 #[must_use]
198 pub fn host_api(&self) -> &AlefHostApiV1 {
199 &self.host
200 }
201
202 #[must_use]
203 pub fn strong_library_references(&self) -> usize {
204 Arc::strong_count(&self.library)
205 }
206}
207
208impl<T: AlefContract> ComponentInstance<T> {
209 #[must_use]
210 pub fn table(&self) -> &T {
211 self.component
213 .contract_table::<T>()
214 .expect("validated component contract table changed")
215 }
216
217 #[must_use]
218 pub fn handle(&self) -> *mut c_void {
219 self.handle.as_ptr()
220 }
221
222 #[must_use]
223 pub fn component(&self) -> &Arc<LoadedComponent> {
224 &self.component
225 }
226}
227
228impl<T: AlefContract> Drop for ComponentInstance<T> {
229 fn drop(&mut self) {
230 if let Some(destroy) = self.component.descriptor().destroy {
231 unsafe { destroy(self.handle.as_ptr()) };
232 }
233 }
234}
235
236#[must_use]
250pub unsafe fn take_owned_bytes(buffer: AlefOwnedBuffer) -> Vec<u8> {
251 let bytes = if buffer.ptr.is_null() || buffer.len == 0 {
252 Vec::new()
253 } else {
254 unsafe { core::slice::from_raw_parts(buffer.ptr, buffer.len) }.to_vec()
256 };
257 if let Some(free) = buffer.free {
258 unsafe { free(buffer.context, buffer.ptr, buffer.len, buffer.capacity) };
260 }
261 bytes
262}
263
264#[must_use]
275pub unsafe fn take_owned_text(buffer: AlefOwnedBuffer) -> String {
276 String::from_utf8_lossy(&unsafe { take_owned_bytes(buffer) }).into_owned()
278}
279
280unsafe fn take_owned_buffer(buffer: AlefOwnedBuffer) -> String {
281 let message = if buffer.ptr.is_null() {
282 if buffer.len == 0 {
283 String::new()
284 } else {
285 "component returned an invalid error buffer".to_string()
286 }
287 } else {
288 String::from_utf8_lossy(unsafe { core::slice::from_raw_parts(buffer.ptr, buffer.len) }).into_owned()
289 };
290 if let Some(free) = buffer.free {
291 unsafe { free(buffer.context, buffer.ptr, buffer.len, buffer.capacity) };
292 }
293 message
294}
295
296fn contract_table_from_descriptor<T: AlefContract>(descriptor: &AlefComponentV1) -> Result<&T, ComponentError> {
297 if descriptor.contract_hash != T::CONTRACT_HASH
298 || descriptor.contract_size < core::mem::size_of::<T>()
299 || descriptor.contract.is_null()
300 || !(descriptor.contract as usize).is_multiple_of(core::mem::align_of::<T>())
301 {
302 return Err(ComponentError::InvalidContractTable);
303 }
304 Ok(unsafe { &*descriptor.contract.cast::<T>() })
305}
306
307fn process_pins() -> &'static Mutex<Vec<Arc<Library>>> {
308 static PINS: OnceLock<Mutex<Vec<Arc<Library>>>> = OnceLock::new();
309 PINS.get_or_init(|| Mutex::new(Vec::new()))
310}
311
312fn pin_for_process(library: Arc<Library>) {
313 process_pins()
314 .lock()
315 .unwrap_or_else(|poisoned| poisoned.into_inner())
316 .push(library);
317}
318
319pub(crate) fn validate_manifest(
320 manifest: &ComponentManifest,
321 requirements: &ComponentRequirements,
322) -> Result<(), ComponentError> {
323 if manifest.schema_version != COMPONENT_MANIFEST_SCHEMA || manifest.abi_version != COMPONENT_ABI_VERSION {
324 return Err(ComponentError::UnsupportedManifest);
325 }
326 if manifest.identity.component != requirements.component_id {
327 return Err(ComponentError::IdentityMismatch {
328 expected: requirements.component_id.clone(),
329 actual: manifest.identity.component.clone(),
330 });
331 }
332 let provided = manifest
333 .provides
334 .iter()
335 .find(|provided| provided.contract == requirements.contract_name)
336 .ok_or_else(|| ComponentError::ContractNotProvided {
337 component_id: requirements.component_id.clone(),
338 contract: requirements.contract_name.clone(),
339 })?;
340 if decode_hash(&provided.contract_hash)? != requirements.contract_hash {
341 return Err(ComponentError::ContractHashMismatch);
342 }
343 if let Some(expected) = requirements.feature_set_hash
344 && decode_hash(&manifest.identity.feature_hash)? != expected
345 {
346 return Err(ComponentError::FeatureSetHashMismatch);
347 }
348 Ok(())
349}
350
351const _: () = assert!(core::mem::offset_of!(AlefComponentV1, struct_size) == 0);
356
357unsafe fn reject_undersized_descriptor(descriptor: NonNull<AlefComponentV1>) -> Result<(), ComponentError> {
367 let struct_size = unsafe { descriptor.as_ptr().cast::<usize>().read_unaligned() };
371 if struct_size < core::mem::size_of::<AlefComponentV1>() {
372 return Err(ComponentError::InvalidDescriptor("descriptor is smaller than ABI v1"));
373 }
374 Ok(())
375}
376
377unsafe fn validate_descriptor(
378 descriptor: &AlefComponentV1,
379 requirements: &ComponentRequirements,
380) -> Result<(String, String), ComponentError> {
381 if descriptor.struct_size < core::mem::size_of::<AlefComponentV1>() {
382 return Err(ComponentError::InvalidDescriptor("descriptor is smaller than ABI v1"));
383 }
384 if descriptor.abi_major != ABI_MAJOR_V1 || descriptor.abi_minor > ABI_MINOR_V1 {
385 return Err(ComponentError::IncompatibleAbi {
386 expected_major: ABI_MAJOR_V1,
387 expected_minor: ABI_MINOR_V1,
388 actual_major: descriptor.abi_major,
389 actual_minor: descriptor.abi_minor,
390 });
391 }
392 let component_id = unsafe { copy_abi_str(descriptor.component_id)? };
393 let component_version = unsafe { copy_abi_str(descriptor.component_version)? };
394 if component_id != requirements.component_id {
395 return Err(ComponentError::IdentityMismatch {
396 expected: requirements.component_id.clone(),
397 actual: component_id,
398 });
399 }
400 if descriptor.contract_hash != requirements.contract_hash {
401 return Err(ComponentError::ContractHashMismatch);
402 }
403 if requirements
404 .feature_set_hash
405 .is_some_and(|expected| descriptor.feature_set_hash != expected)
406 {
407 return Err(ComponentError::FeatureSetHashMismatch);
408 }
409 if descriptor.contract_size > 0 && descriptor.contract.is_null() {
410 return Err(ComponentError::InvalidDescriptor(
411 "non-empty contract has a null pointer",
412 ));
413 }
414 Ok((component_id, component_version))
415}
416
417unsafe fn copy_abi_str(value: alef_component_abi::AlefStr) -> Result<String, ComponentError> {
418 if value.len > MAX_DESCRIPTOR_STRING {
419 return Err(ComponentError::StringTooLong);
420 }
421 if value.len == 0 {
422 return Ok(String::new());
423 }
424 if value.ptr.is_null() {
425 return Err(ComponentError::InvalidDescriptor("non-empty string has a null pointer"));
426 }
427 let bytes = unsafe { core::slice::from_raw_parts(value.ptr.cast::<u8>(), value.len) };
428 let text = core::str::from_utf8(bytes).map_err(|_| ComponentError::InvalidUtf8)?;
429 Ok(text.to_owned())
430}
431
432pub(crate) fn decode_hash(value: &str) -> Result<[u8; 32], ComponentError> {
433 let bytes = hex::decode(value).map_err(|_| ComponentError::InvalidDigest(value.to_owned()))?;
434 bytes
435 .try_into()
436 .map_err(|_| ComponentError::InvalidDigest(value.to_owned()))
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use alef_component_abi::{AlefComponentV1, AlefStr};
443 use std::ffi::c_char;
444
445 static ID: &[u8] = b"demo";
446 static VERSION: &[u8] = b"1.2.3";
447
448 #[repr(C)]
449 struct DemoContract {
450 answer: u32,
451 }
452
453 unsafe impl AlefContract for DemoContract {
454 const CONTRACT_HASH: [u8; 32] = [7; 32];
455 }
456
457 static CONTRACT: DemoContract = DemoContract { answer: 42 };
458
459 fn descriptor(contract_hash: [u8; 32]) -> AlefComponentV1 {
460 AlefComponentV1 {
461 struct_size: core::mem::size_of::<AlefComponentV1>(),
462 abi_major: ABI_MAJOR_V1,
463 abi_minor: ABI_MINOR_V1,
464 component_id: AlefStr {
465 ptr: ID.as_ptr().cast::<c_char>(),
466 len: ID.len(),
467 },
468 component_version: AlefStr {
469 ptr: VERSION.as_ptr().cast::<c_char>(),
470 len: VERSION.len(),
471 },
472 contract_hash,
473 feature_set_hash: [8; 32],
474 contract: core::ptr::dangling(),
475 contract_size: 1,
476 create: None,
477 destroy: None,
478 }
479 }
480
481 fn requirements() -> ComponentRequirements {
482 ComponentRequirements {
483 component_id: "demo".into(),
484 contract_name: "engine".into(),
485 contract_hash: [7; 32],
486 feature_set_hash: Some([8; 32]),
487 }
488 }
489
490 #[test]
491 fn accepts_matching_descriptor() {
492 let descriptor = descriptor([7; 32]);
493 let validated = unsafe { validate_descriptor(&descriptor, &requirements()) }.unwrap();
494 assert_eq!(validated, ("demo".into(), "1.2.3".into()));
495 }
496
497 #[test]
498 fn rejects_wrong_contract_hash() {
499 let descriptor = descriptor([9; 32]);
500 assert!(matches!(
501 unsafe { validate_descriptor(&descriptor, &requirements()) },
502 Err(ComponentError::ContractHashMismatch)
503 ));
504 }
505
506 #[test]
507 fn rejects_short_descriptor_before_optional_fields_are_read() {
508 let mut descriptor = descriptor([7; 32]);
509 descriptor.struct_size = 8;
510 assert!(matches!(
511 unsafe { validate_descriptor(&descriptor, &requirements()) },
512 Err(ComponentError::InvalidDescriptor(_))
513 ));
514 }
515
516 #[test]
522 fn rejects_a_descriptor_backed_by_memory_too_small_to_hold_the_full_struct() {
523 let layout = std::alloc::Layout::from_size_align(
524 core::mem::size_of::<usize>(),
525 core::mem::align_of::<AlefComponentV1>(),
526 )
527 .unwrap();
528 let raw = unsafe { std::alloc::alloc(layout) };
530 assert!(!raw.is_null(), "test allocation failed");
531 unsafe { raw.cast::<usize>().write_unaligned(8) };
534 let descriptor = NonNull::new(raw).unwrap().cast::<AlefComponentV1>();
535
536 let result = unsafe { reject_undersized_descriptor(descriptor) };
539
540 unsafe { std::alloc::dealloc(raw, layout) };
542 assert!(matches!(result, Err(ComponentError::InvalidDescriptor(_))));
543 }
544
545 #[test]
546 fn manifest_validation_checks_feature_identity() {
547 let manifest = ComponentManifest {
548 schema_version: COMPONENT_MANIFEST_SCHEMA,
549 abi_version: 1,
550 identity: crate::ComponentIdentity {
551 crate_name: "demo-core".into(),
552 component: "demo".into(),
553 version: "1.2.3".into(),
554 target: "test-target".into(),
555 feature_hash: hex::encode([9; 32]),
556 contract_hash: hex::encode([7; 32]),
557 },
558 provides: vec![crate::ComponentProvidedContract {
559 contract: "engine".into(),
560 interface_version: 1,
561 contract_hash: hex::encode([7; 32]),
562 implementation: "demo::Engine".into(),
563 }],
564 features: Vec::new(),
565 default_features: false,
566 library: crate::ComponentLibrary {
567 file: "libdemo.so".into(),
568 sha256: hex::encode([0; 32]),
569 size: 0,
570 },
571 };
572 assert!(matches!(
573 validate_manifest(&manifest, &requirements()),
574 Err(ComponentError::FeatureSetHashMismatch)
575 ));
576 }
577
578 #[test]
579 fn typed_contract_table_checks_hash_size_and_alignment() {
580 let mut descriptor = descriptor(DemoContract::CONTRACT_HASH);
581 descriptor.contract = (&raw const CONTRACT).cast();
582 descriptor.contract_size = core::mem::size_of::<DemoContract>();
583 assert_eq!(
584 contract_table_from_descriptor::<DemoContract>(&descriptor)
585 .unwrap()
586 .answer,
587 42
588 );
589
590 descriptor.contract_hash = [9; 32];
591 assert!(matches!(
592 contract_table_from_descriptor::<DemoContract>(&descriptor),
593 Err(ComponentError::InvalidContractTable)
594 ));
595 }
596}