3 ms·
Here is the More Than You Ever Wanted To Know answer whilst I wait for a build to go green. Firstly, for the last few years when you talk about this topic you
by mike_hearn 4y ago
Here is the More Than You Ever Wanted To Know answer whilst I wait for a build to go green.
Firstly, for the last few years when you talk about this topic you have to distinguish between the "old world" JVMs which can only be given bytecode as input, and GraalVM, which is a superset of those JVMs and enables language implementations to use the JVM's features whilst bypassing the bytecode layer entirely.
Java byte code has some problems that make it unsuitable for C-like languages. Most obviously you can't do pointer arithmetic or arbitrary casts, which is a fundamental requirement for real C code. This doesn't mean it can't be done, these are all Turing complete machines after all, but it means there's no point because performance would be very poor due to all the workarounds. For C++ the gap gets wider because Java has its own ideas about how objects work that aren't the same as those in C++, e.g. multiple inheritance, so you can't implement C++ classes as Java classes.
These sorts of problems affect any language that is semantically too far away from Java, like scripting languages. JVM bytecode is fairly well designed, is reasonably general, and the invokedynamic bytecode was put in there to make scripting languages easier. But it's a high level bytecode so the semantic mismatch is still there.
For a long time this problem seemed inherent to the design space. Any VM bytecode language you can design will end up encoding some assumptions about language design into it, if it doesn't then you've just got assembly language and we already have those. In effect, trying to create a universal bytecode is like trying to create a universal programming language.
But the JVM guys didn't give up. Sun/Oracle Labs spent many years on research and the result is Truffle. Instead of asking people to encode their language into a universal ISA so the JVM can understand it, Truffle is an API. It comes as part of GraalVM. You write an interpreter for your language using this API and compile that to JVM bytecode instead (it doesn't have to be written in Java but it does have to be a language that can produce bytecode). This interpreter is then fused with the code of your target language at runtime and fed through a very advanced optimizing compiler (Graal) which generates machine code for your language. This code is then "installed" into the running JVM using another API called JVMCI, and then has access to all the normal JVM services like the garbage collectors, profiling, deoptimization, observability, standard libraries, OS abstractions, ability to call to/from bytecode world and so on.
With Truffle you don't think about the low level details of all that. You just write an interpreter. You do have the learn the API - your generated machine code will be relatively slow until you start using the API to annotate your interpreter and optimize it for common cases. But the API is pretty comprehensive and offers a lot of functionality, like language interop, debugging, tracing, hot swapping ...
The first languages implemented with Truffle were scripting languages. But the technique is general. It's not scripting or JVM specific and there's no specific reason the language your interpreter reads has to be textual. So they implemented an interpreter for LLVM bitcode. Now you can compile C/C++/FORTRAN/Rust using LLVM, and then execute that on the JVM. Performance is good, not quite as fast as natively compiled with GCC but in the general area. It's a fairly specialized thing to do and today is mostly used for running Python/Ruby extensions. However, because the whole thing is virtualized you can do some mind-bending tricks with it, for example, you can eliminate all the memory errors in the software run this way and then sandbox it without using kernel sandboxing. So it has some potentially big security benefits.
And that's how you can run C or any other language on the JVM without killing performance.
- an-unknown 4y ago> But the technique is general. It's not scripting or JVM specific and there's no specific reason the language your interpreter reads has to be textual. […] And that's how you can run C or any other language on the JVM without killing performance. That's not entirely true though, because the Graal compiler has certain assumptions about your language (e.g. you need some form of "methods" and "calls", otherwise Graal won't compile anything), which means it is not exactly straight forward or efficient to run let's say x86 machine code on the GraalVM. It's also not easy to properly support an "mmap" call in an efficient way although it is required by virtually every compiled program that uses glibc, because none of the "scripting or JVM languages" need this and therefore there are no facilities in Truffle to support this. That being said, a fully sandboxed x86_64 emulator for the GraalVM exists and IIRC it is roughly as fast as the JIT based qemu-x86_64 after warmup. Sulong (the LLVM bitcode interpreter of GraalVM) is also not ideal, because it directly executes the LLVM IR although some C code assumes that it is compiled to machine code. This means some libraries use e.g. inline assembly, or try to do some stack walking, or use entire subroutines implemented in assembly, or rely on certain POSIX behavior, which isn't (fully) implemented in Sulong. A lot of programs work, but if something doesn't work, chances are it's caused by one of the few fundamental flaws of Sulong. Oracle is experimenting with a workaround by not running everything in LLVM IR in the future, but we'll have to wait and see how good this works in the end.
- mike_hearn 4y agoHow do you mean, support mmap efficiently? Do you mean being able to close the mapping without a GC? If so then Panama is fixing that. I agree it's not designed to JIT machine code. To sandbox that it may be better to use a flyweight CPU-level VM anyway. Yes, C can do anything and if it does stuff like trying to disassemble itself, then that will clearly fail. But then you could argue it's not really written in C.
- an-unknown 4y ago> How do you mean, support mmap efficiently? Do you mean being able to close the mapping without a GC? If so then Panama is fixing that. Everything relevant that can be done with Panama in this context can already be done in a Truffle language with sun.misc.Unsafe and e.g. Sulong used it for exactly this purpose. In fact the Unsafe allowed a lot more with a much simpler API because you really get a function for raw memory access to arbitrary addresses. But what's the problem anyway? For any normal compiled program, the dynamic linker will mmap the code and data into memory during startup. And a standard memory allocator in the libc like what's used by malloc also uses mmap (and sbrk) internally. Some C programs also directly use mmap to map files into memory or to reserve large amounts of memory, potentially at fixed addresses and with custom protection bits. All of this requires a proper implementation of mmap in the VM if you want to run such programs, in a way that accesses to unmapped or protected memory can be caught without crashing the VM. Side note: Panama does not provide this. The problem here is that the emulated address space only contains a few mapped regions and a lot of unmapped space in between, so you have to come up with a good way how to implement this. You could implement the emulated memory purely in Java, but it is quite slow because you essentially recreate an MMU. For performance reasons you really have to use the hardware MMU in a smart way. It can be done in the GraalVM (and was done in the GraalVM based x86_64 interpreter [1]), but it's not obvious how to do it and it's not particularly efficient either, at least if you want to catch segfaults properly. To efficiently catch segfaults, changes to at least HotSpot would be necessary. What's even worse here is that it's perfectly valid for a program to register a segfault handler, then cause a segfault and catch it. A few real world programs do exactly this, including the JVM itself. You might ignore such custom signal handling from the guest program, but you definitely have to avoid VM crashes caused by such signals. And again, Panama cannot do it. > Yes, C can do anything and if it does stuff like trying to disassemble itself, then that will clearly fail. But then you could argue it's not really written in C. Sure, C programs can do anything, but the problem is that on e.g. a Linux/x86_64 system many things are allowed, certain low level hacks are necessary for performance reasons, and therefore many real world Linux/x86_64 programs do weird things internally, even if it's hidden within some library where you'll never see it. If you want to run an "average" program, you'll have to handle many such cases in your VM. Otherwise you end up with a toy VM which can run a lot of toy programs but fails at larger "real world" programs. You can do what Sulong does and say "I don't care, I'll just pass malloc/free/mmap/... calls directly to the OS", but then you run into various problems, like e.g. you'll be unable to properly sandbox memory = the guest program can easily crash the VM. You can also do what Sulong in GraalVM Enterprise does and say "we don't support certain features like mmap", but a lot of interesting real world programs won't run. Or you can do what the x86_64 interpreter does and properly (although with reduced performance) emulate all these features, but then you end up building a Java implementation of e.g. the Linux kernel. In case you wonder, the x86_64 interpreter I mentioned started as a tech demo to show that you can in fact emulate x86_64 with a limited Linux userspace in a fully sandboxed and cross-platform way and with somewhat decent performance on the GraalVM. It even supported Truffle interop with standard Linux .so libraries in the past. Of course it also showed some limitations of the Graal compiler and Truffle, after all that was the entire point of the project. Don't expect Sulong-like peak performance, it's much slower than that. One of the more interesting findings was that machine code emulation with Graal is feasible and works even for larger real world programs like GCC or xpdf or CPython and peak performance can (or at least could at some point in the past) somewhat compete with qemu which uses a hand crafted JIT compiler. This was especially interesting since machine code is the worst imaginable "language" for Graal and Graal is absolutely not built for this. [1] https://github.com/pekd/tracer/tree/master/vmx86 https://github.com/pekd/tracer/tree/master/vmx86