Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 326795f

Browse files
authored
Make direct calls to statically-known imported functions (#11228)
* Make direct calls to statically-known imported functions This will allow for their eventual inlining. * address review feedback and fix up some disas tests * Add native code disassembly test * disas: sort `read_dir` entries for deterministic order Because we always use namespace 0 for wasm functions when creating CLIF functions, sorting the parsed CLIF functions by (namespace, index) does not differentiate between the `index`th Wasm function in two different modules in the same component, meaning we would pass through whatever ordering reading from the filesystem gave us, which can be non-deterministic across platforms. Instead, sort the directory entries based on file paths. * fix clippy
1 parent 3da19a9 commit 326795f

19 files changed

Lines changed: 949 additions & 307 deletions

crates/cranelift/src/func_environ.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ pub struct FuncEnvironment<'module_environment> {
118118
#[cfg(feature = "gc")]
119119
gc_heap_bound: Option<ir::GlobalValue>,
120120

121-
#[cfg(feature = "wmemcheck")]
122121
translation: &'module_environment ModuleTranslation<'module_environment>,
123122

124123
/// Heaps implementing WebAssembly linear memories.
@@ -224,7 +223,6 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
224223
// functions should consume at least some fuel.
225224
fuel_consumed: 1,
226225

227-
#[cfg(feature = "wmemcheck")]
228226
translation,
229227

230228
stack_limit_at_function_entry: None,
@@ -1321,26 +1319,34 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
13211319
.vmctx_vmfunction_import_wasm_call(callee_index),
13221320
)
13231321
.unwrap();
1324-
let func_addr = self
1325-
.builder
1326-
.ins()
1327-
.load(pointer_type, mem_flags, base, body_offset);
13281322

13291323
// First append the callee vmctx address.
13301324
let vmctx_offset =
13311325
i32::try_from(self.env.offsets.vmctx_vmfunction_import_vmctx(callee_index)).unwrap();
1332-
let vmctx = self
1326+
let callee_vmctx = self
13331327
.builder
13341328
.ins()
13351329
.load(pointer_type, mem_flags, base, vmctx_offset);
1336-
real_call_args.push(vmctx);
1330+
real_call_args.push(callee_vmctx);
13371331
real_call_args.push(caller_vmctx);
13381332

13391333
// Then append the regular call arguments.
13401334
real_call_args.extend_from_slice(call_args);
13411335

1342-
// Finally, make the indirect call!
1343-
Ok(self.indirect_call_inst(sig_ref, func_addr, &real_call_args))
1336+
// If we statically know the imported function (e.g. this is a
1337+
// component-to-component call where we statically know both components)
1338+
// then we can actually still make a direct call (although we do have to
1339+
// pass the callee's vmctx that we just loaded, not our own). Otherwise,
1340+
// we really do an indirect call.
1341+
if self.env.translation.known_imported_functions[callee_index].is_some() {
1342+
Ok(self.direct_call_inst(callee, &real_call_args))
1343+
} else {
1344+
let func_addr = self
1345+
.builder
1346+
.ins()
1347+
.load(pointer_type, mem_flags, base, body_offset);
1348+
Ok(self.indirect_call_inst(sig_ref, func_addr, &real_call_args))
1349+
}
13441350
}
13451351

13461352
/// Do an indirect call through the given funcref table.
@@ -2651,7 +2657,8 @@ impl FuncEnvironment<'_> {
26512657
// wasm module (e.g. imports or libcalls) are either encoded through
26522658
// the `vmcontext` as relative jumps (hence no relocations) or
26532659
// they're libcalls with absolute relocations.
2654-
colocated: self.module.defined_func_index(index).is_some(),
2660+
colocated: self.module.defined_func_index(index).is_some()
2661+
|| self.translation.known_imported_functions[index].is_some(),
26552662
}))
26562663
}
26572664

