I understand the project started as a hobby, and anyone who wishes to be involved has their own prerogative, but I'd have a hard time getting behind it myself.
I understand the project started as a hobby, and anyone who wishes to be involved has their own prerogative, but I'd have a hard time getting behind it myself.
Why build a new browser in C++ when safer and more modern languages are available?
Ladybird started as a component of the SerenityOS hobby project, which only allows C++. The choice of language was not so much a technical decision, but more one of personal convenience. Andreas was most comfortable with C++ when creating SerenityOS, and now we have almost half a million lines of modern C++ to maintain.
However, now that Ladybird has forked and become its own independent project, all constraints previously imposed by SerenityOS are no longer in effect. We are actively evaluating a number of alternatives and will be adding a mature successor language to the project in the near future. This process is already quite far along, and prototypes exist in multiple languages.
The languages that are memory safe either impose too much of a straightjacket to be productive (Rust), don’t give you a GC or a memory safe way of writing one (Swift, Rust), or give you a GC with the wrong semantics (Java, Go, etc). Yes, the browser needs to have a GC. No, there’s no way around that.
I’m not saying it can’t be done - but there are good reasons to stick with C++ to get this job done. Not an ideal situation but it’s where the world is at.
The JS engine - about 1/3 of the browser engine, ish - is tightly coupled to the GC through-and-through. Let's even assume you have a JS engine that doesn't JIT. (If it did JIT then rewriting it in a memory safe language isn't going to do much for you at all.) The interpreter, the runtime, and the libraries are going to have to be making decisions on behalf of the GC (for stuff like weak maps, at least) and is going to have to play along with the GC's tracing (every class in the JS engine participates in the JS GC and so must tell the GC how to mark - or worse, move - outgoing references).
The rest of the browser has to play along as well, just not as much. The DOM is basically JS's "standard library" within the browser and it also needs to have its objects play along with the GC's semantics.
So, maybe there is code out there where the GC can be an island. The browser is not that.
And yes, you can make that happen. Marking references for a GC is in the same ballpark as a custom allocator. It doesn't have to fill the code using it with unsafe.
You're going to have to add GC awareness to objects so it can trace through them, but the code that makes and manipulates those objects can have all the necessary rules enforced by types.
GC != RC.
A type system can let you say things like “make sure someone finds out when I assign this pointer”. But that’s not what GC wants, unless (as I said before) you’re happy with your non-GC heap pinning objects (the browser isn’t, but some GC clients are, sometimes). GC wants a guarantee like: you cannot have a pointer in any data structure unless that data structure can be found by GC, and when it is found, make sure to respond to the GC’s request to scan it. That ends up being hard to do with any type system, unless that type system only allows objects to be allocated by that GC and the abstraction sits in the compiler/runtime below the type system (like in Java, Go, and Fil-C).
I am aware.
> But that’s not what GC wants, unless (as I said before) you’re happy with your non-GC heap pinning objects (the browser isn’t, but some GC clients are, sometimes).
I can't figure out exactly what you mean here. Temporary pins on the stack should be fine, and I don't see why anything else would be necessary.
> GC wants a guarantee like: you cannot have a pointer in any data structure unless that data structure can be found by GC, and when it is found, make sure to respond to the GC’s request to scan it. That ends up being hard to do with any type system, unless that type system only allows objects to be allocated by that GC and the abstraction sits in the compiler/runtime below the type system (like in Java, Go, and Fil-C).
Doing heap allocations through the GC doesn't sound very hard. And why does it need to be below the type system?
Maybe instead I should ask you what the already existing GC libraries for Rust get wrong? Do they require users to be unsafe or do something to break safety? Are they unusable to implement javascript?
The browser has a whole native heap (i.e. C++ objects, in current impls) that participates in the JS GC heap as follows:
- If the C++ object is referenced from C++ or from JS, it must be kept alive.
- If the C++ object references a JS object, then the JS object must be kept alive, so long as the C++ object would have been alive per the previous rule.
- It's possible for an object reference chain like JSobject->C++object->C++object->JSobject, and let's assume there are no other pointers to the C++ objects, in which case the last JS object should kept alive by GC only if the first JS object is alive.
- It's possible for dead reference cycles to exist like JSobject<->C++object, in which case both should die.
This requires that C++ has the ability to place references to JS objects in C++ fields.
This is where pinning comes in. It would be quite simple (and memory safe) but also totally incorrect (i.e. massive memory leak) to say that if a C++ field points to a JS object, then the GC just sees that field as a root. This is what I mean by pinning. (Note that "pinning" has many meanings in GC jargon; I'm using the Hermes version of the jargon. I guess I could have said "strong root" or something, but that's weirder to say.) This would be wrong, since it would not allow us to collect the dead cycle at all. Dead cycles are a common case in the browser. It would also cause other subtle breakage.
So, what the browser does instead is to have the C++ heap participate in GC: every C++ object that could possibly store a reference to JS objects anywhere can respond to GC callbacks asking it to account for all of those references. And, every C++ object needs to have a story for being referenced exclusively from JS, exclusively from C++, or a combo of the two. And the C++ code needs to be able to participate in whatever barrier discipline is necessary to get generations or incrementality or concurrency that the JS heap wants.
There are different ways to do this. Blink's Oilpan is probably the most principled, and that's basically a whole GC-for-C++ framework - very complex stuff. So, tons of inherently not-memory-safe code on the browser side just so it can do business with the JS heap.
The hard part of using a data structure like that is giving it ownership of the data and control over destroying it, but Rc does that too, doesn't it? That kind of thing is why I mentioned Rc. The difference between Rc and GC is much more in the behind-the-scenes tracking than in the API it gives.
If this code was all written in Rust, I could imagine there being almost no uses of `unsafe`, except for one: the thing where the GC decides to delete an object.
But this means that all of that code that isn't marked `unsafe`, but instructs the GC about what objects to mark or not, is really super unsafe because if it makes a logic error in telling the GC what to mark then the GC will delete an object that it should not have deleted.
So, the problem here isn't that you can't wrap the unsafe stuff in a safe API. The problem is that even if you do that, all of your seemingly-safe code is really super unsafe.
> The hard part of using a data structure like that is giving it ownership of the data and control over destroying it, but Rc does that too, doesn't it? That kind of thing is why I mentioned Rc. The difference between Rc and GC is much more in the behind-the-scenes tracking than in the API it gives.
The difference between RC and GC is exactly in the fact that the API they give is radically different. And that the behind-the-scenes tracking is different, too.
It's totally valid to tell RC "I want to point at this object so keep it alive".
But that's not the API that the GC will give you, unless it's a pinning API. The API where you tell the GC "keep this alive" will prevent the deletion of the garbage cycles I mentioned earlier.
So, the GC (in the case of something like a browser) gives a different API: one where user code either marks something, or not, at its discretion. There's no easy way to make that safe.
By putting it in a GC structure, it would be giving ownership to the GC, and would borrow it back to use. So whenever it's not borrowed, it's safe for the GC to delete it. And the GC won't delete anything outside of its control.
If you have a logic error and prematurely delete, then attempting to borrow the object will return None, which is still safe.
> It's totally valid to tell RC "I want to point at this object so keep it alive".
> But that's not the API that the GC will give you, unless it's a pinning API. The API where you tell the GC "keep this alive" will prevent the deletion of the garbage cycles I mentioned earlier.
The idea is that any pointing/pinning you do while manipulating an object would be a borrow on the stack. As soon as the function returns, there's no pinning.
So there's a few things that exist in this system:
* A GC-object references another GC-object, keeping the target alive while it is alive. A GC-object can be created by either Rust or JS. The loop you describe would be made up of GC-objects, so it would be straightforward to collect.
* A non-GC object has a permanent pinning reference to a GC-object. These are rare and purposeful.
* (Optional) A non-GC object has a weak reference to a GC-object, which can be collected at any time.
* Rust code has a temporary pinning reference to a GC-object on the stack, while traversing/manipulating it, and it goes away as soon as the function exits. It won't pin too long because the release is automatic. It won't free prematurely because the GC code knows the borrow is happening.
How can you have a real memory safe application when the memory itself is managed by a shit ton of code written in an unsafe language?
It turns out actually pretty effectively? There are going to be more bugs and boundary issues of the OS, but memory unsafety isn't some goop that oozes into your runtime.
My point is, people act like anything built with non memory safe languages is shit ever since Rust became the Jesus. We have the actual operating systems written in C, working ok. It is fine.
Yeah maybe Rust would be better but it is not a magic bullet and memory safety comes with many caveats in design of the code and possible performance issues when you are building something low level like a browser.
In addition to that, the browser is executing third-party code all the time while making it a good point of entry.
I mean, the most memory safe language will invoke syscalls. It will ask for memory pages and stuff. If we assume the app is running on a pile of shit because the OS is written in C, how can we be sure that the language is memory safe?
I understand the value of memory safe languages but people bashing on C/C++ code does not make sense to me when everything is essentially running on C. That’s all.
> In addition to that, the browser is executing third-party code all the time while making it a good point of entry.
This made me curious about something. Has there been many exploits stemming from JavaScript engines? I honestly do not know but as far as I can tell, most of the issues originate from faulty image decoders screwing up memory and tls implementations etc.
https://www.zdnet.com/article/chrome-70-of-all-security-bugs...
https://msrc.microsoft.com/blog/2019/07/a-proactive-approach...
> ~70% of the vulnerabilities Microsoft assigns a CVE each year continue to be memory safety issues
You may be right if you're talking about a GC'ed language, which gives memory safety and at the same time frees up the developer's mind. But if you're talking about constructs like borrow checkers then they do add cognitive load that is perhaps better spent elsewhere.