How to implement S3 generics (print, format, etc.) with #[miniextendr(s3)].

πŸ”—Quick Example

use miniextendr_api::{miniextendr, ExternalPtr};

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

#[miniextendr(s3)]
impl Person {
    /// @param name Name of the person.
    /// @param age Age of the person.
    pub fn new(name: String, age: i32) -> Self {
        Person { name, age }
    }

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

    /// Implements print.Person - prints and returns self invisibly.
    #[miniextendr(generic = "print")]
    pub fn show(&mut self) {
        println!("Person: {}, age {}", self.name, self.age);
    }

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

// Registration is automatic via #[miniextendr].

Generated R code:

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

# format.Person - returns the string directly
format.Person <- function(x, ...) {
  .Call(C_Person__fmt, x)
}

# print.Person - calls Rust, then returns invisible(x)
print.Person <- function(x, ...) {
  .Call(C_Person__show, x)
  invisible(x)
}

# greet generic + method
greet <- function(x, ...) UseMethod("greet")
greet.Person <- function(x, ...) {
  .Call(C_Person__greet, x)
}

πŸ”—Key Concepts

πŸ”—Generic Override with #[miniextendr(generic = "...")]

By default, the Rust method name becomes the S3 generic name. Use generic = "..." to override this, mapping to a different R generic:

// Rust method is `fmt`, but R generic is `format`
#[miniextendr(generic = "format")]
pub fn fmt(&self) -> String { ... }

// Rust method is `show`, but R generic is `print`
#[miniextendr(generic = "print")]
pub fn show(&mut self) { ... }

This is essential for base R generics like print and format where you want your Rust method to have a more descriptive name.

πŸ”—Dots (...)

All S3 method signatures include ... automatically. You don’t need to declare them in your Rust signature. The generated R wrapper adds ... to the parameter list:

# Generated: always has (x, ...) or (x, other_params, ...)
format.Person <- function(x, ...) { ... }
greet.Person <- function(x, ...) { ... }

If your Rust method takes extra parameters, they appear between x and ...:

pub fn describe(&self, verbose: bool) -> String { ... }
// β†’ describe.Person <- function(x, verbose, ...) { ... }

πŸ”—Constructor

The constructor is always the method returning Self. It generates a function named new_<classname_lowercase>() that wraps the result with structure(..., class = "ClassName"):

pub fn new(name: String, age: i32) -> Self { ... }
// β†’ new_person <- function(name, age) {
//     structure(.Call(C_Person__new, name, age), class = "Person")
//   }

πŸ”—Static Methods

Methods without self become standalone functions prefixed with the lowercase class name:

pub fn species() -> String { "Homo sapiens".into() }
// β†’ person_species <- function() { .Call(C_Person__species) }

πŸ”—Implementing print

R convention: print() displays output and returns invisible(x) so the object can be used in pipelines without double-printing.

Recommended pattern: use &mut self returning ():

#[miniextendr(generic = "print")]
pub fn show(&mut self) {
    println!("Person: {}, age {}", self.name, self.age);
}

This generates the ChainableMutation return strategy:

print.Person <- function(x, ...) {
  .Call(C_Person__show, x)
  invisible(x)
}

The &mut self + void return triggers invisible(x) after the .Call(). This matches the R convention where print() returns the object invisibly.

Why &mut self? The invisible(x) pattern is only generated for &mut self methods returning (). With &self returning (), the generated code would be a bare .Call(...) returning NULL, which is functional but doesn’t follow R convention. If your print method doesn’t actually mutate, using &mut self is a pragmatic choice to get correct R behavior.

Alternative: &self returning ():

#[miniextendr(generic = "print")]
pub fn show(&self) {
    println!("Person: {}, age {}", self.name, self.age);
}

Generates:

print.Person <- function(x, ...) {
  .Call(C_Person__show, x)
}

This works (the println! output appears), but the function returns NULL instead of invisible(x). For most interactive use this is fine, but y <- print(x) gives NULL rather than x.

πŸ”—Implementing format

R convention: format() returns a character vector representation. Many R functions use format() internally (e.g., paste(), cat(format(x))).

Recommended pattern: &self returning String:

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

Generates:

format.Person <- function(x, ...) {
  .Call(C_Person__fmt, x)
}

The string is returned directly (visible), which is correct for format().

Tip: Implement format even if you also implement print. Other R code may call format() on your object (e.g., when building error messages or tibble displays).

πŸ”—Implementing Both print and format

A complete type should implement both. The standard R pattern is for print to call format internally, but with miniextendr each method dispatches to separate Rust code:

#[derive(ExternalPtr)]
pub struct Temperature {
    celsius: f64,
}

#[miniextendr(s3)]
impl Temperature {
    pub fn new(celsius: f64) -> Self {
        Temperature { celsius }
    }

    #[miniextendr(generic = "format")]
    pub fn fmt(&self) -> String {
        format!("{:.1}Β°C", self.celsius)
    }

