miniextendr supports five R class systems. This guide helps you choose the right one for your use case.

πŸ”—Quick Comparison

FeatureEnvR6S3S4S7
Attribute#[miniextendr]#[miniextendr(r6)]#[miniextendr(s3)]#[miniextendr(s4)]#[miniextendr(s7)]
Method Callobj$method()obj$method()generic(obj)generic(obj)generic(obj)
EncapsulationWeakStrongNoneModerateStrong
DependenciesNoneR6 packageNonemethods packageS7 package
Active BindingsNoYesNoNoYes (computed/dynamic properties)
InheritanceNoLimitedS3 dispatchS4 dispatchS7 dispatch
Best ForSimple APIsComplex stateTidyverse compatBioconductorModern OOP

πŸ”—Choosing a Class System

                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                         β”‚  Do you need method dispatch on     β”‚
                         β”‚  object type (polymorphism)?        β”‚
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ No                                      β”‚ Yes
                    β–Ό                                         β–Ό
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   Use Env style  β”‚              β”‚  Do you need tidyverse       β”‚
         β”‚   (simplest)     β”‚              β”‚  compatibility?              β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                        β”‚
                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                   β”‚ Yes                                     β”‚ No
                                   β–Ό                                         β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  Use S3           β”‚           β”‚  Need reference semantics    β”‚
                        β”‚  (generic.class)  β”‚           β”‚  (modify in place)?          β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                     β”‚
                                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                              β”‚ Yes                                       β”‚ No
                                              β–Ό                                           β–Ό
                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                   β”‚  Use R6           β”‚                   β”‚  Modern or legacy?    β”‚
                                   β”‚  (encapsulation)  β”‚                   β”‚                       β”‚
                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                                     β”‚
                                                             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                             β”‚ Modern                                β”‚ Legacy
                                                             β–Ό                                       β–Ό
                                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                  β”‚  Use S7           β”‚                 β”‚  Use S4           β”‚
                                                  β”‚  (new standard)   β”‚                 β”‚  (Bioconductor)   β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”—Environment Style (Default)

The simplest approach. Methods are functions attached to an environment.

πŸ”—Rust Code

#[derive(miniextendr_api::ExternalPtr)]
pub struct Counter {
    value: i32,
}

#[miniextendr]  // env is default
impl Counter {
    /// Create a new counter.
    pub fn new(initial: i32) -> Self {
        Counter { value: initial }
    }

    /// Get the current value.
    pub fn value(&self) -> i32 {
        self.value
    }

    /// Increment by one.
    pub fn inc(&mut self) {
        self.value += 1;
    }
}

πŸ”—Generated R Code

Counter <- local({
    e <- new.env(parent = emptyenv())

    e$new <- function(initial) {
        ptr <- .Call(C_Counter__new, initial)
        structure(
            list(.ptr = ptr),
            class = "Counter"
        )
    }

    e$value <- function(x) {
        .Call(C_Counter__value, x$.ptr)
    }

    e$inc <- function(x) {
        .Call(C_Counter__inc, x$.ptr)
        invisible(x)
    }

    e
})

πŸ”—Usage

c <- Counter$new(0L)
c$value()      # 0
c$inc()
c$value()      # 1

πŸ”—When to Use

  • Simple APIs with few methods
  • No need for method dispatch
  • Minimal dependencies
  • Quick prototyping

πŸ”—R6 Style

Full-featured reference classes with encapsulation.

πŸ”—Rust Code

#[derive(miniextendr_api::ExternalPtr)]
pub struct Rectangle {
    width: f64,
    height: f64,
}

#[miniextendr(r6)]
impl Rectangle {
    pub fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }

    pub fn get_width(&self) -> f64 {
        self.width
    }

    pub fn set_width(&mut self, width: f64) {
        self.width = width;
    }

    /// Active binding for computed property.
    #[miniextendr(r6(active))]
    pub fn area(&self) -> f64 {
        self.width * self.height
    }

    /// Private method.
    fn validate(&self) -> bool {
        self.width > 0.0 && self.height > 0.0
    }

    /// Static method.
    pub fn square(size: f64) -> Self {
        Rectangle { width: size, height: size }
    }
}

