As I mentioned in my overview of the upcoming Builder release, a lot of this development cycle focused on improving machinery the user does not see. In the past four years of development, a number of patterns emerged. When I started this project, I had some ideas of how to keep things organized, but Builder quickly grew beyond my ability to keep the whole design in my head at once.
Thankfully, I’ve worked on many large software teams during my tenure at companies like VMware and MongoDB. These company’s products have something in common, in that they’re a number of sub-systems performing specialized tasks that coordinate with each other. Not surprising, I know. But sometimes, knowing where to draw the lines between sub-systems is what differentiates products. (Interestingly, if you want to build a database engine, you better have a good reason to deviate from page 144 of Architecture of a Database System).
Now days, Builder segments the code into a series of static libraries. We have a core library that provides the basic functionality used by all sub-systems. We have a process and threading library. One library deals with representing and manipulating source code. Yet another focuses on building, executing, testing, and packaging software. To enable Language Servers, we have one too. Even the gui and editor are somewhat compartmentalized.
In the end, we link all of these static libraries into the final executable, which is then processed to generate GObject Introspection data; the *.gir
and *.typelib
. That means that our plugins, written in C, C++, Python, or Vala, do not need to rely on linking against any shared libraries. All symbols come from the executable with -export-dynamic
. (One side-effect of this, is that we no longer allow linking our Vala based plugin into the executable, since it relies on *.gir
generation).
To keep startup fast, all of our bundled C-based plugins are compiled into the final executable. That reduces the number of link loader work, file-system directory scanning and iops, and allows for more aggressive compiler optimizations. We also avoid loading some plugins in the early initialization phase, instead deferring that work until later.
The biggest change in the refactoring is IdeObject. This object has become somewhat like a multi-threaded version of what GtkObject
used to be. It allows us to build a top-down object tree. The root of the tree is a specialized form, IdeContext. Each sub-system mounts itself on this tree. Plugins may have or work with objects that are descendants of the sub-systems. In the end, it makes things much easier for me to debug at runtime because I can get a clearer picture of how things interact and what plugins are active. One interesting feature of IdeObject
is that we guarantee they are only ever finalized in the main thread.
The sub-system that I think resulted in the most cleanup was libide-foundry.a
. This contains the build manager, pipelines, and extension points for plugins to provide the the abstraction glue. It also contains runtime abstractions, execution management, testing infrastructure, device management (phone, tablet, simulator), and toolchains. Currently, the debugger engine is a separate library, but I may fold it into this library for completeness.
When cleaning up libide-code.a
, I simplified the buffer management. This code was a bit temperamental because I put a few too many features into the base class. I think that happened simply because I couldn’t yet foresee many of the ways it would be extended. Seeing how the code evolved over time allowed me to create better abstractions and extract those features.
What kicked off this whole process was an hour hack to add multi-monitor support. That somehow turned into a 2 month project that involved me ripping everything apart and putting it back together because of our initial design. I’m happy to say that I think things turned out quite well and we are poised to keep moving fast for the next few development cycles to come.
We’re getting many more requests to support external plugins in Builder. I’ve always taken the Linux kernel approach here in that I very much want to encourage upstream contributions over external plugins. However, I am starting to loosen up just a bit and we’ve added the X-Builder-ABI=3.31
key to plugin definitions. This allows us to continue to evolve our APIs but promise to not break them in micro releases. My expectation, however, is that this major refactoring will set us down the path towards some amount of ABI stability.
In my next post I’d like to cover how the UI components are abstracted and give you an idea of which plugin interfaces to use and when. In the mean time, I have plenty of documentation to write.