Private fields in macro-generated structs

Let's say I have a macro that generates a struct:

macro_rules! foo {
    ($arg:expr) => {
        struct Foo {
            inner: u32,
        }
        
        impl Foo {
            fn get(&self) -> u32 {
                self.inner + $arg
            }
        }
    };
}

I would like inner to be private so that users of the macro cannot use it. Unfortunately, this compiles:

foo! { 10 }

fn bar(arg: Foo) -> u32 {
    arg.inner
}
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s

One option is to use a module:

macro_rules! foo {
    ($arg:expr) => {
        // defined outside `helper_mod` for visibility reasons
        const ARG: u32 = $arg;
        use helper_mod::Foo;
    
        mod helper_mod {
            struct Foo {
                inner: u32,
            }
            
            impl Foo {
                fn get(&self) -> u32 {
                    self.inner + super::ARG
                }
            }
        }
    };
}

foo! { 10 }

fn bar(arg: Foo) -> u32 {
    arg.inner
}
error[E0616]: field `inner` of struct `Foo` is private
  --> src/lib.rs:24:9
   |
24 |     arg.inner
   |         ^^^^^ private field

But this prevents you from using the macro in certain environments:

// should work but doesn't
fn main() {
    foo! { 10 }
}
error[E0425]: cannot find value `ARG` in module `super`
  --> src/main.rs:14:41
   |
14 |                     self.inner + super::ARG
   |                                         ^^^ not found in `super`
...
22 |     foo! { 10 }
   |     ----------- in this macro invocation

This is a problem because documentation without a main function is implicitly wrapped in a main function, causing the above situation. (Note that this is issue #130274.)


How can I have macros emit types with private fields without making the above case break?

playground

1 Like

This maybe?

        use helper_mod::{Foo, ARG};
    
        mod helper_mod {
            pub(super) const ARG: u32 = $arg;

            pub(super) struct Foo {
                inner: u32,
            }
            
            impl Foo {
                fn get(&self) -> u32 {
                    self.inner + ARG
                }
            }
        }

That breaks this case:

const MY_CONST: u32 = 17;
foo! { MY_CONST }
error[E0425]: cannot find value `MY_CONST` in this scope
  --> src/lib.rs:27:8
   |
27 | foo! { MY_CONST }
   |        ^^^^^^^^ not found in this scope
   |
   = help: consider importing this constant:
           crate::MY_CONST

damn macro hygiene

How is the struct used? (Though, I don't think that will matter.)

The closest I think you can get is to hide the field in a boxed trait object. This has the downside of limiting you to an object-safe impl and a slight perf impact, but I believe Bar.0 should be completely inaccessible outside new().

(And just to be sure, Foo is intended to be mutable, just only by macro-generated methods?)

If the only use case is to make it work in doc tests, you can wrap everything in a module hidden in the docs (playground):

/// Example:
/// ```
/// # mod _wrap {
/// # use playground::foo;
/// const MY_CONST: u32 = 17;
/// foo! { MY_CONST }
/// # } // mod _wrap
/// ```

Or alternatively, add a hidden fn main() {} like suggested in the issue you linked:

/// Example:
/// ```
/// # use playground::foo;
/// const MY_CONST: u32 = 17;
/// foo! { MY_CONST }
/// # fn main() {}
/// ```

IMHO, neither solution is pretty. And neither solves the general case outside of doc tests.

A {problem with, feature of} macros is that aside from compile time calculations and syntactic abstraction, what they can do must be manually possible as well. They are (conceptually) heavily template-based, after all.

One thing you might be able to do depending on your use case is turn it into an attribute macro, apply it to a unit struct, and have the attribute macro generate the field and methods:

#[foo(42)]
struct Foo;

Technically that won't prevent access if you don't also generate a module, but it will be hidden better (and therefore more difficult to muck about with it as a consumer) than it would be with a macro_rules! macro, and it would of course be undocumented (or you can explicitly document it as "manual manipulation unsupported" if you prefer), and therefore arguably not part of the public API.
It's not perfect but it would work.

A possible alternative or even augmentation could be to also write a trait with the necessary functionality, generate an impl Trait for the generated type, and then turn each new value of that type into a trait object (assuming the functionality allows for that) before handing it over to the consumer, thereby making direct field manipulation impossible in the first place.

1 Like

That particular case can be fixed by using a trait to lift the constant declaration out of the helper module:

macro_rules! foo {
    ($arg:expr) => {
        use helper_mod::Foo;
    
        impl helper_mod::Arg for Foo {
            const ARG:u32 = $arg;
        }
        
        mod helper_mod {
            pub(super) struct Foo {
                inner: u32,
            }
            
            pub(super) trait Arg {
                const ARG:u32;
            }
            
            impl Foo {
                fn get(&self) -> u32 {
                    self.inner + <Foo as Arg>::ARG
                }
            }
        }
    };
}
4 Likes

I think that this may be the crucial point here. For simple types such as u32 as you suggested, I would try something like this:

  1. Create two types in your crate (where the macro is exposed) which have both a generic private field.
pub struct PrivateField<T>(T);
pub struct PrivateFieldGet<T>(T);
  1. Use these types on your macro to wrap each field
  2. To have more fine-grained control (eg. if you want a setter but not a getter) you can implement a trait like this:
trait Get<T> {
    fn get(&self) -> T;
}

impl<T> Get<T> for PrivateFieldGet<T>
where
    T: Clone,
{
    fn get(&self) -> T {
        self.0.clone()
    }
}
  1. In the generation of the struct, we wrap private fields within this new type.
macro_rules! generate_struct {
    (...) => {
        struct {
            // Allows to only set this variable once but never read it.
            inner_private: mycrate::PrivateField(u32),
            // We set the value initially and can use a getter function to access it
            inner_private_get: mycrate::PrivateFieldGet(u32),
            // Classic struct field which is public for the user
            inner_public: u32,
            ...
        }
    }
}

The main problem here is of course the syntax. Having to access a field with foo.inner.get() is unwieldy compared to the standard foo.inner but Rust can not distinguish between "get-only" or "get-and-set" field types (not even inside your own crate). Thus we need to rely on traits or other functions.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.