Skip to content

Commit b5bc187

Browse files
committed
add support for function pointers
1 parent dde8058 commit b5bc187

11 files changed

Lines changed: 678 additions & 77 deletions

File tree

Readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ All examples live in `tests/binary` and are compiled to JVM bytecode & run/teste
5454
- Implementations for ADTs, including using and returning `self`, `&self`, `&mut self`
5555
- Traits, including dynamic dispatch (`&dyn Trait`)
5656
- Closures, including capturing closures
57+
- Function pointers
5758
- **Integration tests** for all features, in debug and release modes
5859

5960
🚧 **Next Milestone:** Full support for the Rust `core` crate.

src/lower1/control_flow.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -608,12 +608,24 @@ pub fn convert_basic_block<'tcx>(
608608
});
609609
}
610610
} else {
611-
// Fallback for function pointers
612-
let legacy_name = make_jvm_safe(format!("{:?}", func).as_str());
613-
instructions.push(oomir::Instruction::Call {
614-
dest: dest_var_name, // Note: Function pointers might also need Void check eventually
615-
function: legacy_name,
611+
let func_oomir_operand =
612+
convert_operand(&func, tcx, instance, mir, data_types, &mut instructions);
613+
614+
let oomir_sig =
615+
super::types::fn_ptr_signature_from_ty(func_ty, tcx, data_types, instance);
616+
super::types::ensure_fn_ptr_interface(&oomir_sig, data_types);
617+
618+
let effective_dest = if matches!(oomir_sig.ret.as_ref(), oomir::Type::Void) {
619+
None
620+
} else {
621+
dest_var_name.clone()
622+
};
623+
624+
instructions.push(oomir::Instruction::CallIndirect {
625+
dest: effective_dest,
626+
function_ptr: Box::new(func_oomir_operand),
616627
args: oomir_operands.clone(),
628+
signature: oomir_sig,
617629
});
618630
}
619631

src/lower1/control_flow/rvalue.rs

Lines changed: 320 additions & 71 deletions
Large diffs are not rendered by default.

src/lower1/types.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,69 @@ pub fn ty_is_never<'tcx>(ty: Ty<'tcx>) -> bool {
2323
matches!(ty.kind(), TyKind::Never)
2424
}
2525

26+
pub fn fn_ptr_signature_from_ty<'tcx>(
27+
ty: Ty<'tcx>,
28+
tcx: TyCtxt<'tcx>,
29+
data_types: &mut HashMap<String, oomir::DataType>,
30+
instance_context: rustc_middle::ty::Instance<'tcx>,
31+
) -> oomir::Signature {
32+
let sig = ty.fn_sig(tcx).skip_binder();
33+
let params = sig
34+
.inputs()
35+
.iter()
36+
.enumerate()
37+
.map(|(i, ty)| {
38+
(
39+
format!("arg{}", i),
40+
ty_to_oomir_type(*ty, tcx, data_types, instance_context),
41+
)
42+
})
43+
.collect();
44+
let ret = ty_to_oomir_type(sig.output(), tcx, data_types, instance_context);
45+
46+
oomir::Signature {
47+
params,
48+
ret: Box::new(ret),
49+
is_static: true,
50+
}
51+
}
52+
53+
pub fn ensure_fn_ptr_interface(
54+
signature: &oomir::Signature,
55+
data_types: &mut HashMap<String, oomir::DataType>,
56+
) -> String {
57+
let interface_name = signature.fn_ptr_interface_name();
58+
let method_signature = signature.fn_ptr_interface_method_signature();
59+
60+
match data_types.get_mut(&interface_name) {
61+
Some(oomir::DataType::Interface { methods }) => {
62+
methods
63+
.entry("call".to_string())
64+
.or_insert(method_signature);
65+
}
66+
Some(oomir::DataType::Class { .. }) => {
67+
breadcrumbs::log!(
68+
breadcrumbs::LogLevel::Warn,
69+
"type-mapping",
70+
format!(
71+
"Function pointer interface name '{}' already exists as a class",
72+
interface_name
73+
)
74+
);
75+
}
76+
None => {
77+
data_types.insert(
78+
interface_name.clone(),
79+
oomir::DataType::Interface {
80+
methods: HashMap::from([("call".to_string(), method_signature)]),
81+
},
82+
);
83+
}
84+
}
85+
86+
interface_name
87+
}
88+
2689
/// Converts a Rust MIR type (`Ty`) to an OOMIR type (`oomir::Type`).
2790
pub fn ty_to_oomir_type<'tcx>(
2891
ty: Ty<'tcx>,
@@ -359,12 +422,22 @@ pub fn ty_to_oomir_type<'tcx>(
359422
ExistentialPredicate::Trait(trait_ref) => {
360423
let trait_name = tcx.def_path_str(trait_ref.def_id);
361424
let safe_name = make_jvm_safe(&trait_name);
425+
data_types.entry(safe_name.clone()).or_insert_with(|| {
426+
oomir::DataType::Interface {
427+
methods: HashMap::new(),
428+
}
429+
});
362430
resolved_types.push(oomir::Type::Interface(safe_name));
363431
}
364432
ExistentialPredicate::AutoTrait(def_id) => {
365433
// Auto traits like Send/Sync — treat as interfaces as well.
366434
let trait_name = tcx.def_path_str(def_id);
367435
let safe_name = make_jvm_safe(&trait_name);
436+
data_types.entry(safe_name.clone()).or_insert_with(|| {
437+
oomir::DataType::Interface {
438+
methods: HashMap::new(),
439+
}
440+
});
368441
resolved_types.push(oomir::Type::Interface(safe_name));
369442
}
370443
ExistentialPredicate::Projection(_) => {
@@ -428,6 +501,12 @@ pub fn ty_to_oomir_type<'tcx>(
428501
}
429502
oomir::Type::Class(safe_name)
430503
}
504+
rustc_middle::ty::TyKind::FnPtr(_, _) => {
505+
let signature =
506+
fn_ptr_signature_from_ty(resolved_ty, tcx, data_types, instance_context);
507+
let interface_name = ensure_fn_ptr_interface(&signature, data_types);
508+
oomir::Type::Interface(interface_name)
509+
}
431510
rustc_middle::ty::TyKind::FnDef(def_id, _args) => {
432511
// Named functions are Zero-Sized Types (ZSTs).
433512
// We generate a singleton class so generics like Map<Iter, MyFunc>

src/lower2/translator.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2351,6 +2351,42 @@ impl<'a, 'cp> FunctionTranslator<'a, 'cp> {
23512351
self.jvm_instructions.push(JI::Athrow);
23522352
}
23532353
}
2354+
OI::CallIndirect {
2355+
dest,
2356+
function_ptr,
2357+
args,
2358+
signature,
2359+
} => {
2360+
let interface_name = signature.fn_ptr_interface_name();
2361+
let class_index = self.constant_pool.add_class(&interface_name)?;
2362+
let descriptor = signature.to_jvm_descriptor_with_explicit_params();
2363+
let method_ref = self.constant_pool.add_interface_method_ref(
2364+
class_index,
2365+
"call",
2366+
&descriptor,
2367+
)?;
2368+
2369+
self.load_operand(function_ptr)?;
2370+
for arg in args.iter() {
2371+
self.load_call_argument(arg)?;
2372+
}
2373+
2374+
let count = self.invokeinterface_count(args)?;
2375+
self.jvm_instructions
2376+
.push(JI::Invokeinterface(method_ref, count));
2377+
2378+
if let Some(dest_var) = dest {
2379+
if *signature.ret != oomir::Type::Void {
2380+
self.store_result(dest_var, &signature.ret)?;
2381+
}
2382+
} else if *signature.ret != oomir::Type::Void {
2383+
match get_type_size(&signature.ret) {
2384+
1 => self.jvm_instructions.push(JI::Pop),
2385+
2 => self.jvm_instructions.push(JI::Pop2),
2386+
_ => {}
2387+
}
2388+
}
2389+
}
23542390
OI::Move { dest, src } => {
23552391
// Determine the type of the VALUE being moved (from the source operand)
23562392
let value_type = match src {
@@ -2839,8 +2875,9 @@ impl<'a, 'cp> FunctionTranslator<'a, 'cp> {
28392875
}
28402876

28412877
// 3. Emit 'invokeinterface' instruction
2878+
let count = self.invokeinterface_count(args)?;
28422879
self.jvm_instructions
2843-
.push(JI::Invokeinterface(method_ref_index, args.len() as u8 + 1)); // Stack: [result]
2880+
.push(JI::Invokeinterface(method_ref_index, count)); // Stack: [result]
28442881

28452882
// 4. Handle the return value
28462883
if let Some(dest_var) = dest {
@@ -2984,4 +3021,20 @@ impl<'a, 'cp> FunctionTranslator<'a, 'cp> {
29843021
}
29853022
Ok(())
29863023
}
3024+
3025+
fn call_argument_slot_size(operand: &oomir::Operand) -> u16 {
3026+
let ty = get_operand_type(operand);
3027+
match ty {
3028+
oomir::Type::Reference(inner) if inner.is_jvm_primitive() => get_type_size(&inner),
3029+
_ => get_type_size(&ty),
3030+
}
3031+
}
3032+
3033+
fn invokeinterface_count(&self, args: &[oomir::Operand]) -> Result<u8, jvm::Error> {
3034+
let slots = 1 + args.iter().map(Self::call_argument_slot_size).sum::<u16>();
3035+
u8::try_from(slots).map_err(|_| jvm::Error::VerificationError {
3036+
context: format!("Function {}", self.oomir_func.name),
3037+
message: format!("invokeinterface argument slot count {slots} exceeds u8 range"),
3038+
})
3039+
}
29873040
}

src/oomir.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use super::lower2::BIG_INTEGER_CLASS;
77
use breadcrumbs::LogLevel;
88
use core::panic;
99
use ristretto_classfile::attributes::Instruction as JVMInstruction;
10+
use sha2::Digest;
1011
use std::{
1112
collections::{HashMap, HashSet},
1213
fmt,
@@ -203,6 +204,33 @@ pub struct Signature {
203204
}
204205

205206
impl Signature {
207+
pub fn to_jvm_descriptor_with_explicit_params(&self) -> String {
208+
let mut result = String::new();
209+
result.push('(');
210+
for (_param_name, param_type) in &self.params {
211+
result.push_str(&param_type.to_jvm_descriptor());
212+
}
213+
result.push(')');
214+
result.push_str(&self.ret.to_jvm_descriptor());
215+
result
216+
}
217+
218+
pub fn fn_ptr_interface_name(&self) -> String {
219+
let descriptor = self.to_jvm_descriptor_with_explicit_params();
220+
let mut hasher = sha2::Sha256::new();
221+
hasher.update(descriptor.as_bytes());
222+
let hash = format!("{:x}", hasher.finalize());
223+
format!("FnPtr_{}", &hash[..16])
224+
}
225+
226+
pub fn fn_ptr_interface_method_signature(&self) -> Signature {
227+
Signature {
228+
params: self.params.clone(),
229+
ret: self.ret.clone(),
230+
is_static: false,
231+
}
232+
}
233+
206234
/// Replaces all occurrences of `Type::Class(old_name)` with `Type::Class(new_name)`
207235
/// in the signature's parameters and return type.
208236
/// Returns a tuple (params_changed, return_changed) indicating whether any replacements were made.
@@ -378,6 +406,12 @@ pub enum Instruction {
378406
function: String, // Name of the function to call
379407
args: Vec<Operand>, // Arguments to the function
380408
},
409+
CallIndirect {
410+
dest: Option<String>, // Optional destination variable for the return value
411+
function_ptr: Box<Operand>, // Operand holding the function pointer object
412+
args: Vec<Operand>, // Arguments to the function
413+
signature: Signature, // Function pointer signature
414+
},
381415
InvokeInterface {
382416
class_name: String, // JVM interface name (e.g., MyTrait)
383417
method_name: String, // Name of the method to call

src/optimise1/dataflow.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,79 @@ pub fn process_block_instructions(
929929
};
930930
keep_original_instruction = true; // Always keep the call itself
931931
}
932+
Instruction::CallIndirect {
933+
dest,
934+
function_ptr,
935+
args,
936+
signature,
937+
} => {
938+
// Propagate constants into function pointer and arguments
939+
let new_function_ptr = match function_ptr.as_ref() {
940+
Operand::Variable { name, ty } => lookup_const(
941+
&Operand::Variable {
942+
name: name.clone(),
943+
ty: ty.clone(),
944+
},
945+
&current_state,
946+
)
947+
.map_or(function_ptr.as_ref().clone(), Operand::Constant),
948+
other => other.clone(),
949+
};
950+
let new_args: Vec<Operand> = args
951+
.iter()
952+
.map(|arg| {
953+
lookup_const(arg, &current_state).map_or(arg.clone(), Operand::Constant)
954+
})
955+
.collect();
956+
957+
// Determine if any argument could potentially have side effects
958+
let mut has_potential_side_effect_arg = false;
959+
for arg_operand in &new_args {
960+
let arg_type = match arg_operand {
961+
Operand::Variable { ty, .. } => Some(ty.clone()),
962+
Operand::Constant(c) => Some(Type::from_constant(c)),
963+
};
964+
if let Some(ty) = arg_type {
965+
if matches!(ty, Type::Array(_) | Type::Class(_)) {
966+
has_potential_side_effect_arg = true;
967+
break;
968+
}
969+
}
970+
}
971+
972+
// 1. Invalidate the destination variable
973+
if let Some(d) = dest {
974+
current_state.remove(d);
975+
}
976+
977+
// 2. CONSERVATIVE INVALIDATION due to potential side effects:
978+
if has_potential_side_effect_arg {
979+
let keys_to_remove: Vec<String> = current_state.iter()
980+
.filter_map(|(key, constant_val)| {
981+
match constant_val {
982+
Constant::I8(_) | Constant::I16(_) | Constant::I32(_) | Constant::I64(_) |
983+
Constant::F32(_) | Constant::F64(_) | Constant::Boolean(_) | Constant::Char(_) |
984+
Constant::String(_) => None,
985+
_ => {
986+
breadcrumbs::log!(breadcrumbs::LogLevel::Info, "optimisation", format!("Optimizer: Invalidating potentially mutable constant '{}' due to indirect call with array/class arg.", key));
987+
Some(key.clone())
988+
}
989+
}
990+
})
991+
.collect();
992+
for key in keys_to_remove {
993+
current_state.remove(&key);
994+
}
995+
}
996+
997+
optimised_instruction = Instruction::CallIndirect {
998+
dest: dest.clone(),
999+
function_ptr: Box::new(new_function_ptr),
1000+
args: new_args,
1001+
signature: signature.clone(),
1002+
};
1003+
keep_original_instruction = true;
1004+
}
9321005
Instruction::ArrayStore {
9331006
array,
9341007
index,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../config.toml

tests/binary/fn_pointers/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
cargo-features = ["profile-rustflags"]
2+
3+
[package]
4+
name = "fn_pointers"
5+
version = "0.1.0"
6+
edition = "2024"
7+
8+
[dependencies]

0 commit comments

Comments
 (0)