Skip to content

objects returned from c++ can be wrapped as another library's module class with the same name #1531

Closed

Description

@kevinushey

When C++ code returns an object of a module class to R (by value, or as a T*), wrap() goes through make_new_object<Class>(), which asks R to build the reference object by calling cpp_object_maker(typeid(Class).name(), xp). That looks the class up in a single, process-wide map keyed by the typeid name:

Rcpp/R/Module.R

Lines 159 to 162 in 2cf7685

cpp_object_maker <- function(typeid, pointer){
Class <- .classes_map[[ typeid ]]
new( Class, .object_pointer = pointer )
}

cpp_object_maker <- function(typeid, pointer){
    Class <- .classes_map[[ typeid ]]
    new( Class, .object_pointer = pointer )
}

.classes_map[[typeid]] holds whichever module class with that typeid name was loaded last, from any package or sourceCpp() file. Unrelated C++ classes with the same (unqualified) name have the same typeid name in every shared library, e.g. Foo is 3Foo. Generic names like Model, Graph, Stack or Node are not unusual in module code. So an object returned by one library can be wrapped as the other library's class, and its methods then run the other library's code on it.

There is a second, name-based lookup in the same function: new(Class, ...) resolves the reference class by its R name (Rcpp_Foo), and two modules that both define Rcpp_Foo in the same environment (e.g. the global environment, for sourceCpp()) overwrite each other's class definition.

Reproducible example

a.cpp:

#include <Rcpp.h>

using namespace Rcpp;

// a.cpp's Foo
class Foo {
public:
    Foo(double x_) : x(x_) {}
    double get() { return x; }
    double x;
};

RCPP_EXPOSED_CLASS_NODECL(Foo)

Foo make_foo(double x) {
    return Foo(x);
}

RCPP_MODULE(module_a) {
    class_<Foo>("Foo")
        .constructor<double>()
        .method("get", &Foo::get);

    function("make_foo", &make_foo);
}

b.cpp:

#include <Rcpp.h>

using namespace Rcpp;

// b.cpp's Foo: an unrelated class that happens to have the same name
class Foo {
public:
    Foo() : values(3, 1.0) {}
    std::vector<double> get() { return values; }
    std::vector<double> values;
};

RCPP_MODULE(module_b) {
    class_<Foo>("Foo")
        .constructor()
        .method("get", &Foo::get);
}

repro.R:

a <- new.env()
b <- new.env()
Rcpp::sourceCpp("a.cpp", env = a)
Rcpp::sourceCpp("b.cpp", env = b)

# a Foo from a.cpp, returned by value
foo <- a$make_foo(42)
cat("class:", class(foo), "\n")
print(foo$get())  # expected: 42

Output with R 4.6.1 (macOS 27, Apple clang 21, Rcpp 1.1.2.6 / current master):

class: Rcpp_Foo
Error: vector
Execution halted

foo is a.cpp's Foo, but it was wrapped as b.cpp's class, so foo$get() ran b.cpp's Foo::get() on it. That read the double member as a std::vector<double>, and copying the garbage vector threw std::length_error. With other layouts the same mix-up silently returns garbage or corrupts memory. When b.cpp's get() doesn't touch any members, it just returns b.cpp's result ([1] 1 1 1).

I'll follow up with a PR that lets make_new_object<Class>() tell R which module class to use: class_<Class> records the class it registers in a per-library slot (a hidden-visibility function-local static, so libraries don't share it), and make_new_object() passes that class pointer along, so R can pick the class registered from the same library and build the object through that class's own generator. Without such a record (e.g. a library that returns a Foo it doesn't expose itself, or a package built against older headers) the lookup falls back to the typeid name as today.

Activity

  1. kevinushey commented on Oct 29, 2026

    @kevinushey
    ContributorAuthor

    While working on the fix I found that the sourceCpp() reprex above mixes two separate problems, and only one of them is in the C++ side:

    1. typeid collision (C++ side). make_new_object() only passes the typeid name, so R cannot tell which module class to use. This is what the follow-up PR fixes.
    2. Reference class name collision (R side). Module() defines the R class Rcpp_<name> in where. For sourceCpp() that is always .GlobalEnv, so a second file exposing a class with the same name replaces the first file's reference class there. methods' initialize then resolves class(.Object) by name to the surviving definition, so a.cpp's class can't be used at all after b.cpp is loaded, whichever map entry R picks. Across packages, each namespace keeps its own Rcpp_<name>, but new(pkgB::Foo) still resolves the class by its bare name through the methods class cache (Found more than one class "Rcpp_Foo" in cache; using the first, from namespace 'pkgA') and fails with C++ object not initialized.

    So the realistic case for (1) is two packages exposing unrelated classes with the same name. There, on current master, objects returned from C++ by one of the packages are wrapped as the other package's class. With two packages collisionA/collisionB built from the two files above (module renamed to collision_module, loaded with loadModule("collision_module", TRUE), and a make_foo() added to collisionB that returns its Foo by value):

    library(collisionA)
    library(collisionB)
    collisionA::make_foo(42)$get()  # 42
    collisionB::make_foo()$get()    # expected: 1 1 1

    Output on master (R 4.6.1, macOS):

    [1] 42
    [1] 2.646569e-312
    

    collisionB's object was wrapped as collisionA's class, so get() read its std::vector as a double. (collisionA's object only comes out right because the class cache happens to resolve Rcpp_Foo to collisionA, the first one loaded.)

    The PR fixes (1): both calls return the right result. (2) is a deeper issue with how module classes are named and looked up in R, and I'll leave it out of this fix. Fixing it would mean making Rcpp_<name> unique per module, or no longer going through name-based lookup in new().

  2. eddelbuettel commented on Nov 4, 2026

    @eddelbuettel
    Member

    This has been taken care of via #1532.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions