Is there a way to make a "token" structure which can only be returned to the object which created it?

I'd like to have some API that looks like this:

struct Machine;

struct Token<'a>;

impl Machine {
    fn get_token<'a>(&'a self) -> Token<'a>;

    fn give_token<'a>(&'a self, token: Token<'a>);

However, I want to make sure that every Token is given back only to the Machine which created it (for soundness reasons, to create a safe API for some unsafe backend). In particular, I want the following code to be illegal:

let m1 = Machine;
let m2 = Machine;

let token = m1.get_token();
m2.give_token(token); // should not be allowed
Backstory: working with identifiers for some borrowed datum The backstory with this is that my `Machine`s have internally stored, partially-initialized data, split across a struct-of-arrays layout. The longer backstory is that it's for a nearest-neighbors search algorithm I'm playing with, but that's beyond the scope of this question.
struct Machine {
    data: Vec<MaybeUninit<FirstField>>;
    data: Vec<MaybeUninit<SecondField>>;

struct Token(usize);

Each Machine can be queried, and if we find something matching the query we return a Token identifying the item inside the Machine. When we want to get information about the item inside the machine, we can go back and ask for its fields:

impl Machine {
    fn get_field_1<'a>(&'a Self, token: Token<'a>) -> &'a FirstField;

If Tokens cross between Machines, they could be used to access uninitialized data inside the second Machine.

The only way that I know to implement this so far is to make give_token a method on Token rather than Machine, but I don't like that since I intend for Token to be extremely small and this will be run in a high-performance context. However, this is my current implementation, so it's not the end of the world if this is impossible.

If the Token's lifetime is invariant (e.g. by storing PhantonData<&' a mut ()>) then downstream users should get lifetime errors if they try to return the token to the wrong machine.

Lifetimes, even if invariant, aren't enough. What you have now (lock-guard-esque, token knows its source) is the best I've seen.

1 Like

Two things I can think of. The first is adding a compile-time guarantee with a type parameter on Machine, if applicable.

struct Machine<T> {
    _phantom: PhantomData<T>,

struct Token<'a, T> {
    _phantom: PhantomData<&'a T>,

struct M1;

struct M2;

fn main() {
    let m1 = Machine::<M1>::default();
    let m2 = Machine::<M2>::default();

    let token = m1.get_token();
    m2.give_token(token); // compile error

It might not be possible to add a type parameter. In this case, runtime checks can be done. Each Machine is initialized with a u64 using a PRNG, and each Token gets a copy of it. Runtime checks are assertions that verify the "unique" IDs are equal.


I'm confused as to how these are related. Adding a method is a type-level operation, it doesn't enlarge instances of the struct.

That doesn't make it invariant.

Have you seen GhostCell?


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.