I am trying to write Nim bindings for Grappa framework, a C++ frameworks for distributed computing. I am at a point where their hello world example works as expected.
Grappa can execute functions in a distributed manner by calling a function on_all_cores with an argument that is a C++ lambda, possibly capturing some values. Grappa takes care of serializing and distributing such a closure, so that it can be executed on various cores.
Everything works fine if we do not capture variables that are local to a single process. The next step is to make an example where the functions that are distributed actually capture some value. To do so, Grappa uses the C++ lambda functions syntax
on_all_cores([x] () {
# do something with x
})
whereby [x] denotes that the variable x should be captured. One can also use [=] to denote that all captured variables should be copied.
Now, in porting this to Nim we are using the fact that Nim takes care to capture the environment for closures. The issue is that Nim gives us a tuple with the raw proc and the captured environment, but crucially the environment is of type pointer. When we compile to C++, we get something like
void onAllCores(TY123456 p) {
Grappa::on_all_cores([=] () {
p.clPrc(p.clEnv);
})
}
where TY123456 is some generated type for a tuple of a proc and a pointer to an environment. C++ copies p as expected, but since all we have is a pointer to an environment, that becomes an invalid pointer on other machines.
What we would like to do is to avoid the indirection for the environment. If we had a concrete type T for the environment, we could generate something like
void onAllCores(TY123456 p) {
auto env = * ( (T*)(p.ClEnv) );
auto prc = p.ClPrc;
Grappa::on_all_cores([=] () {
prc(&env);
})
}
By doing so, we make sure that C++ is able to properly copy the environment, and in fact if we modify the generated C++ sources as above, everything works as expected at runtime.
The problem is that we are not able to write anything generic because we don't know the actual type of the captured environment. Surely the compiler knows it - is there a way to access this information in Nim?
I am trying to do some stuff that serializes closures and sends them to execute on other machines. At first I got stuck sending functions, but it turns out that just sending a closure address (f.rawProc) is easy and works fine (no problems with ASLR, as the closure in inside my own executable and not dynamically linked).
I still have the problem of sending a closure environment, though. If I had some way to recover the compiler-generated type for the environment, I could
Unfortunately, f.rawEnv is just a pointer to void. Is there any way I could get some more information?
Sure:
# import low level RTTI mechanism:
include "system/hti.nim"
type
Cell = object ## duplicated from system/cellsets
refcount: int # the refcount and some flags
typ: PNimType
proc getRtti(env: pointer): PNimType =
let header = cast[ptr Cell](cast[int](env) -% sizeof(Cell))
header.typ
proc getSize(env: pointer): int = getRtti(env).size
Unfortunately, I get lib/system/hti.nim(88, 51) Error: invalid pragma: benign
If I understand correctly, we are relying on ref types being implemented with a header in memory with a reference count and some runtime type information, correct?