# VTable Refactor Plan
## Goal
Replace multiple per-method exported symbols with a single VTable symbol per trait.
### Current Design
- Each method: `#[unsafe(export_name = "...")] fn method(...)` (via `emit_export` in `decl/mod.rs`)
- `drop` and `typeid`: separate symbols (via `expand_drop_impl` / `expand_cast_impl`)
- N+2 symbols per trait (N methods + drop + typeid)
### New Design
- Single `#[unsafe(export_name = "...")] static VT: __TraitVTable`
- VTable contains all method pointers + typeid + drop
## Benefits
- Reduced linker symbol pollution
- Single linker error when trait not implemented (vs N+2 errors)
- Potentially smaller binary and faster compilation
## Notes on LTO
With LTO enabled, the entire VTable struct is eliminated and function calls are inlined
directly — verified by testing. Both dead code elimination and indirect call overhead are
non-issues under LTO.
This crate is designed for cross-crate interfaces where LTO is the expected configuration.
Even the current per-method symbol design has call overhead without LTO, so LTO is
effectively a requirement regardless of dispatch strategy.
## Critical Design Notes
### `#[repr(C)]` Required
The VTable struct is defined **twice** — once on the proxy side (trait definition) and once
on the impl side (macro_rules expansion). The two definitions have **different field types**
for ref/ptr Self (proxy uses `ProxyType`, impl uses `$ty`), but they share the same linker
symbol.
Their memory layouts **must** be identical. Default Rust struct layout is unspecified and may
differ between compilation units. Both definitions **must** use `#[repr(C)]` to guarantee
deterministic field ordering and alignment.
Layout equivalence is guaranteed by:
- `#[repr(C)]` on both structs ensures identical field ordering
- By-value Self: proxy uses `ProxyType` which is `#[repr(transparent)]` over `Repr`;
impl uses `Repr` directly — ABI identical
- Ref/ptr Self: both sides are pointer types — same size and alignment
- Non-Self types: identical on both sides
### `safe static` for VTable Symbol
The proxy side declares the VTable as an extern static. Following the existing pattern for
`TYPEID` (which already uses `safe static`), the VTable should also be declared `safe static`:
```rust
unsafe extern "Rust" {
#[link_name = #vtable_sym]
safe static VT: __TraitVTable;
}
```
This allows `(VT.field)(args)` without wrapping every access in an `unsafe` block.
Safety invariant: the impl side guarantees the exported static has the correct type and layout
(enforced by the macro_rules expansion and `#[repr(C)]`).
### Self Type on Proxy Side
By-value `Self` on the proxy side uses `ProxyType` directly (same as the current per-method
design — `emit_method` already substitutes `Self` → `ProxyType` via `SelfKind::Value.to_type()`).
This means for a method like `fn new(num: i32) -> Self`, the proxy-side VTable field type is
`fn(i32) -> HelloProxy`, and the trait impl can return the call result directly:
```rust
fn new(num: i32) -> Self {
(VT.new)(num) // returns HelloProxy, which IS Self
}
```
No `Self(...)` wrapping needed — `HelloProxy` is already `Self` in this context.
On the impl side, the same field uses `Repr` (since the concrete type is erased), with
`Repr::from_value` / `Repr::into_value` conversions in the closure body.
## Implementation
### 1. `impl/src/decl/symbol.rs`
**Current state:**
```rust
pub struct Symbol {
extern_trait: String,
package: String,
version: String,
crate_name: String,
package_disambiguator: u64,
trait_name: String,
local_disambiguator: u64,
name: String, // per-method suffix, set via .with_name()
}
```
**Changes:**
- Delete `name: String` field
- Delete `with_name()` method
**Reason:** Only one symbol per trait now (vtable), no need for per-method name customization.
### 2. `impl/src/decl/supertraits.rs`
**No changes needed.**
- `collect_supertraits()` already returns `Vec<SupertraitInfo>` with methods
- `SupertraitInfo` already provides `methods: Vec<VerifiedSignature>`
### 3. `impl/src/decl/types.rs`
**No changes needed.**
- `to_type()` method on `MaybeSelf` already handles Self type substitution
- `VerifiedSignature` already separates inputs/output as `Vec<MaybeSelf>` / `Option<MaybeSelf>`
### 4. `impl/src/decl/mod.rs`
**Current state:**
- `ExpandCtx` holds: `extern_trait: Path`, `proxy: Proxy`, `input: ItemTrait`, `sym: Symbol`, `macro_items: TokenStream`
- `emit_method()` — generates proxy-side impl method (link_name extern call)
- `emit_export()` — generates impl-side exported function (export_name)
- `expand_trait_impl()`, `expand_supertrait_impls()`, `expand_drop_impl()`, `expand_cast_impl()`, `expand_macro_rules()`, `expand()`
**Major restructuring:**
#### 4.1 New type: `MethodInfo`
```rust
struct MethodInfo {
sig: VerifiedSignature,
/// None for trait's own methods, Some(path) for supertrait methods
supertrait_path: Option<Path>,
}
impl MethodInfo {
fn field_name(&self) -> Ident {
match &self.supertrait_path {
None => self.sig.ident.clone(),
Some(path) => {
let last = path.segments.last().unwrap();
format_ident!("{}_{}", last.ident, self.sig.ident)
}
}
}
}
```
#### 4.2 Replace `emit_method()` + `emit_export()`
Both are removed. In their place:
- `generate_vtable_struct()` — generates `#[repr(C)] struct __TraitVTable { ... }` with:
- `typeid: ConstTypeId`
- `drop: unsafe fn(*mut ProxyType)`
- One field per method (trait + supertrait), fn pointer type
- Takes a type parameter to control Self substitution:
- Proxy side: `Self` → `ProxyType`
- Impl side: by-value `Self` → `Repr`, ref/ptr `Self` → `$ty`
- `generate_proxy_trait_impl()` — calls through VTable fields:
```rust
fn method(&self, arg: T) -> R {
(VT.method)(self, arg)
}
```
- `generate_impl_vtable_init()` — generates the static VTable initializer:
```rust
static VT: __TraitVTable = __TraitVTable {
typeid: ConstTypeId::of::<$ty>(),
drop: |this| unsafe { core::ptr::drop_in_place(this) },
method: |args| { <$ty as $trait>::method(converted_args) },
};
```
#### 4.3 Reflow `expand()`
1. Collect all methods: trait methods + supertrait methods → `Vec<MethodInfo>`
2. Generate proxy-side VTable struct (`#[repr(C)]`)
3. Generate extern static declaration (`safe static VT`)
4. Generate trait impl for proxy (calls `(VT.field)(args)`)
5. Generate supertrait impls (calls through VTable, same pattern)
6. Generate Drop impl (`unsafe { (VT.drop)(self as *mut Self) }`)
7. Generate cast methods (`VT.typeid` instead of separate extern static)
8. Generate macro_rules with impl-side VTable struct + static initializer
#### 4.4 VTable Field Type Mapping
**Proxy-side struct:**
| `Self` (by value) | `ProxyType` | `ProxyType` |
| `&Self` | `&ProxyType` | N/A (never returned) |
| `&mut Self` | `&mut ProxyType` | N/A |
| `*const Self` | `*const ProxyType` | `*const ProxyType` |
| `*mut Self` | `*mut ProxyType` | `*mut ProxyType` |
| Non-Self | unchanged | unchanged |
All Self variants use `ProxyType` on the proxy side, consistent with the current per-method
design (`emit_method` uses `input.to_type(proxy.clone())`).
**Impl-side struct:**
| `Self` (by value) | `Repr` | `Repr` |
| `&Self` | `&$ty` | N/A |
| `&mut Self` | `&mut $ty` | N/A |
| `*const Self` | `*const $ty` | `*const $ty` |
| `*mut Self` | `*mut $ty` | `*mut $ty` |
| Non-Self | unchanged | unchanged |
**Layout equivalence**: `ProxyType` is `#[repr(transparent)]` over `Repr`, so proxy-side
`ProxyType` and impl-side `Repr` have identical ABI. Ref/ptr types are all pointer-sized.
Combined with `#[repr(C)]`, the two struct definitions are layout-compatible.
#### 4.5 Impl-side Field Initialization (call wrappers)
| `Self` (by value) param | `unsafe { Repr::into_value::<$ty>(arg) }` |
| `&Self`, `&mut Self`, `*const Self`, `*mut Self` | No conversion needed (pointer-compatible) |
| `Self` (by value) return | `unsafe { Repr::from_value(result) }` |
| Non-Self | No conversion |
### 5. `impl/src/imp.rs`
**No changes needed.** The impl side just invokes the macro_rules generated by the decl side.
The macro_rules content changes (VTable struct + static init instead of per-method exports),
but `imp.rs` itself only does:
```rust
#trait_!(#trait_: #ty);
```
## Generated Code Example
### Trait Definition Side
```rust
pub trait Hello {
fn new(num: i32) -> Self;
fn hello(&self);
}
#[repr(transparent)]
pub struct HelloProxy(::extern_trait::Repr);
const _: () = {
#[repr(C)]
struct __HelloVTable {
typeid: ::extern_trait::__private::ConstTypeId,
drop: unsafe fn(*mut HelloProxy),
new: fn(i32) -> HelloProxy,
hello: fn(&HelloProxy),
}
unsafe extern "Rust" {
#[link_name = "Symbol { ... trait_name: \"Hello\" ... }"]
safe static VT: __HelloVTable;
}
impl Hello for HelloProxy {
fn new(num: i32) -> Self {
(VT.new)(num)
}
fn hello(&self) {
(VT.hello)(self)
}
}
impl Drop for HelloProxy {
fn drop(&mut self) {
unsafe { (VT.drop)(self) }
}
}
impl HelloProxy {
fn assert_type_is_impl<T: Hello>() {
let typeid = ::extern_trait::__private::ConstTypeId::of::<T>();
assert!(typeid == VT.typeid, "...");
}
pub fn from_impl<T: Hello>(value: T) -> Self {
Self::assert_type_is_impl::<T>();
Self(unsafe { ::extern_trait::Repr::from_value(value) })
}
pub fn into_impl<T: Hello>(self) -> T {
Self::assert_type_is_impl::<T>();
unsafe {
::extern_trait::Repr::into_value(
::extern_trait::Repr::from_value(self)
)
}
}
pub fn downcast_ref<T: Hello>(&self) -> &T {
Self::assert_type_is_impl::<T>();
unsafe { &*(self as *const Self as *const T) }
}
pub fn downcast_mut<T: Hello>(&mut self) -> &mut T {
Self::assert_type_is_impl::<T>();
unsafe { &mut *(self as *mut Self as *mut T) }
}
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! __extern_trait_Hello {
($trait:path: $ty:ty) => {
const _: () = {
#[repr(C)]
struct __HelloVTable {
typeid: ::extern_trait::__private::ConstTypeId,
drop: unsafe fn(*mut $ty),
new: fn(i32) -> ::extern_trait::Repr,
hello: fn(&$ty),
}
#[unsafe(export_name = "Symbol { ... trait_name: \"Hello\" ... }")]
static VT: __HelloVTable = __HelloVTable {
typeid: ::extern_trait::__private::ConstTypeId::of::<$ty>(),
drop: |this: *mut $ty| unsafe { ::core::ptr::drop_in_place(this) },
new: |num: i32| {
let __result = <$ty as $trait>::new(num);
unsafe { ::extern_trait::Repr::from_value(__result) }
},
hello: |this: &$ty| <$ty as $trait>::hello(this),
};
};
};
}
#[doc(hidden)]
#[allow(unused_imports)]
pub use __extern_trait_Hello as Hello;
```
### With Supertraits
```rust
#[extern_trait(ResourceProxy)]
trait Resource: Send + Sync + Clone + Debug {
fn new() -> Self;
}
// Proxy-side VTable struct:
#[repr(C)]
struct __ResourceVTable {
typeid: ConstTypeId,
drop: unsafe fn(*mut ResourceProxy),
new: fn() -> ResourceProxy,
Clone_clone: fn(&ResourceProxy) -> ResourceProxy,
Debug_fmt: fn(&ResourceProxy, &mut core::fmt::Formatter<'_>) -> core::fmt::Result,
}
// impl Clone for ResourceProxy:
// fn clone(&self) -> Self { (VT.Clone_clone)(self) }
// impl Debug for ResourceProxy:
// fn fmt(&self, f: &mut Formatter<'_>) -> Result { (VT.Debug_fmt)(self, f) }
// unsafe impl Send for ResourceProxy {}
// unsafe impl Sync for ResourceProxy {}
```
## Test Plan
1. Run existing tests: `cargo test` — all must pass without modification
2. Check generated code with `cargo expand` (if available)
3. Verify single symbol per trait in object files: `nm` or `objdump`
4. Verify `#[repr(C)]` is present on both VTable struct definitions
5. Add a UI test for mismatched VTable (if possible)