    #[miniextendr(generic = "print")]
    pub fn show(&mut self) {
        println!("{:.1}Β°C", self.celsius);
    }
}

Usage in R:

t <- new_temperature(36.6)
print(t)    # 36.6Β°C  (returns invisible(t))
format(t)   # "36.6Β°C" (returns the string)
cat(format(t), "\n")  # 36.6Β°C

πŸ”—Standalone S3 methods (class defined elsewhere)

The #[miniextendr(s3)]-on-impl pattern above owns both the constructor and the methods: the Rust type Person is the S3 class. That is the right tool when Rust holds the canonical data. It is the wrong tool when:

  • The class is defined in R (an existing structure(list(), class = "my_thing") or a package you don’t control).
  • The class is a vctrs type where the β€œobject” is an R vector with a class attribute, not a Rust struct.
  • You want to provide a method for a base R class (print.data.frame, format.POSIXct) from a Rust extension.

For those cases, drop the impl block and write a plain function:

use miniextendr_api::{miniextendr, SEXP};

/// Implements `format.percent` in Rust.
#[miniextendr(s3(generic = "format", class = "percent"))]
pub fn format_percent(x: SEXP, _dots: ...) -> Vec<String> {
    let data: &[f64] = unsafe { x.as_slice::<f64>() };
    data.iter().map(|v| format!("{:.1}%", v * 100.0)).collect()
}

What the macro does:

  1. Emits an R wrapper named format.percent (the <generic>.<class> convention).
  2. Adds #' @method format percent to the roxygen block. devtools::document() picks that up and writes S3method(format, percent) into NAMESPACE. You do not call .S3method() yourself.
  3. Registers the underlying C entry point via distributed_slice, same as every other #[miniextendr] function.

Your Rust function receives exactly the arguments R dispatches with. For most S3 generics that is (x, ...), so the first param is typed (SEXP, or a concrete type with TryFromSexp) and the last is _dots: ... to absorb the R-level .... If the generic takes more positional arguments (vec_cast(x, to, ...)), list them in order.

πŸ”—Double dispatch (vctrs)

A few generics dispatch on two arguments. vctrs calls vec_ptype2(x, y, ...) and resolves the method as vec_ptype2.<class_of_x>.<class_of_y>. Encode that as a dotted class string:

#[miniextendr(s3(generic = "vec_ptype2", class = "percent.percent"))]
pub fn vec_ptype2_percent_percent(_x: SEXP, _y: SEXP, _dots: ...) -> SEXP { ... }

#[miniextendr(s3(generic = "vec_cast", class = "percent.double"))]
pub fn vec_cast_percent_double(x: SEXP, _to: SEXP, _dots: ...) -> SEXP { ... }

The wrapper name becomes vec_ptype2.percent.percent, and the roxygen becomes #' @method vec_ptype2 percent.percent. That is what vctrs expects.

πŸ”—vctrs @importFrom

The macro recognizes the vctrs generic names (vec_ptype_abbr, vec_proxy, vec_restore, vec_ptype2, vec_cast, etc.) and auto-injects #' @importFrom vctrs <generic> into the roxygen block. This forces vctrs to load before your method is registered, which is necessary for R_GetCCallable lookups (vctrs registers its native generics via ccallable).

For non-vctrs generics from other packages (e.g., tibble::tbl_sum), add the import manually:

/// @importFrom tibble tbl_sum
#[miniextendr(s3(generic = "tbl_sum", class = "my_tbl"))]
pub fn tbl_sum_my_tbl(x: SEXP, _dots: ...) -> Vec<String> { ... }

πŸ”—Constraints

  • s3(...) requires class. generic defaults to the Rust function name if omitted, but supplying it explicitly is the convention.
  • Cannot be combined with r_name = "...". The S3 naming (<generic>.<class>) already fixes the R wrapper name.
  • The constructor for the class is out of scope for the method function. Either write it in R, or write a separate #[miniextendr] function that returns structure(x, class = "my_class") via new_vctr(...) or equivalent helpers in miniextendr_api::vctrs.

πŸ”—When to pick which

You have…Use
A Rust struct that owns the data, methods hang off it#[miniextendr(s3)] impl MyType { ... }
An R-side class (vctrs, base R, another package), methods are Rust logicstandalone #[miniextendr(s3(generic = ..., class = ...))] functions
A vctrs class authored here#[miniextendr(vctrs)] on impl for the constructor + static methods, standalone s3(...) functions for the dispatch-on-class-attribute generics

πŸ”—Summary Table

MethodReceiverReturnGenerated RR Convention
format&selfString.Call(...)Returns formatted string (visible)
print&mut self().Call(...); invisible(x)Returns self invisibly
print&self().Call(...)Returns NULL (works but unconventional)
custom&selfany.Call(...)Returns value directly
custom&mut self().Call(...); invisible(x)Chainable mutation

πŸ”—See Also