Child struct reference to Parent using Trait

Hello, I'm writing a struct and its children with logger with Box<dyn Logging>, where the Box cannot be changed. Reason: https://github.com/mozilla/uniffi-rs/blob/727d6bd65efd89aaa06c8548d5729537939d9d26/docs/manual/src/udl/callback_interfaces.md


pub enum OriginType {
    Foo,
    Bar,
}

pub struct ResultRow {}

#[derive(Debug)]
pub struct DataImporter {
    logger: Box<dyn Logging>, // the `Box` here cannot be changed due to `uniffi-rs` (exposing this class for foreign languages
}

impl DataImporter {
    fn import(&self, origin_type: OriginType, data: String) -> Vec<ResultRow> {
        match origin_type {
            OriginType::Foo => {
                let importer = ChildImporter::new(self.into()); // not working
                // importer.import(origin_type, data)
            }
            OriginType::Bar => {
                let importer = AnotherChildImporter::new(self); // not working either: mismatched types
                // expected struct `Box<(dyn Logging + 'static)>`
                // found reference `&DataImporter`

                // importer.import(origin_type, data)
            }
        }
    }
}

// Child Importer would like to access the `Logging` functionality
pub struct ChildImporter {
    logger: Box<dyn Logging>, // not sure the type here? e.g. Weak<dyn Logging>
}

impl ChildImporter {
    fn new(logger: Box<dyn Logging>) -> Self {
        Self { logger: logger }
    }
}

pub struct AnotherChildImporter {
    logger: Box<dyn Logging>, // TODO: not sure the type here? e.g. Weak<dyn Logging>
}

impl AnotherChildImporter {
    fn new(logger: Box<dyn Logging>) -> Self {
        Self { logger: logger }
    }
}

pub enum LogLevel {
    Debug,
    Info,
    Warn,
    Error,
}

pub trait Logging: Send + Sync + std::fmt::Debug {
    fn log(&self, level: LogLevel, message: String);
}

// Want to delegate the call to `self.logger` by `self`
impl Logging for DataImporter {
    fn log(&self, level: LogLevel, message: String) {
        self.logger.log(level, message);
    }
}

I don't really get what the issue is or even what you are trying to achieve. The definitions of ChildImporter and AnotherChildImporter and their "not working" new() functions are missing, and it's not clear what they are supposed to do, or in what way the current setup is not working.

Sorry for the confusion! I've updated the example.

If the type on both the parent and the child must be Box, you've got nothing to do.

Could we let child call parent (where parent implements Logging), then in parent's implementation, it call self.logger?

Then you still need to get rid of the Box in Child. If you can do that, a reference to the logger might be enough: if not (for ownership reasons), and you cannot change the Box at Parent to Rc, then you may be able to hold a Rc to the parent in the child and let the parent implement log, as you suggested.

2 Likes

How to pass &self in a parent method to child with Rc<T>?

Not sure what exactly you are asking there; are you asking how you can get a &DataImporter from an Rc<DataImporter>? That's simple, Rc implements Deref so you can just access the methods and fields of the wrapped type directly, and an implicit coercion from &Rc<T> to &T will happen. You can also be explicit if you want, and obtain an intermediate reference via .as_ref() or .borrow().

1 Like

I want to expose Logging to children. For example,
let importer = AnotherChildImporter::new(self); // mismatched types
in an instance method of DataImporter.
I'm currently blocked by converting &self to Rc<dyn Logging>..

use std::{rc::Rc};

fn main() {
    println!("Hello, world!");
}

pub enum OriginType {
    Foo,
    Bar,
}

pub struct ResultRow {}

#[derive(Debug)]
pub struct DataImporter {
    logger: Box<dyn Logging>, // the `Box` here cannot be changed due to `uniffi-rs` (exposing this class for foreign languages
}

impl DataImporter {
    fn import(&self, origin_type: OriginType, data: String) {
        match origin_type {
            OriginType::Foo => {
                let importer = ChildImporter::new(self.to_owned()); // mismatched types
                // expected struct `Rc<(dyn Logging + 'static)>`
                // found reference `&DataImporter`
            }
            OriginType::Bar => {
                let importer = AnotherChildImporter::new(self); // mismatched types
                // expected struct `Rc<(dyn Logging + 'static)>`
                // found reference `&DataImporter

                // importer.import(origin_type, data)
            }
        }
    }
}

