#![allow(clippy::result_large_err)]
mod dylink;
mod error;
mod instance_group;
mod internal_types;
mod linker_state;
mod locator;
mod memory_allocator;
mod runtime_hooks;
mod sync;
mod types;
mod wasm_utils;
pub use dylink::*;
pub use error::*;
pub use types::*;
use instance_group::*;
use internal_types::*;
use linker_state::*;
use locator::*;
use memory_allocator::*;
use runtime_hooks::instantiate_with_runtime_hooks;
use sync::*;
use wasm_utils::*;
use std::{
collections::{BTreeMap, HashMap},
ops::DerefMut,
path::Path,
sync::{Arc, Mutex, MutexGuard, atomic::Ordering},
};
use bus::Bus;
use tracing::trace;
use wasmer::{AsStoreMut, Engine, FunctionEnvMut, Memory, Module, StoreMut, Tag, Type};
use wasmer_wasix_types::wasix::WasiMemoryLayout;
use crate::{WasiEnv, WasiFunctionEnv, WasiModuleTreeHandles, import_object_for_all_wasi_versions};
use super::WasiModuleInstanceHandles;
pub static MAIN_MODULE_HANDLE: ModuleHandle = ModuleHandle(1);
static INVALID_MODULE_HANDLE: ModuleHandle = ModuleHandle(u32::MAX);
static MAIN_MODULE_TABLE_BASE: u64 = 1;
#[derive(Clone)]
pub struct Linker {
shared: LinkerShared,
instance_group_state: Arc<Mutex<Option<InstanceGroupState>>>,
}
impl std::fmt::Debug for Linker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Linker").finish()
}
}
impl Linker {
pub fn new(
engine: Engine,
main_module: &Module,
store: &mut StoreMut<'_>,
memory: Option<Memory>,
func_env: &mut WasiFunctionEnv,
stack_size: u64,
ld_library_path: &[&Path],
) -> Result<(Self, LinkedMainModule), LinkError> {
let dylink_section = parse_dylink0_section(main_module)?;
trace!(?dylink_section, "Loading main module");
let mut imports = import_object_for_all_wasi_versions(main_module, store, &func_env.env);
let function_table_type = main_module_function_table_type(main_module)?;
let expected_table_length =
dylink_section.mem_info.table_size + MAIN_MODULE_TABLE_BASE as u32;
let indirect_function_table =
create_indirect_function_table(store, function_table_type, expected_table_length)?;
let memory_base = 2u64.pow(dylink_section.mem_info.memory_alignment);
let memory_type = main_module_memory_type(main_module)?;
let memory = match memory {
Some(m) => m,
None => Memory::new(store, memory_type)?,
};
let stack_low = {
let data_end = memory_base + dylink_section.mem_info.memory_size as u64;
if !data_end.is_multiple_of(1024) {
data_end + 1024 - (data_end % 1024)
} else {
data_end
}
};
if !stack_size.is_multiple_of(1024) {
panic!("Stack size must be 1024-bit aligned");
}
let stack_high = stack_low + stack_size;
memory.grow_at_least(store, stack_high)?;
trace!(
memory_pages = ?memory.grow(store, 0).unwrap(),
memory_base,
stack_low,
stack_high,
"Memory layout"
);
let stack_pointer = create_main_stack_pointer_global(store, main_module, stack_high)?;
let c_longjmp = Tag::new(store, vec![Type::I32]);
let cpp_exception = Tag::new(store, vec![Type::I32]);
let mut barrier_tx = Bus::new(1);
let barrier_rx = barrier_tx.add_rx();
let mut operation_tx = Bus::new(1);
let operation_rx = operation_tx.add_rx();
let mut instance_group = InstanceGroupState {
main_instance: None,
main_instance_tls_base: None,
side_instances: HashMap::new(),
stack_pointer,
memory: memory.clone(),
indirect_function_table: indirect_function_table.clone(),
c_longjmp,
cpp_exception,
recv_pending_operation_barrier: barrier_rx,
recv_pending_operation: operation_rx,
};
let mut linker_state = LinkerState {
engine,
main_module: main_module.clone(),
main_module_dylink_info: dylink_section,
main_module_memory_base: memory_base,
side_modules: BTreeMap::new(),
side_modules_by_name: HashMap::new(),
next_module_handle: MAIN_MODULE_HANDLE.0 + 1,
memory_allocator: MemoryAllocator::new(),
allocated_closure_functions: BTreeMap::new(),
available_closure_functions: Vec::new(),
heap_base: stack_high,
symbol_resolution_records: HashMap::new(),
send_pending_operation_barrier: barrier_tx,
send_pending_operation: operation_tx,
};
let mut link_state = InProgressLinkState::default();
let well_known_imports = [
("env", "__memory_base", memory_base),
("env", "__table_base", MAIN_MODULE_TABLE_BASE),
("GOT.mem", "__stack_high", stack_high),
("GOT.mem", "__stack_low", stack_low),
("GOT.mem", "__heap_base", stack_high),
];
trace!("Resolving main module's symbols");
linker_state.resolve_symbols(
&instance_group,
store,
main_module,
MAIN_MODULE_HANDLE,
&mut link_state,
&well_known_imports,
)?;
trace!("Populating main module's imports object");
instance_group.populate_imports_from_link_state(
MAIN_MODULE_HANDLE,
&mut linker_state,
&mut link_state,
store,
main_module,
&mut imports,
&func_env.env,
&well_known_imports,
)?;
let main_instance = instantiate_with_runtime_hooks(
&func_env.env,
store,
main_module,
&mut imports,
&memory,
)?;
instance_group.main_instance = Some(main_instance.clone());
let tls_base = get_tls_base_export(&main_instance, store)?;
instance_group.main_instance_tls_base = tls_base;
let runtime_path = linker_state.main_module_dylink_info.runtime_path.clone();
for needed in linker_state.main_module_dylink_info.needed.clone() {
trace!(name = needed, "Loading module needed by main");
let wasi_env = func_env.data(store);
linker_state.load_module_tree(
DlModuleSpec::FileSystem {
module_spec: Path::new(needed.as_str()),
ld_library_path,
},
&mut link_state,
&wasi_env.runtime,
&wasi_env.state,
runtime_path.as_ref(),
Some(Path::new("./main.wasm")),
)?;
}
for module_handle in link_state
.new_modules
.iter()
.map(|m| m.handle)
.collect::<Vec<_>>()
{
trace!(?module_handle, "Instantiating module");
instance_group.instantiate_side_module_from_link_state(
&mut linker_state,
store,
&func_env.env,
&mut link_state,
module_handle,
)?;
}
let linker = Self {
shared: LinkerShared::new(linker_state),
instance_group_state: Arc::new(Mutex::new(Some(instance_group))),
};
let stack_layout = WasiMemoryLayout {
stack_lower: stack_low,
stack_upper: stack_high,
stack_size: stack_high - stack_low,
guard_size: 0,
tls_base,
};
let module_handles = WasiModuleTreeHandles::Dynamic {
linker: linker.clone(),
main_module_instance_handles: WasiModuleInstanceHandles::new(
memory.clone(),
store,
main_instance.clone(),
Some(indirect_function_table.clone()),
),
};
func_env
.initialize_handles_and_layout(
store,
main_instance.clone(),
module_handles,
Some(stack_layout),
true,
)
.map_err(LinkError::MainModuleHandleInitFailed)?;
{
trace!(?link_state, "Finalizing linking of main module");
let mut group_guard = linker.instance_group_state.lock().unwrap();
unsafe {
linker.shared.bootstrap_exclusive_write_then(|ls| {
let group_state = group_guard.as_mut().unwrap();
group_state.finalize_pending_globals(
ls,
store,
&link_state.unresolved_globals,
)?;
trace!("Calling data relocator function for main module");
call_initialization_function::<()>(
&main_instance,
store,
"__wasm_apply_data_relocs",
)?;
call_initialization_function::<()>(
&main_instance,
store,
"__wasm_apply_tls_relocs",
)?;
linker.initialize_new_modules(group_guard, store, link_state)
})?;
}
}
trace!("Calling main module's _initialize function");
call_initialization_function::<()>(&main_instance, store, "_initialize")?;
trace!("Link complete");
Ok((
linker,
LinkedMainModule {
instance: main_instance,
memory,
indirect_function_table,
stack_low,
stack_high,
},
))
}
pub fn prepare_for_instance_group(
&self,
parent_ctx: &mut FunctionEnvMut<'_, WasiEnv>,
) -> Result<PreparedInstanceGroupData, LinkError> {
trace!("Preparing for new instance group");
lock_instance_group_state!(
parent_group_state_guard,
parent_group_state,
self,
LinkError::InstanceGroupIsDead
);
let env = parent_ctx.as_ref();
let mut store = parent_ctx.as_store_mut();
let topology_token =
self.shared
.acquire_topology_token(parent_group_state, &mut store, &env)?;
let parent_store = parent_ctx.as_store_mut();
let memory = parent_group_state
.memory
.as_shared(&parent_store)
.ok_or_else(|| LinkError::MemoryNotShared)?;
let indirect_function_table_type =
parent_group_state.indirect_function_table.ty(&parent_store);
let expected_table_length = parent_group_state
.indirect_function_table
.size(&parent_store);
Ok(PreparedInstanceGroupData {
linker_shared: self.shared.clone(),
topology_token,
memory,
indirect_function_table_type,
expected_table_length,
})
}
pub(crate) fn do_pending_link_operations(
&self,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
fast: bool,
) -> Result<(), LinkError> {
if !self.shared.dl_operation_pending_load(if fast {
Ordering::Relaxed
} else {
Ordering::SeqCst
}) {
return Ok(());
}
lock_instance_group_state!(guard, group_state, self, LinkError::InstanceGroupIsDead);
let env = ctx.as_ref();
let mut store = ctx.as_store_mut();
self.shared
.do_pending_link_operations_internal(group_state, &mut store, &env)
}
pub fn create_instance_group(
prepared_instance_group_data: PreparedInstanceGroupData,
store: &mut StoreMut<'_>,
func_env: &mut WasiFunctionEnv,
) -> Result<(Self, LinkedMainModule), LinkError> {
trace!("Spawning new instance group");
let PreparedInstanceGroupData {
linker_shared,
topology_token,
memory,
indirect_function_table_type,
expected_table_length,
} = prepared_instance_group_data;
let (topology_hold, mut ls_write) =
linker_shared.write_linker_state_blocking_holding_topology(topology_token);
let main_module = ls_write.main_module.clone();
let mut imports = import_object_for_all_wasi_versions(&main_module, store, &func_env.env);
let memory = memory.attach(store);
let indirect_function_table = create_indirect_function_table(
store,
indirect_function_table_type,
expected_table_length,
)?;
let (stack_low, stack_high, tls_base) = {
let layout = &func_env.env.as_ref(store).layout;
(
layout.stack_lower,
layout.stack_upper,
layout.tls_base.expect(
"tls_base must be set in memory layout of new instance group's main instance",
),
)
};
trace!(stack_low, stack_high, "Memory layout");
let stack_pointer = create_main_stack_pointer_global(store, &main_module, 0)?;
let c_longjmp = Tag::new(store, vec![Type::I32]);
let cpp_exception = Tag::new(store, vec![Type::I32]);
let barrier_rx = ls_write.send_pending_operation_barrier.add_rx();
let operation_rx = ls_write.send_pending_operation.add_rx();
let mut instance_group = InstanceGroupState {
main_instance: None,
main_instance_tls_base: Some(tls_base),
side_instances: HashMap::new(),
stack_pointer,
memory: memory.clone(),
indirect_function_table: indirect_function_table.clone(),
c_longjmp,
cpp_exception,
recv_pending_operation_barrier: barrier_rx,
recv_pending_operation: operation_rx,
};
let mut pending_resolutions = PendingResolutionsFromLinker::default();
let well_known_imports = [
("env", "__memory_base", ls_write.main_module_memory_base),
("env", "__table_base", MAIN_MODULE_TABLE_BASE),
("GOT.mem", "__stack_high", stack_high),
("GOT.mem", "__stack_low", stack_low),
("GOT.mem", "__heap_base", ls_write.heap_base),
];
trace!("Populating imports object for new instance group's main instance");
instance_group.populate_imports_from_linker(
MAIN_MODULE_HANDLE,
&ls_write,
store,
&main_module,
&mut imports,
&func_env.env,
&well_known_imports,
&mut pending_resolutions,
)?;
let main_instance = instantiate_with_runtime_hooks(
&func_env.env,
store,
&main_module,
&mut imports,
&memory,
)?;
instance_group.main_instance = Some(main_instance.clone());
let instance_group_state = Arc::new(Mutex::new(Some(instance_group)));
let linker = Self {
shared: linker_shared.clone(),
instance_group_state: instance_group_state.clone(),
};
let module_handles = WasiModuleTreeHandles::Dynamic {
linker: linker.clone(),
main_module_instance_handles: WasiModuleInstanceHandles::new(
memory.clone(),
store,
main_instance.clone(),
Some(indirect_function_table.clone()),
),
};
func_env
.initialize_handles_and_layout(
store,
main_instance.clone(),
module_handles,
None,
false,
)
.map_err(LinkError::MainModuleHandleInitFailed)?;
let side_module_handles: Vec<ModuleHandle> =
ls_write.side_modules.keys().copied().collect();
for module_handle in side_module_handles {
trace!(?module_handle, "Instantiating existing side module");
let prepared = {
let mut guard = instance_group_state.lock().unwrap();
let group = guard
.as_mut()
.expect("Internal error: instance group state was cleared during spawn");
group.prepare_side_module_from_linker(
&ls_write,
store,
&func_env.env,
module_handle,
&mut pending_resolutions,
)?
};
let tls_base =
call_initialization_function::<i32>(&prepared.instance, store, "__wasix_init_tls")?
.map(|v| v as u64);
{
let mut guard = instance_group_state.lock().unwrap();
let group = guard
.as_mut()
.expect("Internal error: instance group state was cleared during spawn");
group.complete_side_module_from_linker(prepared, tls_base, store)?;
}
}
trace!("Finalizing pending functions");
{
let guard = instance_group_state.lock().unwrap();
let group = guard
.as_ref()
.expect("Internal error: instance group state was cleared during spawn");
group.finalize_pending_resolutions_from_linker(&pending_resolutions, store)?;
}
trace!("Applying externally-requested function table entries");
{
let guard = instance_group_state.lock().unwrap();
let group = guard
.as_ref()
.expect("Internal error: instance group state was cleared during spawn");
group.apply_requested_symbols_from_linker(store, &ls_write)?;
}
drop(ls_write);
drop(topology_hold);
trace!("Instance group spawned successfully");
Ok((
linker,
LinkedMainModule {
instance: main_instance,
memory,
indirect_function_table,
stack_low,
stack_high,
},
))
}
pub fn shutdown_instance_group(
&self,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
) -> Result<(), LinkError> {
trace!("Shutting instance group down");
let mut guard = self.instance_group_state.lock().unwrap();
match guard.as_mut() {
None => Ok(()),
Some(group_state) => {
let linker_state = self.shared.write_linker_state(group_state, ctx)?;
guard.take();
drop(linker_state);
trace!("Instance group shut down");
Ok(())
}
}
}
pub fn allocate_closure_index(
&self,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
) -> Result<u32, LinkError> {
lock_instance_group_state!(
group_state_guard,
group_state,
self,
LinkError::InstanceGroupIsDead
);
let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
if let Some(function_index) = linker_state.available_closure_functions.pop() {
linker_state
.allocated_closure_functions
.insert(function_index, true);
return Ok(function_index);
}
drop(linker_state);
let (topology_token, mut linker_state) = self
.shared
.write_linker_state_with_topology(group_state, ctx)?;
let mut store = ctx.as_store_mut();
if let Some(function_index) = linker_state.available_closure_functions.pop() {
linker_state
.allocated_closure_functions
.insert(function_index, true);
drop(linker_state);
drop(topology_token);
return Ok(function_index);
}
const CLOSURE_ALLOCATION_SIZE: u32 = 100;
let function_index = group_state
.allocate_function_table(&mut store, CLOSURE_ALLOCATION_SIZE, 0)
.map_err(LinkError::TableAllocationError)? as u32;
linker_state
.available_closure_functions
.reserve(CLOSURE_ALLOCATION_SIZE as usize - 1);
for i in 1..CLOSURE_ALLOCATION_SIZE {
linker_state
.available_closure_functions
.push(function_index + i);
linker_state
.allocated_closure_functions
.insert(function_index + i, false);
}
linker_state
.allocated_closure_functions
.insert(function_index, true);
self.shared.synchronize_link_operation(
topology_token,
DlOperation::AllocateFunctionTable {
index: function_index,
size: CLOSURE_ALLOCATION_SIZE,
},
linker_state,
group_state,
&ctx.data().process,
ctx.data().tid(),
);
Ok(function_index)
}
pub fn free_closure_index(
&self,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
function_id: u32,
) -> Result<(), LinkError> {
lock_instance_group_state!(
group_state_guard,
group_state,
self,
LinkError::InstanceGroupIsDead
);
let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
let Some(entry) = linker_state
.allocated_closure_functions
.get_mut(&function_id)
else {
return Ok(());
};
if !*entry {
return Ok(());
}
*entry = false;
linker_state.available_closure_functions.push(function_id);
Ok(())
}
pub fn is_closure(
&self,
function_id: u32,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
) -> Result<bool, LinkError> {
if let Ok(linker_state) = self.shared.try_read_linker_state() {
return Ok(linker_state
.allocated_closure_functions
.contains_key(&function_id));
}
lock_instance_group_state!(
group_state_guard,
group_state,
self,
LinkError::InstanceGroupIsDead
);
let linker_state = self.shared.write_linker_state(group_state, ctx)?;
Ok(linker_state
.allocated_closure_functions
.contains_key(&function_id))
}
pub fn load_module(
&self,
module_spec: DlModuleSpec,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
) -> Result<ModuleHandle, LinkError> {
trace!(?module_spec, "Loading module");
lock_instance_group_state!(
group_state_guard,
group_state,
self,
LinkError::InstanceGroupIsDead
);
let (topology_token, mut linker_state) = self
.shared
.write_linker_state_with_topology(group_state, ctx)?;
let mut link_state = InProgressLinkState::default();
let env = ctx.as_ref();
let mut store = ctx.as_store_mut();
trace!("Loading module tree for requested module");
let wasi_env = env.as_ref(&store);
let runtime_path: &[String] = &[];
let module_handle = linker_state.load_module_tree(
module_spec,
&mut link_state,
&wasi_env.runtime,
&wasi_env.state,
runtime_path, Option::<&Path>::None, )?;
let new_modules = link_state
.new_modules
.iter()
.map(|m| m.handle)
.collect::<Vec<_>>();
for handle in &new_modules {
trace!(?module_handle, "Instantiating module");
group_state.instantiate_side_module_from_link_state(
&mut linker_state,
&mut store,
&env,
&mut link_state,
*handle,
)?;
}
trace!("Finalizing link");
self.finalize_link_operation(group_state_guard, &mut linker_state, &mut store, link_state)?;
if !new_modules.is_empty() {
lock_instance_group_state!(
group_state_guard,
group_state,
self,
LinkError::InstanceGroupIsDead
);
self.shared.synchronize_link_operation(
topology_token,
DlOperation::LoadModules(new_modules),
linker_state,
group_state,
&ctx.data().process,
ctx.data().tid(),
);
}
trace!("Module load complete");
Ok(module_handle)
}
fn finalize_link_operation(
&self,
mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
linker_state: &mut LinkerState,
store: &mut impl AsStoreMut,
link_state: InProgressLinkState,
) -> Result<(), LinkError> {
let group_state = group_state_guard.as_mut().unwrap();
trace!(?link_state, "Finalizing link operation");
group_state.finalize_pending_globals(
linker_state,
store,
&link_state.unresolved_globals,
)?;
self.initialize_new_modules(group_state_guard, store, link_state)
}
fn initialize_new_modules(
&self,
mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
store: &mut impl AsStoreMut,
link_state: InProgressLinkState,
) -> Result<(), LinkError> {
let group_state = group_state_guard.as_mut().unwrap();
let new_instances = link_state
.new_modules
.iter()
.map(|m| group_state.side_instances[&m.handle].instance.clone())
.collect::<Vec<_>>();
drop(group_state_guard);
trace!("Calling data relocation functions");
for instance in &new_instances {
call_initialization_function::<()>(instance, store, "__wasm_apply_data_relocs")?;
call_initialization_function::<()>(instance, store, "__wasm_apply_tls_relocs")?;
}
trace!("Calling ctor functions");
for instance in &new_instances {
call_initialization_function::<()>(instance, store, "__wasm_call_ctors")?;
}
Ok(())
}
pub fn resolve_export(
&self,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
module_handle: Option<ModuleHandle>,
symbol: &str,
) -> Result<ResolvedExport, ResolveError> {
trace!(?module_handle, symbol, "Resolving symbol");
let resolution_key = SymbolResolutionKey::Requested {
resolve_from: module_handle,
name: symbol.to_string(),
};
lock_instance_group_state!(guard, group_state, self, ResolveError::InstanceGroupIsDead);
if let Ok(linker_state) = self.shared.try_read_linker_state()
&& let Some(resolution) = linker_state.symbol_resolution_records.get(&resolution_key)
{
trace!(?resolution, "Already have a resolution for this symbol");
match resolution {
SymbolResolutionResult::FunctionPointer {
function_table_index: addr,
..
} => {
return Ok(ResolvedExport::Function {
func_ptr: *addr as u64,
});
}
SymbolResolutionResult::Memory(addr) => {
return Ok(ResolvedExport::Global { data_ptr: *addr });
}
SymbolResolutionResult::Tls {
resolved_from,
offset,
} => {
let Some(tls_base) = group_state.tls_base(*resolved_from) else {
return Err(ResolveError::NoTlsBaseGlobalExport);
};
return Ok(ResolvedExport::Global {
data_ptr: tls_base + offset,
});
}
r => panic!(
"Internal error: unexpected symbol resolution \
{r:?} for requested symbol {symbol}"
),
}
}
let (topology_token, mut linker_state) = self
.shared
.write_linker_state_with_topology(group_state, ctx)?;
let mut store = ctx.as_store_mut();
trace!("Resolving export");
let (export, resolved_from) =
group_state.resolve_export(&linker_state, &mut store, module_handle, symbol, false)?;
trace!(?export, ?resolved_from, "Resolved export");
match export {
PartiallyResolvedExport::Global(addr) => {
linker_state
.symbol_resolution_records
.insert(resolution_key, SymbolResolutionResult::Memory(addr));
Ok(ResolvedExport::Global { data_ptr: addr })
}
PartiallyResolvedExport::Tls { offset, final_addr } => {
linker_state.symbol_resolution_records.insert(
resolution_key,
SymbolResolutionResult::Tls {
resolved_from,
offset,
},
);
Ok(ResolvedExport::Global {
data_ptr: final_addr,
})
}
PartiallyResolvedExport::Function(func) => {
let func_ptr = group_state
.append_to_function_table(&mut store, func.clone())
.map_err(ResolveError::TableAllocationError)?;
trace!(
?func_ptr,
table_size = group_state.indirect_function_table.size(&store),
"Placed resolved function into table"
);
linker_state.symbol_resolution_records.insert(
resolution_key,
SymbolResolutionResult::FunctionPointer {
resolved_from,
function_table_index: func_ptr,
},
);
self.shared.synchronize_link_operation(
topology_token,
DlOperation::ResolveFunction {
name: symbol.to_string(),
resolved_from,
function_table_index: func_ptr,
},
linker_state,
group_state,
&ctx.data().process,
ctx.data().tid(),
);
Ok(ResolvedExport::Function {
func_ptr: func_ptr as u64,
})
}
}
}
pub fn is_handle_valid(
&self,
handle: ModuleHandle,
ctx: &mut FunctionEnvMut<'_, WasiEnv>,
) -> Result<bool, LinkError> {
if let Ok(linker_state) = self.shared.try_read_linker_state() {
return Ok(linker_state.side_modules.contains_key(&handle));
}
lock_instance_group_state!(guard, group_state, self, LinkError::InstanceGroupIsDead);
let linker_state = self.shared.write_linker_state(group_state, ctx)?;
Ok(linker_state.side_modules.contains_key(&handle))
}
}