πŸ”—Generated R Code

Rectangle <- R6::R6Class("Rectangle",
    public = list(
        initialize = function(width, height, .ptr = NULL) {
            if (!is.null(.ptr)) {
                private$.ptr <- .ptr
            } else {
                private$.ptr <- .Call(C_Rectangle__new, width, height)
            }
        },
        get_width = function() {
            .Call(C_Rectangle__get_width, private$.ptr)
        },
        set_width = function(width) {
            .Call(C_Rectangle__set_width, private$.ptr, width)
        }
    ),
    private = list(
        .ptr = NULL,
        validate = function() {
            .Call(C_Rectangle__validate, private$.ptr)
        }
    ),
    active = list(
        area = function() {
            .Call(C_Rectangle__area, private$.ptr)
        }
    ),
    lock_objects = TRUE,
    lock_class = FALSE,
    cloneable = FALSE
)

# Static method
Rectangle$square <- function(size) {
    Rectangle$new(.ptr = .Call(C_Rectangle__square, size))
}

πŸ”—Usage

r <- Rectangle$new(3, 4)
r$get_width()    # 3
r$area           # 12 (active binding, no parens!)
r$set_width(5)
r$area           # 20

# Static method
s <- Rectangle$square(5)
s$area           # 25

πŸ”—When to Use

  • Complex state management
  • Need private methods
  • Active bindings (computed properties)
  • Reference semantics (modify in place)

πŸ”—Field Access via Sidecar

For R6 and Env classes, the sidecar pattern (#[r_data] + RSidecar) provides zero-overhead field access as R6 active bindings:

#[r_data]
pub struct MyData {
    pub name: String,
    pub value: f64,
}

r_data_accessors!(MyStruct, MyData);

This generates obj$name and obj$value active bindings automatically. See the R6 section above for a complete example.


πŸ”—S3 Style

Traditional R generic function dispatch.

πŸ”—Rust Code

#[derive(miniextendr_api::ExternalPtr)]
pub struct Person {
    name: String,
    age: i32,
}

#[miniextendr(s3)]
impl Person {
    pub fn new(name: String, age: i32) -> Self {
        Person { name, age }
    }

    /// Implements print.Person β€” &mut self triggers invisible(x) return
    #[miniextendr(generic = "print")]
    pub fn show(&mut self) {
        println!("Person: {}, age {}", self.name, self.age);
    }

    /// Implements format.Person
    #[miniextendr(generic = "format")]
    pub fn fmt(&self) -> String {
        format!("{} ({})", self.name, self.age)
    }

    pub fn greet(&self) -> String {
        format!("Hello, I'm {}!", self.name)
    }
}

πŸ”—Generated R Code

#' @export
new_person <- function(name, age) {
    ptr <- .Call(C_Person__new, name, age)
    structure(ptr, class = "Person")
}

#' @export
print.Person <- function(x, ...) {
    .Call(C_Person__show, x)
    invisible(x)
}

#' @export
format.Person <- function(x, ...) {
    .Call(C_Person__fmt, x)
}

#' @export
greet <- function(x, ...) UseMethod("greet")

#' @export
greet.Person <- function(x, ...) {
    .Call(C_Person__greet, x)
}

πŸ”—Usage

p <- new_person("Alice", 30)
print(p)         # Person: Alice, age 30
format(p)        # "Alice (30)"
greet(p)         # "Hello, I'm Alice!"

# Works with tidyverse
tibble::tibble(person = list(p))

πŸ”—When to Use

  • Tidyverse integration
  • Extending existing generics (print, format, etc.)
  • vctrs-compatible types
  • Simple polymorphism

πŸ”—S4 Style

Formal class system with slots and method signatures.

πŸ”—Rust Code

#[derive(miniextendr_api::ExternalPtr)]
pub struct Gene {
    symbol: String,
    chromosome: i32,
}

#[miniextendr(s4)]
impl Gene {
    pub fn new(symbol: String, chromosome: i32) -> Self {
        Gene { symbol, chromosome }
    }

    pub fn symbol(&self) -> String {
        self.symbol.clone()
    }

    pub fn chromosome(&self) -> i32 {
        self.chromosome
    }

    #[miniextendr(generic = "show")]
    pub fn display(&self) {
        println!("Gene {} on chr{}", self.symbol, self.chromosome);
    }
}

πŸ”—Generated R Code

setClass("Gene", contains = "externalptr")

#' @export
Gene <- function(symbol, chromosome) {
    ptr <- .Call(C_Gene__new, symbol, chromosome)
    new("Gene", ptr)
}

setGeneric("symbol", function(object) standardGeneric("symbol"))
setMethod("symbol", "Gene", function(object) {
    .Call(C_Gene__symbol, object)
})

setGeneric("chromosome", function(object) standardGeneric("chromosome"))
setMethod("chromosome", "Gene", function(object) {
    .Call(C_Gene__chromosome, object)
})

setMethod("show", "Gene", function(object) {
    .Call(C_Gene__display, object)
})

πŸ”—Usage

g <- Gene("TP53", 17L)
symbol(g)       # "TP53"
chromosome(g)   # 17
show(g)         # Gene TP53 on chr17

πŸ”—When to Use

  • Bioconductor packages
  • Formal class hierarchies
  • Strict type checking
  • Legacy S4 codebases

πŸ”—S7 Style

Modern OOP system (successor to S3/S4).

πŸ”—Rust Code

#[derive(miniextendr_api::ExternalPtr)]
pub struct Point {
    x: f64,
    y: f64,
}

#[miniextendr(s7)]
impl Point {
    pub fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }

    pub fn x(&self) -> f64 {
        self.x
    }

    pub fn y(&self) -> f64 {
        self.y
    }

    pub fn distance(&self, other: &Point) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }

    #[miniextendr(generic = "base::print")]
    pub fn show(&self) {
        println!("Point({}, {})", self.x, self.y);
    }
}

