[ORC JIT] -Resolving cross references in a multi-process scenario

Hello LLVM-Mailing-List and Lang,

I have a very weird (or strict?) scenario and was curious if the ORC JIT can help me with it. Soo here it comes:

I have currently a windows process (“Runtime”) which does nothing but creating a shared memory with read, write and execute rights - and also writing some function addresses like from printf to it.

Then I have two or more processes which are using the ORC JIT to load modules to this shared memory, in “perspective” of the process I mentioned above (“Runtime”). The sections are remapped from the other processes to be correct for “Runtime”, also they resolve the undefined references for “Runtime” - like for printf and so on.

This works quite well so far! Wuhuhu x3

However, there is one issue about it. Given that those two (or more processes) are now loading modules that reference each other. Like Module A is using a function of Module B - but Module B also uses a function of Module A. How could I resolve those modules when they are loaded from different processes? Normally I would use llvm-link to link those modules but in my current scenario this is sadly not possible.

Can the ORC JIT help me with that? Can I solve this problem differently? Like replacing all the functions of Module A that rely on Module B with function pointers? Is something like that possible?

I know this situation is pretty uncommon but sadly I ran into that issue.

Kind greetings

Björn

Hello LLVM-Mailing-List and Lang,

I played around with this problem a bit and found a way of doing this – however I’m not sure about the consequences and if this is really a good idea.

I went trough all of the global symbols and functions being defined in the llvm::Module and set for each of them a new section name (based on there name). With this approach the memorymanager could tell me already the addresses of my symbols, which I could then share with the other process. However, this approach needed to load the module two times, first to get the future address and in a second run to use the addresses to resolve the symbols.

The only disadvantage I noticed so far was that the overall size of the emitted code was bigger then having the regular sections. Are there any other risks I might have overseen?

Kind greetings

Björn

Hi Bjoern,

