Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: inheritance and trait in magnus #140

Open
erickguan opened this issue Feb 15, 2025 · 1 comment · May be fixed by #143
Open

Question: inheritance and trait in magnus #140

erickguan opened this issue Feb 15, 2025 · 1 comment · May be fixed by #143

Comments

@erickguan
Copy link
Contributor

erickguan commented Feb 15, 2025

Hi, I hope you get a head start in the new year. In fact, the influenza is wild this year. Hope you are well too!

I am starting the final part of OpenDAL's Ruby binding. I am implementing:

  • A base class called Layer that works as a marker to accept any "layers". A layer is a concept that adds functionality to an operator (Operator is the main API to access files).
  • RetryLayer
  • ConcurrentLimitLayer.

Both of these subclasses are builders that I will build binding functions for users to configure. Usage should look like this:

layer = RetryLayer.new
op = Operator.new.layer(layer)

On Rust end, the Operator#layer is simple:

    fn layer(&self, layer: &layers::Layer) -> Result<Self, Error> {
        let op = layer.0.layer()
        // ...
    }

The layers that I want to implement look like this:

pub trait RubyLayer: Send + Sync {
    fn layer(&self, op: Operator) -> Operator;
}

#[magnus::wrap(class = "OpenDAL::Layer", free_immediately, size)]
pub struct Layer(pub Box<dyn RubyLayer>);

#[magnus::wrap(class = "OpenDAL::RetryLayer", free_immediately, size)]
pub struct RetryLayer(Arc<Mutex<ocore::layers::RetryLayer>>);

impl RubyLayer for RetryLayer {
    fn layer(&self, op: Operator) -> Operator {
        op.layer(self.0.clone())
    }
}

impl RetryLayer {
    fn new() -> Self {
        Self(Arc::new(Mutex::new(ocore::layers::RetryLayer::default())))
    }
}

I want to have ways to:

  1. Build subclasses in Rust
  2. Bind a subclass to interface and pass that to a function.

The reason not to use enum is that most of the functions for these layers are different. What would be a good approach to use only a Rust function in a class hierarchy?

As a side, this idea of implementation is mostly from OpenDAL python binding and pyo3.

The code is here.

@erickguan
Copy link
Contributor Author

erickguan commented Mar 24, 2025

Finally reached the end of Magnus inheritance. I ended up going back to using an enum:

  • Another issue mentioned this approach.
  • Magnus doesn’t use rb_data_type_t.parent, but instead declares class_for.
  • Magnus and Ruby exchange type information via this enum.
  • Users benefit from static dispatch provided by Rust through the enum.

Here's a code example:

#[magnus::wrap(class = "OpenDAL::Layer", free_immediately, size)]
pub enum Layer {
    #[magnus(class = "OpenDAL::RetryLayer")]
    Retry(Arc<Mutex<ocore::layers::RetryLayer>>),

    #[magnus(class = "OpenDAL::ConcurrentLimitLayer")]
    ConcurrentLimit(Arc<Mutex<ocore::layers::ConcurrentLimitLayer>>),
}

impl Layer {
    fn new_retry_layer() -> Self {
        Layer::Retry(Arc::new(Mutex::new(ocore::layers::RetryLayer::default())))
    }

    fn new_concurrent_limit_layer(permits: usize) -> Self {
        Layer::ConcurrentLimit(Arc::new(Mutex::new(
            ocore::layers::ConcurrentLimitLayer::new(permits),
        )))
    }

    pub fn include(gem_module: &RModule) -> Result<(), Error> {
        let layer_class = gem_module.define_class("Layer", class::object())?;
        layer_class.undef_default_alloc_func();

        let retry_layer_class = gem_module.define_class("RetryLayer", layer_class)?;
        retry_layer_class.define_singleton_method("new", function!(Layer::new_retry_layer, 0))?;

        Ok(())
    }
}

Pros and cons of this approach:

  • Pro: Concise
  • Pro: Efficient
  • Con: You need to understand how TypedData is passed to Rust, and how the correct Rust type is identified.

I’m still unsure what exactly to document, but including an example like this is probably a good starting point.

@erickguan erickguan linked a pull request Mar 26, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant