Since Google Chrome (and other browsers based on the Chromium source code, such as Google Chrome Canary, Opera 15+, Maxthon, White Hat Aviabor) will no longer support NPAPI soon, we needed to find a different way for our bookmarks managemenet apps to communication with our Chrome extension. The alternative suggested by the Chrome team, Native Messaging, looks like it will do the job. At least it passed our proof-of-concept test. UPDATE: It is now shipping in the Stable/Production versions of our apps Synkmark, Markster and BookMacster. The architecture looks like this…
This architecture looks quite similar to how NPAPI worked, except that Your “Messenger” Tool was your NPAPI plugin. It still has two moving parts, but the Messenger has a drastically smaller surface area than the NPAPI Plugin…
• Its size, in our case, is looking to be less than 200 KB, compared to the 2 MB size of our NPAPI plugin, which is based on FireBreath. (I gave up after a couple days of trying to write an NPAPI plugin myself, and found FireBreath.)
• Much as I appreciate the FireBreath product, it was scary to use such a complicated black box which I could barely troubleshoot when there was an issue. You see, I gave up on C++ a decade ago after realizing that I could never learn the new C++ features as fast as the C++ gurus were adding them. Life will be sweet with no more shared pointers, no more Boost library, no more black magic, no more CMake. (CMake is another amazing open source project, one which Firebreath uses to create cross-platform project files for Apple’s Xcode. Unfortunately, since Apple changes Xcode so much and so frequently, it is challenging to keep it working.)
• I no longer need to worry whether or not our NPAPI plugin will work when the Chrome team ever decides to ship a 64-bit version of Chrome for OS X. (A 64-bit developer build has recently appeared for Microsoft Windows.) Of course, it could not be tested until they do. Now, it won't be needed :))
• When our extension is installed into multiple user profiles in Chrome (which can be running simultaneously), each profile launches its own instance of our Messenger tool. Our NPAPI plugin required some careful accounting to avoid “crosstalk" in this case.
• Because whatever goes into ~/Library/Internet Plug-Ins gets loaded into all browsers whether it needs them or not, our plugin uselessly loads itself into Firefox, Safari, etc., where it has no effect other than to waste the user’s RAM, and paste our good name into the Crash Report whenever one of these browsers decides to take a nosedive. Viva isolated processes!
* * *
Here is how the above subsystem seems to work. When user opens the first window in a given Chrome profile, a background.html page in our Chrome extension invokes chrome.runtime.connectNative() with a given host name. Chrome then looks for a special manifest file matching the given host name in
~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
Of course, our extension installer will have installed this file. Most importantly, it contains the path to our new “Messenger” tool, a Command-Line tool written in Objective-C. We’re probably going to install that into our own ~/Library/Application Support folder. Knowing this path, Chrome launches an instance of our Messenger tool.
Chrome communicates with its child messenger via unix stdin and stdout. It seemed like an odd choice for interprocess communication, until I realized that these big-name web browsers need to be cross-platform and run on old systems. They needed something ubiquitous.
In the Chrome-to-Messenger direction, you first open communication via chrome.runtime.connectNative() , which returns a port object that will accept a postMessage() call whose single parameter is a JavaScript object, typically a dictionary. Very nice; you can use the keys in the dictionary like commands, and the values like command parameters. Via stdin, the Messenger receives a JSON serialization of this object, prefixed by a four byte header. The four byte header is one field, the length of the JSON data in bytes.
In the Messenger-to-Chrome direction, your Messenger similarly creates a similar command/parameter dictionary, serializes it object into JSON, (using for example, NSJSONSerialization), encodes it to data as UTF8, then gets the length of the data, prepends it to the JSON, and writes it to stdout. About 9 lines of Objective-C code if you’re verbose like me. Not bad.
Apparently because the background.html is a child of the Chrome user profile and not of a window, communication remains open even after the last browser window of the current user profile in Chrome is closed. (Whew! This behavior is critical for edge-case apps like bookmarks management, that work on the background profile data, unrelated to a browser window.)
When Chrome completely quits, it apparently sends an end-of-file (EOF) to your Messenger. (I can’t see the EOF for sure because NSFileHandle probably swallows it, but I’m guessing this must be what happens.) Anyhow, when your messenger receives an empty stdin from NSFileHandle, it should terminate itself gracefully too. Or you can just wait for the system to kill it due to its parent process (Chrome) terminating.
I’ve noticed one odd but probably harmless behavior and maybe beneficial behavior, which is that if Chrome is running with browser windows open in two different user profiles, and both have our extension loaded and consequently two of our Messenger tools are running, if you quit Chrome without closing the browser windows, then relaunch, two Messenger tools will launch, apparently one for each user profile, even though only one browser window opens (for the last-started user profile). Probably, Chrome re-opens two background.html pages, one for each profile which had a browser window open when Chrome was quit. If browser windows are closed first, then only one Messenger will launch, for the one browser window which opens.
Native Message has a very nice, super-minimalist, low-level protocol. You define your own higher-level protocols with commands and parameters (Use JSON!). The design process is natural, and easily extensible; you can pretty much make it up as you go along.
I think the Chrome team did a nice job on Native Messaging. I just wish they’d done it four or five years ago, so I wouldn’t have had to design in and then design out NPAPI 😩
* * *
Update 2017-03-03. Firefox now uses the Chrome extension system, with this same Native Messaging. Future versions of our Firefox extension will use it.
Safari extensions, unfortunately, still have so few capabilities that we’ve found no reason to bother writing one yet.