Skip to content

Expose a stable native API for class and property access - #789

Open
t-kalinowski wants to merge 5 commits into
mainfrom
feat/native-api
Open

t-kalinowski wants to merge 5 commits into
mainfrom
feat/native-api

Conversation

@t-kalinowski

@t-kalinowski t-kalinowski commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Native extensions currently need to depend on S7's internal representation or call back into R to inspect objects. This adds a stable native API for retrieving S7 class definitions and reading properties through their getters.

  • S7_get_class() returns the stored S7 class definition, resolving shared class references, or R_NilValue when there is none.
  • S7_get_prop() reads a property with the same getter and storage behavior as prop(). A non-missing scalar character name is a caller precondition.
  • C and C++ packages include S7-init.h in their initialization source file, call S7_init_api() during DLL initialization, and include S7.h elsewhere. Calls use shared function pointers without per-call initialization checks.
  • Rust and other bindings can retrieve and cache the registered pointers with R_GetCCallable().

The registered entry points use the existing C implementations directly. Documentation describes the two function contracts and includes a complete example of initialization and property access. A compiled consumer fixture exercises C, C++, direct registered lookup, and repeated header inclusion. The benchmark compares native access with evaluating cached and namespace-qualified R calls.

Local results from bench/native-api.R, using R 4.6.1 on an Apple M4 Max with clang -O2:

Operation Cached R call via Rf_eval() Native API Native vs cached R
Class lookup 0.64 µs 0.021 µs 31x faster
Stored property 0.26 µs 0.063 µs 4.1x faster
Simple getter 1.2 µs 0.97 µs 1.2x faster

Times are per access: the median of session medians from three fresh R sessions, with 1,000 accesses per C loop. R functions and complete call objects are cached before timing, and all paths repeatedly access the same object. Parsing and call construction are excluded.

Evaluating a namespace-qualified S7::prop() call takes 0.32 µs for a stored property versus 0.063 µs through the native API (5.1x faster). Getters still execute R code. Class lookup also avoids the general classification logic in R's S7_class().

Closes #788.

Register S7_get_class and S7_get_prop for native consumers, with a public
C/C++ header and documented signatures for other bindings. Reuse S7's
class lookup and property getters so consumers do not depend on storage
details.

Add compiled consumer tests and document the API contract.

Closes #788
Provide shared function pointers initialized once by S7_init_api() in the
consumer's DLL initialization routine. Register the existing accessors
directly and remove the forwarding wrappers and per-call name validation.

Document scalar, non-missing property names as a caller precondition.
Update the compiled consumer tests to initialize the API once and use it
from both C and C++.
Compare class lookup, stored properties, and a custom getter through the
native API and through cached R calls evaluated from C. Batch 1000 accesses
per timing sample, verify returned values, and save timings and build
metadata for comparisons across fresh R sessions.
@t-kalinowski
t-kalinowski marked this pull request as ready for review October 20, 2026 18:23
@t-kalinowski
t-kalinowski requested a review from hadley October 21, 2026 16:36

@hadley hadley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really have any strong feelings here, but I added a few questions as they occurred to me

Comment thread R/native-api.R
Comment on lines +3 to +4
#' S7 provides a stable native API for retrieving class definitions and reading
#' properties without depending on how S7 stores them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' S7 provides a stable native API for retrieving class definitions and reading
#' properties without depending on how S7 stores them.
#' S7 provides a stable native API for retrieving class definitions and reading
#' properties.

Comment thread R/native-api.R
#' S7 provides a stable native API for retrieving class definitions and reading
#' properties without depending on how S7 stores them.
#'
#' * `SEXP S7_get_class(SEXP object)` returns the stored S7 class definition, or

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_class() or class_get()? Is one or the other more C like?

Comment thread R/native-api.R
Comment on lines +23 to +28
#' static SEXP object_value(SEXP object) {
#' SEXP name = PROTECT(Rf_mkString("value"));
#' SEXP value = S7_get_prop(object, name);
#' UNPROTECT(1);
#' return value;
#' }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make the example clearer to put this in a separate file, like you would typically?

local_dev_S7_lib(frame = frame)
if (is.null(lib)) {
dir.create(lib <<- tempfile("S7-native-api-"))
install.packages(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make the tests substantially slower?

local_native_api <- local({
lib <- NULL
function(frame = parent.frame()) {
local_dev_S7_lib(frame = frame)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we skip_on_cran() here?

This branch has not been deployed

No deployments
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 this pull request may close these issues.

Consider exposing a stable native API for S7 interoperability

2 participants