// Child Importer would like to access the `Logging` functionality
pub struct ChildImporter {
    logger: Rc<dyn Logging>,
}

impl ChildImporter {
    fn new(logger: Rc<dyn Logging>) -> Self {
        Self { logger: logger }
    }

    fn some_logic(&self) {
        self.logger.log(LogLevel::Debug, "test".to_string());
    }
}

pub struct AnotherChildImporter {
    logger: Rc<dyn Logging>, // TODO: not sure the type here? e.g. Weak<dyn Logging>
}

impl AnotherChildImporter {
    fn new(logger: Rc<dyn Logging>) -> Self {
        Self { logger: logger }
    }
}

pub enum LogLevel {
    Debug,
    Info,
    Warn,
    Error,
}

pub trait Logging: Send + Sync + std::fmt::Debug {
    fn log(&self, level: LogLevel, message: String);
}

// Want to delegate the call to `self.logger` by `self`
impl Logging for DataImporter {
    fn log(&self, level: LogLevel, message: String) {
        self.logger.log(level, message);
    }
}

It is not possible to convert a reference back to an Rc.

Oh, understand.

Any approach to achieve this?

also cc @chrefr

You have to put the parent in an Rc from the very beginning, and construct its children by handing out copies of that Rc to them. Maybe you could try to use the custom self type Rc<Self>:

impl DataImporter {
    fn import(self: Rc<Self>, origin_type: OriginType, data: String) {
        ...
    }
}

If we do not use Rc, for example, Weak, same rule will also apply?

Which rule?

Thanks!

It works as I expected.

use std::{rc::Rc, sync::Arc};

fn main() {
    println!("Hello, world!");
}

pub enum OriginType {
    Foo,
    Bar,
}

pub struct ResultRow {}

#[derive(Debug)]
struct LogContainer {
    pub logger: Box<dyn Logging>,
}

#[derive(Debug)]
pub struct DataImporter {
    // logger: Box<dyn Logging>, // the `Box` here cannot be changed due to `uniffi-rs` (exposing this class for foreign languages
    logContainer: Arc<LogContainer>,
}

impl DataImporter {
    fn import(&self, origin_type: OriginType, data: String) {
        match origin_type {
            OriginType::Foo => {
                let importer = ChildImporter::new(self.logContainer.clone()); // mismatched types
                                                                      // expected struct `Rc<(dyn Logging + 'static)>`
                                                                      // found reference `&DataImporter`
            }
            OriginType::Bar => {
                let importer = ChildImporter::new(self.logContainer.clone()); // mismatched types
                                                                      // expected struct `Rc<(dyn Logging + 'static)>`
                                                                      // found reference `&DataImporter

                // importer.import(origin_type, data)
            }
        }
    }
}

// Child Importer would like to access the `Logging` functionality
pub struct ChildImporter {
    logger: Arc<LogContainer>,
}

impl ChildImporter {
    fn new(logger: Arc<LogContainer>) -> Self {
        Self { logger: logger }
    }

    fn some_logic(&self) {
        self.logger.logger.log(LogLevel::Debug, "test".to_string());
    }
}

pub enum LogLevel {
    Debug,
    Info,
    Warn,
    Error,
}

pub trait Logging: Send + Sync + std::fmt::Debug {
    fn log(&self, level: LogLevel, message: String);
}

I meant

What if we use Weak?

It's the same. This is nothing specific to Rc, this is a fundamental problem. If you only have a reference to a wrapped thing, you can't reliably (or safely, for that matter) get a reference to whatever it is wrapped in, because a reference (by means of its very type) doesn't convey any other information as to what the thing might be wrapped in.

This is simply a consequene of the compositionality and the locality of the type system. If you have a &Foo, it might have come from:

  • a temporary or a const, wherever the compiler pleases to put it in memory
  • a stack-allocated variable
  • a static
  • a heap-allocated buffer (Box, Rc, or anything else)
  • a thread-local variable
  • an FFI-allocated chunk of memory

and that reference shouldn't need to care whether it came from the stack or the heap or anywhere else, or whether the value is stored inside another struct or enum variant or tuple or array (or …). It would simply be too complicated having to deal with such non-local information. Types (and values) should be useful and interpretable on their own, without having to know all of the (open-ended and vast) context that their inhabiting values can possibly come from.

5 Likes

Thanks for the detailed explanation! It helps a lot!

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.