πŸ”—Generated R Code

Point <- S7::new_class("Point",
    properties = list(
        .ptr = S7::class_any
    ),
    constructor = function(x, y, .ptr = NULL) {
        if (!is.null(.ptr)) {
            S7::new_object(S7::S7_object(), .ptr = .ptr)
        } else {
            S7::new_object(S7::S7_object(),
                .ptr = .Call(C_Point__new, x, y))
        }
    }
)

S7::method(x, Point) <- function(x) {
    .Call(C_Point__x, x@.ptr)
}

S7::method(y, Point) <- function(x) {
    .Call(C_Point__y, x@.ptr)
}

S7::method(distance, Point) <- function(x, other) {
    .Call(C_Point__distance, x@.ptr, other@.ptr)
}

S7::method(print, Point) <- function(x, ...) {
    .Call(C_Point__show, x@.ptr)
    invisible(x)
}

πŸ”—Usage

p1 <- Point(0, 0)
p2 <- Point(3, 4)
x(p1)              # 0
distance(p1, p2)   # 5
print(p1)          # Point(0, 0)

πŸ”—When to Use

  • New packages without legacy constraints
  • Clean, modern OOP design
  • Computed and dynamic properties (see below)
  • S7 ecosystem integration

πŸ”—S7 Computed and Dynamic Properties

S7 supports properties that are computed from Rust methods. Use #[miniextendr(s7(getter))] for read-only computed properties and add #[miniextendr(s7(setter, prop = "name"))] for read-write dynamic properties.

πŸ”—Rust Code

#[derive(miniextendr_api::ExternalPtr)]
pub struct Range {
    start: f64,
    end: f64,
}

#[miniextendr(s7)]
impl Range {
    pub fn new(start: f64, end: f64) -> Self {
        Range { start, end }
    }

    /// Computed property (read-only): length of the range.
    /// Accessed as obj@length in R.
    #[miniextendr(s7(getter))]
    pub fn length(&self) -> f64 {
        self.end - self.start
    }

