I've got something like this, where a "writer" owns a buffer, and allows allocating things within:
let mut writer = Writer::new();
let handle = writer.create::<Something>();
writer.create_with_param::<AnotherThing>(handle);
A handle is specific to a given writer, and you cannot do anything with it except pass it back to the writer.
But I would like to avoid mixing handles between writers:
let mut writer1 = Writer::new();
let mut writer2 = Writer::new();
let handle1 = writer1.create();
writer1.create_with_param(handle1); // This is OK
writer2.create_with_param(handle1); // I would like this to not compile
Is there a way to achieve this ?
I'm using lifetimes in the full code, so the handle is something like Handle<'writer>, but then with:
If your requirement is that "method X of object Y must always take object Z as parameter", then it's not really a parameter! It's not something that your caller can choose; instead it is an internal invariant that must be upheld by the type itself.
So, don't take a parameter – store it instead!
struct Handle<'a> {
parent_writer: &'a mut Writer,
}
impl Handle<'_> {
fn create_with_param(&mut self) {
// `self.parent_writer` is always the correct writer
}
}
impl Writer {
fn create(&mut self) -> Handle<'_> {
Handle { parent_writer: self }
}
}
fn main() {
let handle1 = writer1.create();
handle1.create_with_param(); // this will always use `writer1`
}
The issue is that I need several handles alive at a given time. They need to be passed back to the writer, and some of the create methods take several handles. So I don't think I can have them borrow the writer mutable like you suggested, nor own it.
I'm not sure this works for you, and maybe requires some more manual work than ideal, but... would it be possible that
writer is generic over some type T
when you create a writer, you set T to some concrete zero-sized struct that you create specifically for that writer. This concrete struct type in essence becomes your writer identity.
handler is also generic over T and when writer creates a handler, the returned handler has the same concrete T type
when you pass the handler to the writer, you use type constraints to guarantee that the identity type T is the same for both the writer and the handler
Potentially, the creation of the writer along with the marker type (the struct MarkerX; and let mut writerX = Writer::<MarkerX>::new(); in the code) could be combined, hidden and automated using a macro. Something like let mut writer1 = new_writer!("params to Writer::new function") where every time the macro is called, a new struct MarkerX; is created specifically for that writer.
Hmm I see. The macro I proposed is flawed. Either: 1) don't use the macro and provide types manually (not as practical), 2) use the macro but avoid these cases (not guaranteed) or 3) fix the macro (although I don't know how, but maybe there is a way to guarantee a new type on every invocation).
EDIT: maybe const generics can also be used (at least in the non-macro case), which I find simpler:
use core::marker::PhantomData;
struct Handle<const X: u64> {
// etc
}
struct Writer<const X: u64> {
// etc
}
impl<const X: u64> Writer<X> {
fn new() -> Self {
Self {}
}
fn get_handle(&mut self) -> Handle<X> {
Handle {}
}
fn use_handle(&mut self, handler: &Handle<X>) {
}
}
fn main() {
let mut writer1 = Writer::<1>::new();
let mut writer2 = Writer::<2>::new();
let handle1 = writer1.get_handle();
writer1.use_handle(&handle1); // Compiles
// writer2.use_handle(&handle1); // Does not compile
let handle2 = writer2.get_handle();
// writer1.use_handle(&handle2); // Does not compile
writer2.use_handle(&handle2); // Compiles
}
EDIT EDIT: regarding the vector case, that seems like a flawed example from the start. The point is to have compiler guarantees. Once you put a few writers into a Vec, the compiler is no longer able to help you regarding which writer you can use for a specific handle, right? At most, you can check at run time with some kind of cast or relfection check. But the compiler should at least give you an error to state that the writer-handle pair are not guaranteed to match, so the macro is still wrong. Personally, I think I would use const generics here.
The vector case is similar to my first example: the issue is that several writers have the same type, so the Writer<X> is not enough to prevent mixing them. Also, given a writer, several handles can be used; there are several create(...) methods.
The easier way would be if I somehow could have the "actual lifetime" as a parameter of the writer, rather than "any suitable lifetime", making each instance have its own type. (That maybe wouldn't solve the case with the vector, but if it works for other cases, that would be ok)
pub fn wrong_usage() {
let mut w1 = Writer::new(OpaqueBox::new(|| ()));
let mut w2 = Writer::new(OpaqueBox::new(|| ()));
let ha = w1.create_simple_a();
let hb = w1.create_simple_b();
w2.create_composite(ha, hb);
}
The OpaqueBox is just there to force a move of the closure (in case a caller was tempted to reuse the same closure twice), since a regular Box or lack of it allows T to be just a reference.
The code to create a writer is quite ugly with this, but I might just use a macro like new_writer!() for that.
I already have macros for the whole thing anyway.
With this, anyone using the macro cannot get it wrong.
Another trick/hack that I'm using is to declare some functions (here new()) unsafe, and have the macro-generated code call it knowing that. (Since the macro-generated code lives in the client crate, I cannot use regular access control).
Again, that leaves an opening for a really motivated user to break things for themselves, but my goal is to prevent accidental mistakes, rather than malicious manipulation.
This thread reminds me of various others I've seen about trying to produce singletons (e.g. for embedded projects). All the type-based approaches basically boil down to "for a given span of code with all parameters monomorphized, there can be only one type, because Rust is statically typed". So if you're in a loop for example, a given closure will have a unique type compared to other closures, but the same type every time through the loop.
So to get it wrong with your macro, I just need to have something like
// Off the cuff but I think you'll get the idea
let mut writer1 = None;
let mut writer2 = None;
loop {
let writer = if writer1.is_none() {
&mut writer1
} else if writer2.is_none() {
&mut writer 2
} else {
break
};
*writer = Some( your_macro!() );
}
let writer1 = writer1.unwrap();
let writer2 = writer2.unwrap();
Or as @cuviper pointed out, just the return of any function, modulo its parameters.
Which is to say, your macro can make things less error prone, but I haven't seen any way to get a static type-based guarantee.
It's been a year or two though, maybe look around for "singletons" to see where the state of the art is at.
(The example above may seem contrived, but consider collecting a Vec of these or what-have-you.)
I don't really want a singleton, there can be two writers for a given "Domain", I just don't want the handles to get mixed.
I will try to live with the loop/function issue being a risk for now, assuming the creation code will probably straightforward like:
let main_writer = new_writer!();
let auxiliary_writer = new_writer()!;
I think the LCellOwner suggested by @2e71828 looks like a good working solution, for the longer term, when I get to refactor things more. It forces a weird usage (having to write the consuming code in a closure) but I guess it's the only way.