-1

One way to construct and destruct C++ objects from Rust is to call the constructor and return an int64_t pointer to Rust. Then, Rust can call methods on the object by passing the int64_t which will be cast to the pointer again.

void do_something(int64_t pointer, char* methodName, ...) {
    //cast pointer and call method here
}

However, this is extremely unsafe. Instead I tend to store the pointer into a map and pass the map key to Rust, so it can call C++ back:

void do_something(int id, char* methodName, ...) {
    //retrieve pointer from id and call method on it
}

Now, imagine I create, from Rust, a C++ object that calls Rust back. I could do the same: give C++ an int64_t and then C++ calls Rust:

#[no_mangle]
pub fn do_somethind(pointer: i64, method_name: &CString, ...) {

}

but that's also insecure. Instead I'd do something similar as C++, using an id:

#[no_mangle]
pub fn do_something(id: u32, method_name: &CString, ...) {
    //search id in map and get object
    //execute method on the object
}

However, this isn't possible, as Rust does not have support for static variables like a map. And Rust's lazy_static is immutable.

The only way to do safe calls from C++ back to Rust is to pass the address of something static (the function do_something) so calling it will always point to something concrete. Passing pointers is insecure as it could stop existing. However there should be a way for this function to maintain a map of created objects and ids.

So, how to safely call Rust object functions from C++? (for a Rust program, not a C++ program)

trent
  • 25,033
  • 7
  • 51
  • 90
PPP
  • 1,279
  • 1
  • 28
  • 71
  • 3
    Your map solution isn't safer than using pointers, even worse if either sides run multiple threads. Also it is much slower (hashing + lookups). – Angelicos Phosphoros Oct 20 '20 at 06:33
  • 2
    Not sure why you would use `int64_t` to store a pointer. In the first place you should use `uintptr_t` to match the pointer size (and `usize` on the Rust side), but also, why convert to integer in the first place? Just use `void *`/`*mut c_void`. – trent Oct 20 '20 at 15:26
  • Serious question: Where did you get the idea that Rust doesn't have support for static variables? You can trivially make immutable and atomic static variables even with just the standard library, and `lazy_static` lets you easily create mutable statics of any type. One of the [top voted Rust questions on SO](/q/27791532/3650362) is about how to do so. I'm just asking because I have seen several people repeat the falsehood that Rust doesn't have statics. If you read this somewhere (in a tutorial, or documentation or something) maybe it can be corrected. – trent Oct 20 '20 at 20:21

1 Answers1

5

Pointers or Handles

Ultimately, this is about object identity: you need to pass something which allows to identify one instance of an object.

The simplest interface is to return a Pointer. It is the most performant interface, albeit requires trust between the parties, and a clear ownership.

When a Pointer is not suitable, the fallback is to use a Handle. This is, for example, typically what kernels do: a file descriptor, in Linux, is just an int.

Handles do not preclude strong typing.

C and Linux are poor examples, here. Just because a Handle is, often, an integral ID does not preclude encapsulating said integer into a strong type.

For example, you could struct FileDescriptor(i32); to represent a file descriptor handed over from Linux.

Handles do not preclude strongly typed functions.

Similarly, just because you have a Handle does not mean that you have a single syscall interface where the name of the function must be passed by ID (or worse string) and an unknown/untyped soup of arguments follow.

You can perfectly, and really should, use strongly typed functions:

int read(FileDescriptor fd, std::byte* buffer, std::size_t size);

Handles are complicated.

Handles are, to a degree, more complicated than pointers.

First of all, handles are meaningless without some repository: 33 has no intrinsic meaning, it is just a key to look-up the real instance.

  • The repository need not be a singleton. It can perfectly be passed along in the function call.
  • The repository should likely be thread-safe and re-entrant.
  • There may be data-races between usage and deletion of a handle.

The latter point is maybe the most surprising, and means that care must be taken when using the repository: accesses to the underlying values must also be thread-safe, and re-entrant.

(Non thread-safe or non re-entrant underlying values leave you open to Undefined Behavior)

Use Pointers.

In general, my recommendation is to use Pointers.

While Handles may feel safer, implementing a correct system is much more complicated than it looks. Furthermore, Handles do not intrinsically solve ownership issues. Instead of Undefined Behavior, you'll get Null Pointer Dangling Handle Exceptions... and have to reinvent the tooling to track them down.

If you cannot solve the ownership issues with Pointers, you are unlikely to solve them with Handles.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722