    /// Dynamic property getter: read the midpoint.
    #[miniextendr(s7(getter, prop = "midpoint"))]
    pub fn get_midpoint(&self) -> f64 {
        (self.start + self.end) / 2.0
    }

    /// Dynamic property setter: set the midpoint.
    /// Adjusts start and end to maintain length while centering on new midpoint.
    #[miniextendr(s7(setter, prop = "midpoint"))]
    pub fn set_midpoint(&mut self, value: f64) {
        let half = (self.end - self.start) / 2.0;
        self.start = value - half;
        self.end = value + half;
    }

    /// Regular method (not a property).
    pub fn start(&self) -> f64 {
        self.start
    }
}

πŸ”—Generated R Code

Range <- S7::new_class("Range",
    properties = list(
        .ptr = S7::class_any,
        length = S7::new_property(
            getter = function(self) .Call(C_Range__length, self@.ptr)
        ),
        midpoint = S7::new_property(
            getter = function(self) .Call(C_Range__get_midpoint, self@.ptr),
            setter = function(self, value) {
                .Call(C_Range__set_midpoint, self@.ptr, value)
                self
            }
        )
    ),
    constructor = function(start, end, .ptr = NULL) { ... }
)

# Regular method as S7 generic
S7::method(start, Range) <- function(x, ...) .Call(C_Range__start, x@.ptr)

πŸ”—Usage

r <- Range(0, 10)

# Computed property (read-only)
r@length         # 10

# Dynamic property (read-write)
r@midpoint       # 5
r@midpoint <- 10 # Adjusts start/end
r@midpoint       # 10
start(r)         # 5 (new start after midpoint shift)
r@length         # 10 (length preserved)

πŸ”—Property Attributes

AttributeDescription
#[miniextendr(s7(getter))]Read-only computed property. Property name = method name.
#[miniextendr(s7(getter, prop = "name"))]Getter with custom property name.
#[miniextendr(s7(setter, prop = "name"))]Setter for a dynamic property. Must match a getter’s prop name.

Rules:

  • A getter without a setter creates a computed (read-only) property
  • A getter + setter with the same prop name creates a dynamic (read-write) property
  • Property methods are NOT exposed as S7 generics (accessed via @ only)
  • Setters must take exactly one parameter (the new value)

πŸ”—Feature Comparison Matrix

πŸ”—Constructor Patterns

SystemConstructor NameReturns
EnvTypeName$new()Environment with class
R6TypeName$new()R6 object
S3new_typename()Object with class attribute
S4TypeName()S4 object
S7TypeName()S7 object

πŸ”—Method Access

SystemInstance MethodStatic Method
Envobj$method()TypeName$method()
R6obj$method()TypeName$method()
S3method(obj)typename_method()
S4method(obj)TypeName_method()
S7method(obj)TypeName$method()

πŸ”—Mutable Receivers (&mut self)

All class systems support mutable receivers. The Rust method:

pub fn increment(&mut self) {
    self.value += 1;
}

Modifies the underlying data in place. The R object reference remains valid.


πŸ”—Multiple Impl Blocks

You can have multiple impl blocks with labels:

#[miniextendr(s3, label = "core")]
impl MyType {
    pub fn new() -> Self { ... }
    pub fn value(&self) -> i32 { ... }
}

#[miniextendr(s3, label = "math")]
impl MyType {
    pub fn add(&mut self, x: i32) { ... }
    pub fn multiply(&mut self, x: i32) { ... }
}

Both blocks generate methods for the same type.


πŸ”—Trait Implementations

For cross-package interoperability:

#[miniextendr]
pub trait Counter {
    fn value(&self) -> i32;
    fn increment(&mut self);
}

#[miniextendr]
impl Counter for MyCounter {
    fn value(&self) -> i32 { self.count }
    fn increment(&mut self) { self.count += 1; }
}

This enables type-erased dispatch across package boundaries.


πŸ”—Direct Field Access via Sidecar