Thanks for your patience. The good news is that there is a neater way to do this: ExecutionSession’s lookup methods take a orc::SymbolState parameter which describes the state that symbols must reach before a query can return (See https://github.com/llvm/llvm-project/blob/d1a7bfca74365c6523e647fcedd84f8fa1972dd3/llvm/include/llvm/ExecutionEngine/Orc/Core.h#L1273).In.In) your case you want to use orc::SymbolState::Resolved, which will cause the query to return as soon as the searched-for symbols have been assigned an address.

So if your process is generating a set of symbols that may need to be transmitted to other sessions then you would write something like:

auto Syms = ES.lookup(SearchOrder, SymbolsToTransmit, LookupKind::Static, SymbolState::Resolved);
if (!Syms)
reportError(Syms.takeError());
sendSymbolsToRemote(*Syms);

Out of interest – Is it necessary for your JIT itself to be split across two or more processes? I had always anticipated having a single ExecutionSession attached to the target process and then running just the compilers on other processes. That gives you a star-like IPC network with N + 1 connections: One between the execution session and each of the N compilers, and one between the execution session and the target processs. In your model it sounds like the risk is that you may end up with N(N + 1) IPC links with every execution session having to communicate with every other and also with the target process.

– Lang.

Hey Lang,

Thanks for your patience.

I’m happy you consider me patient x3

Out of interest – Is it necessary for your JIT itself to be split across two or more processes?

In general I agree with your approach, but sadly - the current software design does not allow this yet. So I try understanding how to tackle both solutions - one process managing all the modules vs multiple processes manging just there modules. I would prefer just having one process doing it, because it makes way more sense.

However… About your solution. This looks really promising – Thank you! - but sadly, I’m not experienced enough with ORC yet to understand how to use it.

I create a llvm::orc::LLJIT and then add my llvm::Module to it.

Can I use the getExecutionSession function of LLJIT to get access to this special lookup function after I added the module?

Also I have some difficulties with providing the parameters .w. For example… I don’t have a “JITDylibSearchList” nor “SymbolNameSet” (I probably have it hidden somewhere). I do know those from when I do the symbol resolving. I didn’t created a JITDylib for it, instead I attached a generator function to the MainJITDylib – because I have to lookup symbols from the shared memory and that list changes.

So I’m not sure how to use it yet - sorry for the noobie question >o<

Kind greetings

Björn

Hi Bjoern,

However… About your solution. This looks really promising – Thank you! - but sadly, I’m not experienced enough with ORC yet to understand how to use it.

I create a llvm::orc::LLJIT and then add my llvm::Module to it.
Can I use the getExecutionSession function of LLJIT to get access to this special lookup function after I added the module?

Yes. :slight_smile:

Also I have some difficulties with providing the parameters .w. For example… I don’t have a “JITDylibSearchList” nor “SymbolNameSet” (I probably have it hidden somewhere). I do know those from when I do the symbol resolving. I didn’t created a JITDylib for it, instead I attached a generator function to the MainJITDylib – because I have to lookup symbols from the shared memory and that list changes.

A JITDylib search list is a vector of (JITDylib*, JITDylibLookupFlags) pairs, where the flags indicate whether non-exported symbols should be visible to the lookup (the default is to match against exported-symbols only). A SymbolLookupSet defines the set of symbols to look up as a vector of (SymbolStringPtr, SymbolLookupFlags) pairs, where the flags tell you whether the symbol must be present (SymbolLookupFlags::RequiredSymbol, the default), or is allowed to be missing without generating an error (SymbolLookupFlags::WeaklyReferencedSymbol). In your case we can just use the defaults. Your code will look like:

// Get symbol names to look up. I’m assuming below that they’re

// already mangled and just need to be interned. If they have

// not been mangled yet you will need to do that.

std::vector SymbolNames = ;

auto &ES = J->getExecutionSession();

SymbolLookupSet LookupSet;

for (StringRef Name : SymbolsNames)

LookupSet.add(ES.intern(Name));

auto Result = ES.lookup(

{ { &J->getMainJITDylib(),

JITDylibLookupFlags::MatchExportedSymbolsOnly } },

LookupSet, LookupKind::Static, SymbolState::Resolved);

Result will now contain a map of linker-mangled symbol names (as SymbolStringPtrs) to their resolved addresses.

I was going to say that you just need to send this mapping to the remote, but then I realized that that’s not sufficient: You will have addresses for the symbols now, but you won’t know if they’re safe to access yet.

How are you solving this problem in your memory-manager based solution? E.g. What if you have a cycle of dependencies: A ← B ← C ← A, with A, B and C all being provided by different processes? How do you know when any given symbol is safe?

Orc has to deal with the same problem internally and handles it by maintaining a dependence graph between symbols. We could re-use this infrastructure by reflecting symbols and symbol-states between ExecutionSessions, but this would not be trivial. If possible, this is a very strong motivation for moving to a single ExecutionSession. :slight_smile:

So I’m not sure how to use it yet - sorry for the noobie question >o<

No worries at all.

Cheers,

Lang.

Hey Lang and LLVM-Mailinglist,

I hoped I was through with the questions but… I have difficulties trying out the code example you shared (message below). First of all, I was not able to use your example because there is no LookupKind::Static. I guess this is because those things are not in the LLVM9 source code?

So I tried this instead:

bool test(const std::vectorstd::string &symbols)

{

auto &ES = jit->getExecutionSession();

llvm::orc::SymbolNameSet LookupSet;

for(const auto &Name : symbols)

{

LookupSet.insert(ES.intern(Name));

}

auto result = ES.lookup({{&jit->getMainJITDylib(), true}}, LookupSet, llvm::orc::SymbolState::Resolved);

if(!result)

{

return false;

}

for(const auto &bla : *result)

{

printf(“Miau: %s\n”, (*(bla.first)).str().c_str());

}

return true;

}

However running this will result in “result” having an error – but I get no message what went wrong. I added a llvm::Module to my jit with addIRModule and well… that’s it basically. Is this because I don’t have the latest changes?

The string vector contains decorated names, I got them from asking the IRModule about its functions and choosing some for testing.

Kind greetings

Björn

Another hello to everyone,

I found the issue by now – it was… a simple misunderstanding you could say.

When I add names to the list of functions that are actually defined in the module, then the function will succeed. However, if I add names of functions that are only declared like “puts”, then the function will fail. This is not exactly what I expected to be honest. I thought, that in such a situation the function would still succeed and that the resulting list would either not contain “puts” or with an 0 address.

I wanted to use this function to identify which symbols of my list are defined and which not…

Kind greetings and sorry for double posting,

Björn