- Part 1: Introducing Electron, starting with npm, creating a browser window and adding Typescript
- Part 2: Creating the app website, adding react, and bundling with webpack
- Part 3: Integrating SASS into the build and consuming a UI toolkit
- Part 4: Using JSON-RPC as an easy way to reuse code from other languages
While electron’s main node process provides a complete standard library, and npm packages are available to implement all sorts of features that your desktop app might want to implement, sometimes you’ll need to call out to a separate commandline to get the behavior you want. Perhaps you already have a significant amount of code in another language you want to reuse, or you need to call a tool that already exists, like git.
There are various ways to call out to code in other languages. The git case (and others like it) are fairly straightforward: just call the commandline with the appropriate arguments, then parse the (ideally porcelain) output to get the information you need.
Another option is to host a webserver in the process you’re calling. Pretty much any language knows how to talk HTTP, so this can be a nice language-agnostic solution. You might feel hosting a full webserver for some quick IPC is overkill, though, and hosting an open endpoint on the local machine creates some security holes that need to be closed.
In this article we’ll talk about another option: using JSON-RPC over standard I/O. This feels like a nice solution: JSON-RPC is a minimal specification for doing API calls over some kind of boundary, and using stdin/stdout for communication instead of HTTP is comparatively lightweight while still being completely cross-platform.
Unfortunately, it’s not all good news — some ambiguities in the JSON-RPC spec and the fact that it isn’t widely used makes it a bit trickier to get started with.
For this example we’ll use a couple of libraries built for VS Code (and more specifically the Language Server Protocol implementation). They are vscode-jsonprc for the node end and StreamJsonRpc for the dotnet end respectively.
These two libraries are designed to be compatible with each other by default, which helps a lot. One problem with JSON-RPC is that the spec doesn’t actually specify how JSON objects are transmitted over the wire. A basic JSON-RPC implementation might just pick an encoding and then chain a series of JSON objects end-to-end. While this would be perfectly valid, most existing JSON parsers are designed to operate on a single string containing a JSON object rather than having to scan through text looking for the beginning and end of each object. For many transports (such as HTTP or websockets) this isn’t a problem, since the transport itself already breaks the stream up into messages — however stdio doesn’t have any such concept baked in.
The upshot of the above is that StreamJsonRpc and vscode-jsonrpc will expect (by default) for each JSON message to be delimited by HTTP-like headers followed by a blank line and the actual JSON string. These headers can contain many different options, but the most important is the Content-Length value, which tells the library how much text to read before passing it to the JSON parser. For our purposes this just works invisibly, but it’s important to be aware of when using different libraries or languages that also implement some form of JSON-RPC.
Once we have the right packages installed, getting started is reasonably straightforward, despite the sparse documentation:
There are some alternate ways we can set up the RPC server — for example, we could define a target object and then have the library automatically discover available methods through reflection — but using
AddLocalRpcMethod seems like the most straightforward option and lets us control the names of the API channels much more easily.
The code to set up the client is fairly straightforward as well:
One place we had trouble was getting the server commandline to shut down cleanly. Ideally we could do something like
rpc.Dispose() in the
shutdown handler, which should unblock the
await rpc.Completion line and allow the cli to shut down cleanly. However, we found that calling
rpc.Dispose meant that the
shutdown response message was never sent, which confused the client end. In the end we settled for waiting for the shutdown method to complete, then killing the child process.
There’s some more error handling needed for this code for it to be properly solid, but having the child process simplifies quite a few things — we basically only need to kill the child process if it appears to have hanged, and restart the child process if it dies unexpectedly. At that point we should be able to handle the errors coming from vscode-jsonrpc and retry, ignore, undo or rollback as appropriate with the new process.
Overall it’s still early days, but JSON-RPC appears to be working well for us so far. The simplicity of the protocol and its implementation is definitely appreciated and it works fully cross-platform.