The sidecar pattern (#[r_data] + RSidecar + r_data_accessors!) is the recommended approach for exposing struct fields directly to R. It separates R-visible fields from Rust-internal state, and generates accessor functions appropriate to each class system.

πŸ”—How It Works

  1. Define a sidecar struct with #[r_data] containing the fields you want to expose to R.
  2. Call r_data_accessors!(MainStruct, SidecarStruct) to generate accessor trait impls.
  3. The constructor returns (Self, SidecarData) instead of just Self.

πŸ”—Rust Code

use miniextendr_api::{r_data_accessors, RSidecar};

#[derive(ExternalPtr)]
pub struct MyConfig {
    // Rust-only internal state
    cache: Vec<u8>,
}

/// Fields exposed to R.
#[r_data]
pub struct MyConfigData {
    pub name: String,
    pub score: f64,
}

r_data_accessors!(MyConfig, MyConfigData);

#[miniextendr(r6)]  // Works with r6, env, s3, s4, s7
impl MyConfig {
    pub fn new(name: String, score: f64) -> (Self, MyConfigData) {
        (MyConfig { cache: vec![] }, MyConfigData { name, score })
    }
}

πŸ”—R Behavior by Class System

SystemGetSet
R6obj$name (active binding)obj$name <- "new"
EnvMyConfig_get_name(obj)MyConfig_set_name(obj, "new")
S3name(obj) (generic)name<-(obj, "new")
S4name(obj) (S4 method)name<-(obj, "new")
S7obj@name (S7 property)obj@name <- "new"

πŸ”—When to Use Sidecar vs Manual Getters

  • Use sidecar when you have multiple fields to expose and want zero-boilerplate accessors.
  • Use manual getters when you need computed values, validation, or side effects on access. Manual getters work identically across all class systems and are straightforward to write.

πŸ”—Export Control

Control R export visibility with #[miniextendr(internal)] and #[miniextendr(noexport)]. These work consistently across all five class systems.

πŸ”—#[miniextendr(internal)]

Adds @keywords internal to roxygen and suppresses @export. The function still gets an .Rd man page but is hidden from the package index.

#[miniextendr(internal)]
pub fn helper_function(x: i32) -> i32 { x * 2 }

#[miniextendr(s3, internal)]
impl InternalType {
    pub fn new() -> Self { ... }
}

πŸ”—#[miniextendr(noexport)]

Suppresses @export only (no @keywords internal). The function gets @noRd and no man page is generated.

#[miniextendr(noexport)]
pub fn private_helper(x: i32) -> i32 { x * 2 }

πŸ”—Comparison

Attribute@export@keywords internalMan page
(default)YesNoYes
internalNoYesYes (hidden from index)
noexportNoNoNo (@noRd)

πŸ”—S4 Helpers Module

miniextendr provides Rust helpers for interoperating with existing S4 objects (e.g., Bioconductor). These are for reading/writing S4 objects passed as arguments, not for generating S4 classes (use #[miniextendr(s4)] for that).

use miniextendr_api::s4_helpers;

unsafe {
    // Check if an object is S4
    if s4_helpers::s4_is(obj) {
        // Get the class name
        let class = s4_helpers::s4_class_name(obj); // Option<String>

        // Check and access slots
        if s4_helpers::s4_has_slot(obj, "data") {
            let data = s4_helpers::s4_get_slot(obj, "data")?; // Result<SEXP, String>
        }

        // Set a slot value
        s4_helpers::s4_set_slot(obj, "label", new_value)?;
    }
}
FunctionPurpose
s4_is(obj)Check if SEXP is an S4 object
s4_class_name(obj)Get the S4 class name as Option<String>
s4_has_slot(obj, name)Check if a slot exists
s4_get_slot(obj, name)Get a slot value as Result<SEXP, String>
s4_set_slot(obj, name, value)Set a slot value

All functions require the R main thread and operate on raw SEXP values.


πŸ”—Recommendations

  1. Start with Env for simple cases
  2. Use R6 when you need encapsulation or active bindings
  3. Use S3 for tidyverse compatibility
  4. Use S4 for Bioconductor integration
  5. Use S7 for new packages wanting modern OOP

When in doubt, start with the default (Env) and migrate if needed.