Skip to content

Allow #[mlua::userdata_impl] in a different module than the type - #727

Open
teddytennant wants to merge 1 commit into
mlua-rs:mainfrom
teddytennant:fix-userdata-impl-cross-module
Open

Allow #[mlua::userdata_impl] in a different module than the type#727
teddytennant wants to merge 1 commit into
mlua-rs:mainfrom
teddytennant:fix-userdata-impl-cross-module

Conversation

@teddytennant

Copy link
Copy Markdown

The bug

#[mlua::userdata_impl] only compiles when the impl block is in the same module as
#[derive(UserData)]. Splitting a type from its Lua bindings — a natural layout when embedding
Lua — fails:

mod types {
    #[derive(mlua::UserData)]
    pub struct Counter {
        pub count: u32,
    }
}

mod bindings {
    use super::types::Counter;

    #[mlua::userdata_impl]
    impl Counter {
        fn increment(&mut self) -> mlua::Result<u32> {
            self.count += 1;
            Ok(self.count)
        }
    }
}
error[E0422]: cannot find struct, variant or union type `__MluaUserDataRegistration_Counter` in this scope
   --> tests/userdata_macro.rs:587:5
    |
587 |     #[mlua::userdata_impl]
    |     ^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
    |
note: struct `crate::counter_type::__MluaUserDataRegistration_Counter` exists but is inaccessible

The derive emitted a private __MluaUserDataRegistration_<Type> struct plus an
inventory::collect! in the type's module, and #[mlua::userdata_impl] submitted to it by bare
identifier. That identifier only resolves in the module where the derive was expanded.

The fix

Route the collection through the type itself, since the type is by definition in scope wherever the
impl block is written. mlua gains two #[doc(hidden)] items behind the macros feature:

pub struct UserDataRegistration<T> {
    pub register: fn(&mut UserDataRegistry<T>),
}

pub trait UserDataRegistrar: Sized + 'static {
    fn inventory_registry() -> &'static inventory::Registry;
}

impl<T: UserDataRegistrar> inventory::Collect for UserDataRegistration<T> {
    fn registry() -> &'static inventory::Registry {
        T::inventory_registry()
    }
}

#[derive(UserData)] implements UserDataRegistrar (holding the per-type inventory::Registry
that inventory::collect! used to create), and both macros now submit
::mlua::userdata::UserDataRegistration::<Type> { register: ... }. That is a fully qualified,
const-constructible struct literal, which is what inventory::submit! requires, and it names no
module-local item.

This is the right layer for the fix: the resolution failure is purely an artifact of how the two
macros agreed on a name, so keeping the per-type inventory registry but keying it off the type
rather than off a generated identifier removes the module coupling without changing when or how
registrations run.

A type used with #[mlua::userdata_impl] but no #[derive(UserData)] previously failed with the
same E0422. It now reports the missing bound, with a #[diagnostic::on_unimplemented] message:

error[E0277]: `NoDerive` is missing `#[derive(UserData)]`
 --> tests/tmp.rs:9:1
  |
9 | #[mlua::userdata_impl]
  | ^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
  |
  = note: `#[mlua::userdata_impl]` requires the type to derive `UserData`

Verification

New test test_impl_in_other_module in tests/userdata_macro.rs puts the derive and the
userdata_impl in sibling modules and asserts that both the derive's field registration and the
impl block's methods land on the same type.

Before the fix (test applied, sources reverted):

error[E0422]: cannot find struct, variant or union type `__MluaUserDataRegistration_Counter` in this scope
   --> tests/userdata_macro.rs:587:5
error: could not compile `mlua` (test "userdata_macro") due to 1 previous error

After:

running 8 tests
test test_known_borrow_wrappers ... ok
test test_param_name_hygiene ... ok
test test_impl_in_other_module ... ok
test test_color ... ok
test test_point ... ok
test test_wildcard_params ... ok
test test_static_metamethods ... ok
test test_rectangle ... ok

test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Full suite, --features lua54,vendored,async,send,serde,macros: 286 tests passed, 0 failed
(1 ignored: the trybuild runner), plus 48 doctests passed. The 6 pre-existing trybuild stderr
mismatches under -- --ignored are unchanged from a clean checkout of main (they are rustc
span-rendering differences; CI blesses them with TRYBUILD=overwrite).

Also checked by hand, all passing:

  • lua51, lua54, luau test runs and a luajit build — the derive is Lua-version independent;
  • a build with macros disabled, to confirm the new items are correctly feature-gated;
  • type reached through a pub use alias from the impl block's module;
  • type in a nested module, impl written as impl crate::types::deep::Deep;
  • two impl blocks for one type in two different modules;
  • #[cfg]-gated fields and methods;
  • the single-module layout, which is what the existing tests in tests/userdata_macro.rs cover;
  • registrations do not leak between types (each type keeps its own registry).

cargo +nightly fmt -- --check is clean and cargo +nightly clippy reports no new warnings.

Not changed

  • Generic types and generic impl blocks are still rejected by the macros, as before.
  • No CHANGELOG.md entry, since that file looks maintainer-maintained at release time — happy to
    add one if you'd like.

`#[derive(UserData)]` used to emit a private `__MluaUserDataRegistration_<Type>`
struct next to the type and `#[mlua::userdata_impl]` referred to it by bare
identifier, so the impl block had to sit in the same module as the derive.
Splitting a type from its Lua bindings failed with:

    error[E0422]: cannot find struct, variant or union type
    `__MluaUserDataRegistration_A` in this scope

Collect the registrations through the type instead: the derive implements the
hidden `UserDataRegistrar` trait, which owns the type's inventory registry, and
both macros submit `UserDataRegistration::<Type>` entries. The type is always in
scope where the impl block is written, so registrations now resolve from any
module, including through `use` aliases and fully qualified paths.

Closes mlua-rs#726
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant