Inside Rubberduck (pt.1)

https://www.cgtrader.com/free-3d-print-models/art/sculptures/rubber-duck-voronoi-style
“Rubber Duck” – Voronoi Style Free 3D print model by Roman Hegglin

Maybe you’ve browsed Rubberduck’s repository, or forked it to get a closer look at the source code. Or maybe you didn’t but you’re still curious about how it might all work.

I haven’t written a blog post in quite a long while (been busy!), so I thought I’d start a series that describes Rubberduck’s internals, starting at the beginning.

Part I: Starting up

Rubberduck embraces the Dependency Injection principle: depend on abstractions, not concrete implementations. Hand-in-hand with DI, the Inversion of Control principle describes how all the decoupled pieces come together. This decoupling enables testable code, which is fundamental when your add-in has a unit testing framework feature in any project of that size.

Because RD is a rather large project, instead of injecting the dependencies (and their dependencies, and these dependencies’ dependencies, and so on…) “by hand”, we use Ninject to do it for us.

We configure Ninject in the Rubberduck.Root namespace, more specifically in the complete mess of a class, RubberduckModule. I say complete mess because, well, a couple of things are wrong in that file. How it steals someone else’s job by constructing the menus, for example. Or how it’s completely under-using the conventions Ninject extension. The abstract factory convention is nice though: Ninject will automatically inject a generated proxy type that implements the factory interface – you never need a concrete implementation of a factory class!

The add-in’s entry point is located in Rubberdcuk._Extension, the class that the VBE discovers in the Windows Registry as an add-in to load. This class implements the IDTExtensibility2 interface, which looks essentially like this:

public interface IDTExtensibility2
{
    void OnAddInsUpdate(ref Array custom);
    void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom);
    void OnStartupComplete(ref Array custom);
    void OnBeginShutdown(ref Array custom);
    void OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom);
}

The Application object is the VBE itself – the very same VBE object you’d get in VBA from the host application’s Application.VBE property, and there are a number of things to consider in how these methods are implemented, but everything essentially starts in OnConnection and ends in OnDisconnection.

So we first get hold a reference to the precious Application and AddInInst objects that we receive here, but because we don’t want a direct dependency on the VBIDE API throughout Rubberduck, we wrap it with a wrapper type that implements our IVBE interface – same for the IAddIn(yes, we wrapped every single type in the VBIDE API type library; that way we can at least try to make Rubberduck work in VB6):

 var vbe = (VBE) Application; 
 _ide = new VBEditor.SafeComWrappers.VBA.VBE(vbe);
 VBENativeServices.HookEvents(_ide);
 
 var addin = (AddIn)AddInInst;
 _addin = new VBEditor.SafeComWrappers.VBA.AddIn(addin) { Object = this };

Then InitializeAddIn is called. That method looks for the configuration settings file, and sets the Thread.CurrentUICulture accordingly. When we know that the settings aren’t disabling the startup splash, we get our build number from the running assembly and bring up the splash screen. Only then do we call the Startup method; when Startup returns (or throws), the splash screen is disposed.

The method is pretty simple:

private void Startup()
{
    var currentDomain = AppDomain.CurrentDomain;
    currentDomain.AssemblyResolve += LoadFromSameFolder;

    _kernel = new StandardKernel(
        new NinjectSettings {LoadExtensions = true}, 
        new FuncModule(), 
        new DynamicProxyModule());
    _kernel.Load(new RubberduckModule(_ide, _addin));

    _app = _kernel.Get<App>();
    _app.Startup();

    _isInitialized = true;
}

We initialize a Ninject StandardKernel, load our module (give it our IVBE and IAddIn object references), get an App object and call its Startup method, where the fun stuff begins:

public void Startup()
{
    EnsureLogFolderPathExists();
    EnsureTempPathExists();
    LogRubberduckSart();
    LoadConfig();
    CheckForLegacyIndenterSettings();
    _appMenus.Initialize();
    _hooks.HookHotkeys(); // need to hook hotkeys before we localize menus, to correctly display ShortcutTexts
    _appMenus.Localize();

    UpdateLoggingLevel();

    if (_config.UserSettings.GeneralSettings.CheckVersion)
    {
        _checkVersionCommand.Execute(null);
    }
}

The method names speak for themselves: we conditionally hit the registry looking for a legacy Smart Indenter key to import indenter settings from, and run the asynchronous “version check” command, which sends an HTTP request to http://rubberduckvba.com/build/version/stable, a URL that merely returns the version number of the build that’s running on the website: by comparing that version with the running version, Rubberduck can let you know when a new version is available.

That’s literally all there is to it: just with that, we have a backbone to build with. If we want a new command, we just implement an ICommand, and if that command goes into a menu we hook it up to a CommandMenuItem class. Commands often delegate their work to more specialized objects, e.g. a refactoring, or a presenter of some sort.

Next post will dive into how Rubberduck’s parser and resolver work.

to be continued…

6 thoughts on “Inside Rubberduck (pt.1)”

  1. First of all, nice article!

    “So we first get hold a reference to the precious Application and AddInInst objects that we receive here”
    What does ‘here’ mean? They get passed to the interface’s methods, but where do they come from?

    “because we don’t want a direct dependency on the VBIDE API throughout Rubberduck, we wrap it with a wrapper type that implements our IVBE interface”
    What are the negative implications of having a direct dependency? Is it only for unit-testing purposes (not being able to mock out these dependencies)?

    Like

    1. The IDTExtensibility2 interface is supplying the VBE and AddIn instances in OnConnection: that method gets invoked by the VBE when it loads its add-ins.

      As for the wrappers, v1.x didn’t have them, and we still had unit tests; the reason it was still possible is because the VBE (and VBComponent, and all other types in the VBIDE API) is really just a COM interface, so we were able to mock them in our tests. I intentionally skipped that part of the startup code, but there’s a type check going on that verifies whether the Application object we get is VBA’s editor, or VB6’s: if you look at the SafeComWrappers namespaces in RD.VBEditor, you’ll see a VBA and a VB6 namespace, each with their respective implementations of the same interfaces. The plan is to *eventually* get RD to run in VB6, and with a direct dependency on the VBA type library, that wouldn’t be possible.

      Another advantage with that additional layer of abstraction is that we can add our own members and eliminate a lot of otherwise redundant code, and/or helper code that would be in static helper classes.. and we get to catch COMExceptions before they bubble up to RD, e.g. when you call the FileName property getter on a VBProject that was never saved, the VBIDE API throws; our wrapper API catches that exception and returns an empty string instead.

      Like

    1. Good question. The basic groundwork is done, in theory we “just” need to implement all the wrappers over the VB6 interop types (the VB6 interop library we have currently seems to be missing some important members for some reason) and adjust the parser logic to read the source files where they are instead of exporting modules to temp files (to parse module & member attributes). I want RD to work in VB6… Heck, source control would be much easier with a .vbp project file and all source code in the file system instead of important bits hidden in a host document! I’ll take a PR that gets Rubberduck to load and run in VB6 any day! Right now the handful of us are focusing on eliminating the few resolver hiccups (causing inspection false positives), and more importantly shutting down cleanly (which may involve ditching our current IoC container /DI framework). RD in VB6 will certainly happen, but we need to get a stable 2.1 release that consistently cleanly unloads first =)

      Like

    2. The latest merged pull request introduces a fully workable interop library for VB6 – the many COM wrappers still need to be implemented, but FYI this is work-in-progress, and we can definitely have Rubberduck 2.1 (“green release”) running in VB6; perhaps not with all features (source control and unit testing are particularly problematic), but the core will be there.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s