About remote pointers: my question is specifically why can’t we write
Object * some_object = new Object(a, b, c, d);
in C++ where some_object is not an ordinary pointer, but a remote pointer?
I gave you a couple of trouble areas in my private-email response, but I’ll repeat them for the record here too.
Circa 2012 I worked for a startup […] allowing Objective-C objects to live anywhere, even on other machines. Then we used hooks in the Objective-C runtime to intercept messages passed to those objects, marshal them, and transfer them across the wire to where the object lived. We were doing this for the purpose of “Mobile Device Management” — that is, we wanted to allow an employer to provide basically an “iPhone app as a service,” so that the app would run on the employer’s server but all the UIKit objects would live on the employee’s mobile device.
We had two problems with this:
-
First, what does the app do when you go into a tunnel and lose connectivity? You have something that looks to the program like a method call — say, result = object->ExecuteMethod(some, parameters)
— which can return a value, or throw an exception, or abort; but which can also “hang” due to a lost network connection. And if the caller treats this as a TimeoutException, then we have the problem that the callee might unexpectedly “resume” sometime later with a return value that the caller (who has unwound the stack and moved on) is no longer equipped to deal with. ExecuteMethod
is acting spookily like a coroutine, here.
-
Second, how does the marshaller deal with memory, and how does it deal with behaviors? We need to be able to implement qsort
across a wire boundary. That means we need to be able to pass an arbitrarily large chunk of memory (the array to sort), and we also need to be able to pass a function pointer (the comparator). These are both extremely intractable problems. Our startup solved these problems by cheating. You need to solve them for real.
To elaborate on the “behaviors” part: Let’s suppose I have
class Object {
int a, b, c, d;
public:
Object(int a, int b, int c, int d) : a(a), b(b), c(c), d(d) {}
virtual int method() { return a+b+c+d; }
~Object();
};
int test() {
remote_ptr some_object = handwave(new Object(1,2,3,4));
int x = some_object->method();
if (x < 0) throw “oops”;
return x;
}
First of all, if you don’t understand why I wrote remote_ptr<T>
instead of T*
, you’re probably in trouble already, C+±language-wise.
Second, you’re trying to make method
execute on the remote machine, right? How does the remote machine get a copy of the code of Object::method
?
Third, when we hit the throw
and unwind the stack, destructors get called. Object
has a non-virtual destructor. Where does it run: on our machine, or on the remote machine? Presumably it must run on the remote machine, which means our stack-unwind is held up waiting for the result of each destructor we have to run (and those destructors must happen in serial, not in parallel).
Fourth, consider
void helper(Object& o) {
o.Object::method();
}
void nexttest() {
remote_ptr some_object = handwave(new Object(1,2,3,4));
Object onstack(5,6,7,8);
helper(*some_object);
helper(onstack);
}
Any attempt to invent non-trivial “fancy pointers” needs to come with a full-fledged idea of how to invent “fancy references,” or it will not be able to get off the ground in C++. (See P0773R0.) Also, I snuck a non-virtual method call in there; you need a way to handle that.
This problem is easier in Objective-C, where every handle is the same kind of pointer and every method call is virtual by definition. It is very hard in C++. And when I say “very hard,” I’m fairly confident that I mean “impossible.”
Fifth, consider subobjects of remote objects:
struct FifthObject { Object m{1,2,3,4}; };
void fifthtest() {
remote_ptr some_object = handwave(new FifthObject());
helper(some_object->m);
}
Finally, even if you invent a new system for dealing with remote objects, you still have to figure out where the code is going to physically run: on which CPU, which process, which thread… avoiding cache ping-pong(*)… all this real-world-multi-processing kind of stuff. And you must be able to handle it all with zero runtime cost, or else people concerned about performance will just go under you and use those “dead-end” but efficient mechanisms, leaving your neat abstraction without a userbase.
(* — In a domain without global shared memory, the analogue of “cache ping-ponging” would be “marshalling the entire array across a wire boundary every time someone calls qsort
.” We hit this problem, and, as I said, solved it by cheating on the demo.)
–Arthur