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.

#[derive(Default)]
struct Machine<T> {
    _phantom: PhantomData<T>,
}

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

#[derive(Default)]
struct M1;

#[derive(Default)]
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.

2 Likes

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?

3 Likes

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.