crates/environ/src/compile/module_environ.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ use crate::module::{
22
FuncRefIndex, Initializer, MemoryInitialization, MemoryInitializer, Module, TableSegment,
33
TableSegmentElements,
44
};
5-
use crate::prelude::*;
65
use crate::{
76
ConstExpr, ConstOp, DataIndex, DefinedFuncIndex, ElemIndex, EngineOrModuleTypeIndex,
87
EntityIndex, EntityType, FuncIndex, GlobalIndex, IndexType, InitMemory, MemoryIndex,
98
ModuleInternedTypeIndex, ModuleTypesBuilder, PrimaryMap, SizeOverflow, StaticMemoryInitializer,
109
TableIndex, TableInitialValue, Tag, TagIndex, Tunables, TypeConvert, TypeIndex, Unsigned,
1110
WasmError, WasmHeapTopType, WasmHeapType, WasmResult, WasmValType, WasmparserTypeConverter,
1211
};
12+
use crate::{StaticModuleIndex, prelude::*};
1313
use anyhow::{Result, bail};
14+
use cranelift_entity::SecondaryMap;
1415
use cranelift_entity::packed_option::ReservedValue;
1516
use std::borrow::Cow;
1617
use std::collections::HashMap;
@@ -54,6 +55,13 @@ pub struct ModuleTranslation<'data> {
5455
/// References to the function bodies.
5556
pub function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
5657

58+
/// For each imported function, the single statically-known defined function
59+
/// that satisfies that import, if any. This is used to turn what would
60+
/// otherwise be indirect calls through the imports table into direct calls,
61+
/// when possible.
62+
pub known_imported_functions:
63+
SecondaryMap<FuncIndex, Option<(StaticModuleIndex, DefinedFuncIndex)>>,
64+
5765
/// A list of type signatures which are considered exported from this
5866
/// module, or those that can possibly be called. This list is sorted, and
5967
/// trampolines for each of these signatures are required.

crates/environ/src/component/dfg.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,40 @@ pub enum SideEffect {
159159
Resource(DefinedResourceIndex),
160160
}
161161

