TruffleC: A C implementation on top of JVM (2014)
dl.acm.org
dl.acm.org
OpenTV provided such a 'middleware' where the main language was C on such a VM, and was (possibly still is) widely used.
I am possibly conflating JVM with Java, however I was under the impression they was designed as one to begin with.
Set top boxes were a very early use case. It coincided with the boom of these types of systems that were growing past the traditional “embedded” boundaries. Gosling and the team were working on that when it was still Oak and hadn’t been branded.
There’s an amazing video of him telling the story. I think it’s on YouTube.
EDIT: I say crappier due to security flaws that were present with JVM and Applets early on
Website, source repo, and paper seem to still be available, try archive.org for dead links:
The code is available at https://github.com/oracle/graal/tree/master/sulong
Is it still interpreter only, or will we have C1 and C2 JIT support as well?
With projects like loom, valhalla, graalVM, JVM/Java is everything a modern language/runtime needs, plus a lot lot more.
I frankly believe the only other commercially viable language that has similar philosophy to JVM development, is Rust.
Opinions?
Perhaps you meant to say WASM, which is indeed very similar to the JVM in goals and philosophy.
However, remember that newer developers get onboarded by older developers. I lead the charge a few years ago to get my then team to adopt Java8 style streams and Vertx, otherwise the stack was the typical SpringBoot annotation affair with the downsides you mention.
In a similar vein, one of Java's touted strengths is the package ecosystem but those packages often are written in the same class-hierarchy-heavy style. When the code you see and use is written in that style, writing in that style becomes the default unless you actively choose to do something else.
Which I don't understand at all. Modern Java is far more expressive than Go which everyone seems to love. Streams, switch expressions, records, pattern matching, and variable type inference all make Java an extremely expressive and fun language to write while also maintaining readability for when you come back to the code later.
As for the hatred to Java, some of it is due to experience with "old Java" which is then negatively compared to some "new X" (rather than comparing "new X" to "new Java"). Some just comes from popularity. In 20222 Java is the dominant server-side language, with no other language coming remotely close in that domain. In 2002 Java was also the most popular server-side language. Very few languages have ever achieved such success for such a long time (e.g. COBOL and PHP never did), and I believe the list includes just C, Java, JavaScript, and, to a lesser extent, Python. Of those four, only C, Java, and JavaScript are commonly/mostly used in large projects, and people often hate large codebases. Those three languages receive roughly equal amounts of hate. C++, which isn't as popular but is also mostly used in large projects, also receives a lot of hate.
I think people believe that some language X easily fixes some flaw they see in Java, but so far it's come at the cost of other shortcomings they don't see, which explains why X never becomes as popular -- which makes those people disappointed -- which, in turn, means that it rarely has big, old codebases, and so is never hated but just fades to remain fondly-remembered, or lingers as a niche language (although sometimes a not very small niche). This is somewhat like the Betamax vs. VHS debate. A relatively small group of ardent fans liked Betamax because it was superior in a metric they cared about, but was inferior in metrics other people -- a larger group than the first -- cared about.
In short, I think it's a combination of unfamiliarity with "new Java", the language's extended popularity, and its use in large codebases.
Also .NET is not as popular, and its adoption is also a little weird. It's quite popular overall, but doesn't dominate any domain. It's used on the server but not nearly as much as Java; it's used on the client, but not nearly as much as JavaScript. I think it's still largely confined to Microsoft shops.
So both in longevity and popularity it's not quite in the same class as those others.
Regarding defining what Java is, what about Android Java, real time Java, Java on mainframes, forks like microEJ, Graal, OpenJ9, Azul....
Just as hard to define.
On a .NET/Java shop and we have like 50% of both stacks in every, single, project.
Nowadays I tend to spend most of my time on the .NET side, because I get to enjoy Valhalla, Vectors, async/await, TPL, since years now, instead of waiting for what might never come.
I get to be on both sides of the coin, which would be advisable to the Java team to better know the competition.
As for competition, we follow what's going on in .NET, JavaScript, Python, Go, and even far less popular languages such as Haskell and Erlang. But regardless of your personal preferences -- some share them while for others Java's superiority in GC, monitoring, compilation, backward compatibility, and language simplicity matter more -- C#/.NET is not yet in the same club as C, Python, JS, and Java in terms of longevity and popularity, and that's what I'm talking about here.
C# is more than 20 years old, it runs on everything and everywhere. It has multiple toolchains and IDEs
Rust is rather unrelated. It’s a compiled static language with no runtime, in which you manage memory. The JVM is a thick VM and you basically only get to script it, which is fine and all considering it’s Turing complete, etc, but you really aren’t anywhere near the metal where rust can go bare metal with no OS or stdlib
Funny - because LLVM IR makes the 'when' (the required partial ordering of instructions) implicit and so both too loose and too constrained at the same time, because it's a linear IR, while the JVM's Graal and C2 IRs makes the 'when' completely explicit and a first-class part of the representation, because they're graphical IRs.
int foo(int a, int b, int c, int d) {
return a * b + c * d;
}
LLVM gives you a single total-ordering of all operations in this code, which makes it appear like computing %5 has to happen before %6, but in reality it doesn't - they could be swapped. define dso_local i32 @foo(i32 noundef %0, i32 noundef %1, i32 noundef %2, i32 noundef %3) local_unnamed_addr #0 !dbg !7 {
%5 = mul nsw i32 %1, %0, !dbg !18
%6 = mul nsw i32 %3, %2, !dbg !19
%7 = add nsw i32 %6, %5, !dbg !20
ret i32 %7, !dbg !21
}
Java's IRs instead tell you that %5 and %6 need to be computed before %7, but don't apply an ordering between them otherwise. They can do this because they use a graph of instructions, not a linear list of instructions.There are two kinds of programming languages : ones that people complain about and ones that no one uses.
I have elaborated plenty of times on why Java is a 1980s language that were obsolete as soon as it was released, the latest is my comment on https://news.ycombinator.com/item?id=32128271, which I reproduce in full at the very bottom of this comment to save you a ctrl-f.
>loom, valhalla, graalVM
Every single one of those has nothing to do with Java and everything to do with the JVM as a high-tech psedo-OS that challenges the classic preconceptions about performance-productivity tradeoffs. I don't want to attribute dishonesty to you, but again, I see absolutely no reason to confuse a VM with a (incredibly inferior and badly designed) human-level programming language just because it happens to be the first language to run on the VM.
>frankly believe the only other commercially viable language that has similar philosophy to JVM development, is Rust.
Come again? How is Rust similar or even comparable to the JVM ?
----------
Reproduced Comment
----------
>>>>I'm a Java hater. Here are the reason I hate it for
- Baking the difference between primitives and objects into the language itself : an ugly mistake with far reaching consequences, made by a language designed in 1995 while another designed in 1980 (smalltalk), in 1991 (Python) and 1995 (Ruby) all didn't fall for it.
The difference is an irrelevant VM-level optimization detail, there is no reason to uglify the human-level language with it. Once the initial mistake has been made, the correct response was NOT to make the even uglier hack of wrapper classes, but to make the primitives objects in the newer releases of the language, this won't break old code, as valid uses of objects are a superset of valid uses of primitives, except perhaps that objects need to be allocated explictely with "new", but this can be a special case for primtives (i.e. "int is a special kind of object that you don't need to allocate explicitly"). The compiler can figure out whether it needs to be represented as objects or as primitives, you can leave hooks and knobs for people to tell the compiler they need to the primitives to be represented as primitves, but it shouldn't be mandatory.
- Baking in choices about object representations : Like the fact that objects are always passed by reference, or that they are always allocated on the heap. Why the "always" part ? why not give developers the choice between pass-by-value and pass-by-reference like C# does ? why not give developers the choice to allocate on the stack (and complain as loud as you want when they want to do something unsafe with it, like escaping from methods), which, unfortunately, even C# doesn't ?
Everytime you see something like "foo deepCopy()" that's a failure of the language, forcing you to explicitely pay attention to the fact that foo objects need to be copied deeply everytime they are copied, instead of just once when you define the object by marking it as a "struct" or whatever word to signify that object has value semantics, and then deep copy is just assignment or passing as a parameter. Why make it the default to be inefficient with the heap when it's very easy to give developers the choice to be efficient in situations where it's always safe ?
- No operator overloading : I get the hate, it's a powerful tool. But it's misguided to ban it, operators should not be special, languages like Haskell and Raku go even further and allow you to define new operators entirely and control their predence and other things. You don't need to go that far, why can't objects use the already built-in symbols the language support ? because it might be confusing ? anything can be confusing, you can write assembly in any programming language, and it will be even worse than assembly because of the more powerful and obscure abstractions.
- Generics : The overall theme of forcing you to do things its way seems to a staple with java. Why do I need to use type-erased generics ? why shouldn't I get the choice to specify whether I need a new class generated for runtime efficiency or use the type-erased catch-all for size efficiency? there is no need to bake VM-level support for this, it can all be done at compile time (possibly with help of additional metadata files or special fields in the .class of the generic type).
- Overall verbosity : Why "extends" and "implements" ? do you really need to know whether you're inheriting a class or an interface ? and can't those be lighter symbols like "<" and ":" perhaps ? why is "private/public/protected" a must in front of every method and field ? most people align fields and methods by their visibility, C++'s way is that you declare "public:" and then everything declared below that is public. In the worst case you can always recover Java's way by "public : <method> ; private : <method> ; public : <method>" and so on, but it's nice to at least have the choice of not repeating yourself.
Why aren't any constructors generated ? there are at least 2 very obvious ones : the empty one, and the one that assigns all the non-defaulted fields (and can take optional arguments to override the default fields). Why aren't generated getters and setters available with a small and light request, like C#'s "get ; set ;" ? Java's design is just full of things like this. It feels like a weird sort of disrespect for your time, "yeah you must write those routine 25 lines of code all by yourself, you have anything better to do?", how about actually writing my application instead of pleasing your language with weird and unnecessary incantations ? It's like a modern COBOL.
- Horrible OOP excesses : Not really the language's fault (except that it encourages verbosity and loves it) and already mentioned, but worth mentioning again.
Overall, I treat java as assembly. I write kotlin in my spare time, and whenever I'm confused about the semantics of some construct I make intellij show the bytecode then hit "decompile" to see a Java rendition of the code, the exact semantics will be obvious but verbose. A language that took this literally is Xtend, a high-level augmented java which transpiles to java and is a strict superset of it, but with option that the Xtend compiler figures out all the verbosity for you. Groovy also takes the "Superset and Augment" approach but doesn't transpile. And off course Kotlin is very good with it's interoperability, every JVM language is but Kotlin's mixture of being close to Java semantics (unlike say, Scala or Clojure) and Intellij excellent support for mixed projects makes it at least somewhat special.
I like the JVM and it's cutting edge research and performance, and these days the Java standard writers seem to show signs of finally waking up to reality after years of being behind every mainstream language, and they regularly augment and modernize the language. But you can't undo 20 years or so of bad design, not easily and not painlessly.
>Indeed, tooling is the new syntax.
Very much agreed, long long gone are the days when a compiler or an interpeter is the only thing expected out of a language. But it's not a panacea to treat any bad design, at best it's just a band-aid for bad designs that makes them barely berable. The language has to be designed from the start with the knowledge of "this is going to run in an IDE" baked in to make full use of the full range of fantastic things an IDE can do.
---------------------
With the initial, interpreter-only operation of the JVM, performance considerations were very imminent. Generics, given the context made a correct choice, and the maintainer team’s vision even back then was quite right, Valhalla seems to be able to heal the rift between primitives and objects. So in this view auto-boxing was again a sane choice.
Regarding stack/heap allocation: I believe the beauty and longevity of Java code is partially thanks to their avoidance of over-specifying language semantics. Sure, C# may have won a few percent better performance by introducing yet another feature, pushing the responsibility onto the developer. So only a select few programs are effected. While Java’s continued improvements to the JIT compiler, GC and escape analysis brought even decades old code bases written for the first versions of Java considerable performance upgrades for free. Of course, value types are still an important performance win left on the table, but it will be solved - yet again, in a not over-specified way. Primitive and value types as per the current view only specify semantics, leaving the allocation strategy up for the JIT compiler to optimize.
Operator overloading is a difficult feature, we have seen plenty of failed attempts and a few where it seems to work fine. I also think that partial operator overloading (a la Rust, adding an Add, Multiply, etc interfaces with single methods corresponding to + and *) could be useful, but left uncontrolled it can be a catastrophe ( a <<! b, what does it mean?). Nonetheless, it is not that big of a pain point, using BigInteger::add 3 times from time to time is more than fine.
Generics were executed very elegantly in my opinion given the constraints. It has some edge cases, but reification is overblown as a problem and overloading may come once primitives and objects are unified.
Verbosity: come on, seriously? I didn’t benchmark it, but I am fairly sure I actually press less keys in a good IDE writing Java than writing the equivalent in a “more concise” programming language , while the end result will be that much more readable. Like, honestly you believe that programming takes long time because you have to write class A extends B instead of class A <: B? Like, you spend multiple orders of magnitude more time over a single line thinking. Writing is never the bottleneck.
The fact that an experienced developer has to transcompile Kotlin to Java to understand some parts of it also doesn't seem like a plus to me.
Because object allocation and the use of an allocated object in a synchronized part can be far away from each other both in time and in source code (quite possibly even in different jars), I would think that creates object overhead that’s hard to optimize away.
What would Java have lost if it required a synchronizable keyword on class definitions to allow callers to synchronize on instances of a class?
Not really, .NET ecosystem is the only match to Java in tooling.
Hence why during the last 20 years I enjoy both platforms.
- it’s a heavyweight blob that you have to ship with your binary. It can be many times the size of the thing I’m trying to run.
- it’s a word salad of technologies that apparently I’m supposed to care about
- many of the claims of how great it is are really just ongoing claims of how great it’s going to be.
To the latter point, value types (discussed in this thread) have been discussed since I was still using Java - a quick Google shows results from 2014. I haven’t written Java code commercially since 2018. I can’t find anything suggesting it’s here yet.
Keeping track of all these technologies and trying to understand when they will arrive and make my day to day life better was a nightmare.
Compared to that, I can build a Go binary, scp it to some machine and run it. That’s not possible in the JVM world, at least not without a level of calisthenics I’m simply no longer willing to deal with.
GraalVM and friends may be wonderful technologies, but honestly I don’t care. I don’t want to have to thread the needle of this ecosystem every time I have work to get done.
For instance, what happens when you call a function-pointer?
Using a combination of native memory and JVM managed memory, depending on what the memory is needed for.
> For instance, what happens when you call a function-pointer?
This is a good example - because TruffleC can inline-cache a function-pointer, inlining the called function!
All this is in the linked paper, of course.
https://www.semanticscholar.org/paper/TruffleC%3A-dynamic-ex...
I would hope that there's a much more efficient way to do it, this idea is just evidence that it could be done in principle. But I don't see what that more efficient way would be. You certainly need to keep a secret reference to each JVM object somehow because C doesn't require you to keep any pointer to an object e.g.
intptr_t x = (intptr_t)malloc(sizeof(int));
*(int*)x = 99;
bool did_subtract_50 = false;
if (x > 50) {
did_subtract_50 = true;
x -= 50;
}
// Now there is no pointer or even integer that contains the address
// ... later ...
// Retrieve the address and use and free it
int* y = (int*)(x + 50 * did_subtract_50);
printf("value: %d\n", *y);
free(y);The JVM is a specification which describes a pretend computer and its instruction set.
This TruffleC doesn't translate C to Java and run a Java program. This compiles C to bytecode which operates on the JVM.
Whatever Java does or doesn't support is irrelevant to this compiler. TruffleC has nothing to do with the Java programming language at all.
Just like you can compile C and get a memory address of a stack or heap location on any physical computer supported by a C compiler, likewise you can compile C with TruffleC and get a memory address within the stack or heap of the pretend computer called the JVM.
This must be how it works, unless the JVM itself has no concept of memory addresses, which seems very unlikely to me. Let me know if I am wrong?
No, it compiles C to an AST, which it then interprets. The AST, which is also the interpreter in the Truffle design, are then partially evaluated to produce machine code. No bytecode is generated at any point, and in fact you can run it on a JVM that doesn't use byteocde, and then there is no bytecode anywhere.
I see what you mean though, thanks!
That's what it was to start with. Partial evaluation came later.
Truffle can give these scripting languages a huge boost in performance (TrufflyRuby is 3x faster than the second fastest implementation), but the JVM “doesn’t like to” rely on FFI all that much - and also, truffle is polyglot with the ability to optimize between different languages. So by creating an LLVM interpreter, ruby or python calling into that can be also optimized by e.g. inlining, in certain cases bettering the performance compared to native FFI.
Other than becoming truly cross-platform, running on top of a singular runtime gives it the ability to observe these parts as well (which in itself a huge advantage because the JVM has some killer observability tools), so for example project loom might be applicable to a python script using C libs for IO, putting the whole on a virtual thread and making its blocking calls unblocking magically.