162+
/// A sound approximation of a particular module's set of instantiations.
163+
///
164+
/// This type forms a simple lattice that we can use in static analyses that in
165+
/// turn let us specialize a module's compilation to exactly the imports it is
166+
/// given.
167+
#[derive(Clone, Copy, Default)]
168+
pub enum AbstractInstantiations<'a> {
169+
/// The associated module is instantiated many times.
170+
Many,
171+
172+
/// The module is instantiated exactly once, with the given definitions as
173+
/// arguments to that instantiation.
174+
One(&'a [info::CoreDef]),
175+
176+
/// The module is never instantiated.
177+
#[default]
178+
None,
179+
}
180+
181+
impl AbstractInstantiations<'_> {
182+
/// Join two facts about a particular module's instantiation together.
183+
///
184+
/// This is the least-upper-bound operation on the lattice.
185+
pub fn join(&mut self, other: Self) {
186+
*self = match (*self, other) {
187+
(Self::Many, _) | (_, Self::Many) => Self::Many,
188+
(Self::One(a), Self::One(b)) if a == b => Self::One(a),
189+
(Self::One(_), Self::One(_)) => Self::Many,
190+
(Self::One(a), Self::None) | (Self::None, Self::One(a)) => Self::One(a),
191+
(Self::None, Self::None) => Self::None,
192+
}
193+
}
194+
}
195+
162196
macro_rules! id {
163197
($(pub struct $name:ident(u32);)*) => ($(
164198
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]

crates/environ/src/component/translate.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::ScopeVec;
2+
use crate::component::dfg::AbstractInstantiations;
23
use crate::component::*;
34
use crate::prelude::*;
45
use crate::{
@@ -8,6 +9,8 @@ use crate::{
89
};
910
use anyhow::anyhow;
1011
use anyhow::{Result, bail};
12+
use cranelift_entity::SecondaryMap;
13+
use cranelift_entity::packed_option::PackedOption;
1114
use indexmap::IndexMap;
1215
use std::collections::HashMap;
1316
use std::mem;
@@ -492,12 +495,133 @@ impl<'a, 'data> Translator<'a, 'data> {
492495
&self.static_modules,
493496
&self.static_components,
494497
)?;
498+
495499
self.partition_adapter_modules(&mut component);
500+
496501
let translation =
497502
component.finish(self.types.types_mut_for_inlining(), self.result.types_ref())?;
503+
504+
self.analyze_function_imports(&translation);
505+
498506
Ok((translation, self.static_modules))
499507
}
500508

509+
fn analyze_function_imports(&mut self, translation: &ComponentTranslation) {
510+
// First, abstract interpret the initializers to create a map from each
511+
// static module to its abstract set of instantiations.
512+
let mut instantiations = SecondaryMap::<StaticModuleIndex, AbstractInstantiations>::new();
513+
let mut instance_to_module =
514+
PrimaryMap::<RuntimeInstanceIndex, PackedOption<StaticModuleIndex>>::new();
515+
for init in &translation.component.initializers {
516+
match init {
517+
GlobalInitializer::InstantiateModule(instantiation) => match instantiation {
518+
InstantiateModule::Static(module, args) => {
519+
instantiations[*module].join(AbstractInstantiations::One(&*args));
520+
instance_to_module.push(Some(*module).into());
521+
}
522+
_ => {
523+
instance_to_module.push(None.into());
524+
}
525+
},
526+
_ => continue,
527+
}
528+
}
529+
530+
// Second, make sure to mark exported modules as instantiated many
531+
// times, since they could be linked with who-knows-what at runtime.
532+
for item in translation.component.export_items.values() {
533+
if let Export::ModuleStatic { index, .. } = item {
534+
instantiations[*index].join(AbstractInstantiations::Many)
535+
}
536+
}
537+
538+
// Finally, iterate over our instantiations and record statically-known
539+
// function imports so that they can get translated into direct calls
540+
// (and eventually get inlined) rather than indirect calls through the
541+
// imports table.
542+
for (module, instantiations) in instantiations.iter() {
543+
let args = match instantiations {
544+
dfg::AbstractInstantiations::Many | dfg::AbstractInstantiations::None => continue,
545+
dfg::AbstractInstantiations::One(args) => args,
546+
};
547+
548+
let mut imported_func_counter = 0_u32;
549+
for (i, arg) in args.iter().enumerate() {
550+
// Only consider function imports.
551+
let (_, _, crate::types::EntityType::Function(_)) =
552+
self.static_modules[module].module.import(i).unwrap()
553+
else {
554+
continue;
555+
};
556+
557+
let imported_func = FuncIndex::from_u32(imported_func_counter);
558+
imported_func_counter += 1;
559+
debug_assert!(
560+
self.static_modules[module]
561+
.module
562+
.defined_func_index(imported_func)
563+
.is_none()
564+
);
565+
566+
match arg {
567+
CoreDef::InstanceFlags(_) => unreachable!("instance flags are not a function"),
568+
569+
// We could in theory inline these trampolines, so it could
570+
// potentially make sense to record that we know this
571+
// imported function is this particular trampoline. However,
572+
// everything else is based around (module,
573+
// defined-function) pairs and these trampolines don't fit
574+
// that paradigm. Also, inlining trampolines gets really
575+
// tricky when we consider the stack pointer, frame pointer,
576+
// and return address note-taking that they do for the
577+
// purposes of stack walking. We could, with enough effort,
578+
// turn them into direct calls even though we probably
579+
// wouldn't ever inline them, but it just doesn't seem worth
580+
// the effort.
581+
CoreDef::Trampoline(_) => continue,
582+
583+
// This imported function is an export from another
584+
// instance, a perfect candidate for becoming an inlinable
585+
// direct call!
586+
CoreDef::Export(export) => {
587+
let Some(arg_module) = &instance_to_module[export.instance].expand() else {
588+
// Instance of a dynamic module that is not part of
589+
// this component, not a statically-known module
590+
// inside this component. We have to do an indirect
591+
// call.
592+
continue;
593+
};
594+
595+
let ExportItem::Index(EntityIndex::Function(arg_func)) = &export.item
596+
else {
597+
unreachable!("function imports must be functions")
598+
};
599+
600+
let Some(arg_module_def_func) = self.static_modules[*arg_module]
601+
.module
602+
.defined_func_index(*arg_func)
603+
else {
604+
// TODO: we should ideally follow re-export chains
605+
// to bottom out the instantiation argument in
606+
// either a definition or an import at the root
607+
// component boundary. In practice, this pattern is
608+
// rare, so following these chains is left for the
609+
// Future.
610+
continue;
611+
};
612+
613+
assert!(
614+
self.static_modules[module].known_imported_functions[imported_func]
615+
.is_none()
616+
);
617+
self.static_modules[module].known_imported_functions[imported_func] =
618+
Some((*arg_module, arg_module_def_func));
619+
}
620+
}
621+
}
622+
}
623+
}
624+
501625
fn translate_payload(
502626
&mut self,
503627
payload: Payload<'data>,

crates/environ/src/component/translate/inline.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,8 +1045,7 @@ impl<'a> Inliner<'a> {
10451045
// and an initializer is recorded to indicate that it's being
10461046
// instantiated.
10471047
ModuleInstantiate(module, args) => {
1048-
let instance_module;
1049-
let init = match &frame.modules[*module] {
1048+
let (instance_module, init) = match &frame.modules[*module] {
10501049
ModuleDef::Static(idx, _ty) => {
10511050
let mut defs = Vec::new();
10521051
for (module, name, _ty) in self.nested_modules[*idx].module.imports() {
@@ -1055,8 +1054,10 @@ impl<'a> Inliner<'a> {
10551054
self.core_def_of_module_instance_export(frame, instance, name),
10561055
);
10571056
}
1058-
instance_module = InstanceModule::Static(*idx);
1059-
dfg::Instance::Static(*idx, defs.into())
1057+
(
1058+
InstanceModule::Static(*idx),
1059+
dfg::Instance::Static(*idx, defs.into()),
1060+
)
10601061
}
10611062
ModuleDef::Import(path, ty) => {
10621063
let mut defs = IndexMap::new();
@@ -1069,20 +1070,24 @@ impl<'a> Inliner<'a> {
10691070
.insert(name.to_string(), def);
10701071
}
10711072
let index = self.runtime_import(path);
1072-
instance_module = InstanceModule::Import(*ty);
1073-
dfg::Instance::Import(index, defs)
1073+
(
1074+
InstanceModule::Import(*ty),
1075+
dfg::Instance::Import(index, defs),
1076+
)
10741077
}
10751078
};
10761079

1077-
let idx = self.result.instances.push(init);
1080+
let instance = self.result.instances.push(init);
1081+
let instance2 = self.runtime_instances.push(instance_module);
1082+
assert_eq!(instance, instance2);
1083+
10781084
self.result
10791085
.side_effects
1080-
.push(dfg::SideEffect::Instance(idx));
1081-
let idx2 = self.runtime_instances.push(instance_module);
1082-
assert_eq!(idx, idx2);
1086+
.push(dfg::SideEffect::Instance(instance));
1087+
10831088
frame
10841089
.module_instances
1085-
.push(ModuleInstanceDef::Instantiated(idx, *module));
1090+
.push(ModuleInstanceDef::Instantiated(instance, *module));
10861091
}
10871092

10881093
ModuleSynthetic(map) => {
@@ -1772,6 +1777,7 @@ impl<'a> ComponentItemDef<'a> {
17721777
}
17731778
}
17741779

1780+
#[derive(Clone, Copy)]
17751781
enum InstanceModule {
17761782
Static(StaticModuleIndex),
17771783
Import(TypeModuleIndex),

crates/environ/src/module.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,13 @@ impl Module {
542542
})
543543
}
544544

545+
/// Get this module's `i`th import.
546+
pub fn import(&self, i: usize) -> Option<(&str, &str, EntityType)> {
547+
match self.initializers.get(i)? {
548+
Initializer::Import { name, field, index } => Some((name, field, self.type_of(*index))),
549+
}
550+
}
551+
545552
/// Returns the type of an item based on its index
546553
pub fn type_of(&self, index: EntityIndex) -> EntityType {
547554
match index {

0 commit comments

Comments
 (0)