Skip to content

Instantly share code, notes, and snippets.

@badsyntax
Last active November 16, 2023 23:17
Show Gist options
  • Save badsyntax/9827722afcb33a4b0e03c809f1aede98 to your computer and use it in GitHub Desktop.
Save badsyntax/9827722afcb33a4b0e03c809f1aede98 to your computer and use it in GitHub Desktop.
The problem with using grpc in a vscode extension

Background

I'm using websockets & protobuf in my vscode extension. I want to replace the message abstractions & inconsistent code with a nicely defined server/client interface using grpc.

The Problem

After working on some POC code and running the extension for the first time, I was faced with the following error:

Activating extension 'richardwillis.vscode-gradle' failed: 
Failed to load gRPC binary module because it was not installed for the current system 
Expected directory: electron-v6.1-darwin-x64-unknown 
Found: [node-v72-darwin-x64-unknown] 
This problem can often be fixed by running "npm rebuild" on the current system 
Original error: Cannot find module '/Users/richardwillis/Projects/badsyntax/vscode-gradle/node_modules/grpc/src/node/extension_binary/electron-v6.1-darwin-x64-unknown/grpc_node.node' Require stack: - /Users/richardwillis/Projects/badsyntax/vscode-gradle/node_modules/grpc/src/grpc_extension.js - /Users/richardwillis/Projects/badsyntax/vscode-gradle/node_modules/grpc/src/client_interceptors.js - /Users/richardwillis/Projects/badsyntax/vscode-gradle/node_modules/grpc/src/client.js - /Users/richardwillis/Projects/badsyntax/vscode-gradle/node_modules/grpc/index.js - /Users/richardwillis/Projects/badsyntax/vscode-gradle/out/java-gradle-tasks/src/main/proto/service_grpc_pb.js -

This is because the grpc node library uses native binary modules, and when I run npm install on my host machine to install the extension dependencies, the installed grpc binaries do not match the platform & version of runtime vscode.

(It's also important to note that vscode does not run npm install when you install the extension from the marketplace. The publisher has to provide the node_modules when publishing.)

Workarounds

The error message above suggests:

This problem can often be fixed by running "npm rebuild" on the current system

When developing locally, the following command fixes the error:

npm rebuild grpc --target=v6.0.1 --runtime=electron --update-binary --fallback-to-build

This will regenerate the binaries to match vscode runtime (electron). You'll note we have to specify an electron runtime version. This complicates things when publishing, as different versions of vscode will be using different electron versions. So for example when a new vscode version is released with a new electron runtime version, the extension breaks. This means we can't rebuild grpc at build time, we have to do it at runtime.

This is the approach the IBM folks settled on with DependencyManager.ts, which installs missing dependecies on extension activate, is somewhat complicated and chunky, and has a few issues, for example: IBM-Blockchain/blockchain-vscode-extension#1621

The issue above exists because no pre-compiled binaries can be downloaded for the matching environment (this fix is awaiting release from google), and there are build issues when grpc attempts to build the binaries from source (which is why it's bad to run npm install on users' machines), as summarized here: IBM-Blockchain/blockchain-vscode-extension#1124 (comment)

This Sucks

Yep. I want to use grpc in my extension! I don't like the hacky approach the IBM folk have taken, but there doesn't seem to be any better option.

What can do be done about it? How can vscode better support this scenario?

This issues seems very relevant, but sadly has not been updated in a bit: microsoft/vscode#658

Related Issues

The Solution

The solution is to use a pure JavaScript implementation of the gRPC client: @grpc/grpc-js

This is easier said than done though, as it's not particularly easy to generate the correct Typescript type definitions.

Issues with TypeScript

The package recommends using @grpc/proto-loader to load the proto files and @grpc/grpc-js to construct the client at runtime:

const packageDefinition = protoLoader.loadSync(protoFileName, options);
const packageObject = grpcLibrary.loadPackageDefinition(packageDefinition);

When consuming this code in TypeScript, you'll get type definition errors, and you'll get around this by using Typescript's any, but this is not ideal and goes against the point of using Typescript.

In order to use the correct type definitions for your client and server stubs, you'll need to generate code at build time and generate types from that code.

Using the proto compiler, you'll need to use --grpc_out with --js_out to generate the service stubs and the gRPC client code. The problem is the compiler assumes you're using grpc-node and will add require('grpc') statements into the generated code.

To prevent this, you can do --grpc-out=generate_package_definition, which prevents the compiler from adding any require statements and client constructor code. This allow you to dynmically create the client at runtime, using @grpc/grpc-js's loadPackageDefinition, but now we're back to the same problem of not having a typed client. Also, the Typescript package I use to generate types (ts-protoc-gen) does not play well with "package definitions", and will generate types for the client and not the package definition, which means I cannot import the package defintion from typescript.

The Solution That Works with Typescript

Eventually I found an approach that works. It's only slightly hacky.

As @grpc/grpc-js is designed to be a "drop-in" replacement for grpc-node, I generate the server stubs & client code as I would do for grpc-node, using ts-protoc-gen to generate the types, and do a simple find/replace to replace grpc require/import statements with @grpc/grpc-js.

It's important to note that, at the time of writing, @grpc/grpc-js does not have feature parity with grpc-node. See here for feature comparisons: https://github.com/grpc/grpc-node/blob/master/PACKAGE-COMPARISON.md

Here's an example of how I do this:

#!/usr/bin/env bash

OUT_DIR="./out"
TS_OUT_DIR="./src"
IN_DIR="java-gradle-tasks/src/main/proto"
PROTOC="$(npm bin)/grpc_tools_node_protoc"
PROTOC_GEN_TS="$(npm bin)/protoc-gen-ts"

mkdir -p "$OUT_DIR"

$PROTOC \
    -I=./ \
    --plugin=protoc-gen-ts=$PROTOC_GEN_TS \
    --js_out=import_style=commonjs:$OUT_DIR \
    --grpc_out=:$OUT_DIR \
    --ts_out=service=grpc-node:$TS_OUT_DIR \
    "$IN_DIR"/service.proto

sed -i "" -e \
    "s/require('grpc')/require('@grpc\/grpc-js')/g" \
    "$OUT_DIR/$IN_DIR/"*

sed -i "" -e \
    "s/from \"grpc\"/from \"@grpc\/grpc-js\"/g" \
    "$TS_OUT_DIR/$IN_DIR/"*

In my typescript code, I construct the gRPC client like so:

import * as grpc from '@grpc/grpc-js'
import { GradleTasksClient as GrpcClient } from './java-gradle-tasks/src/main/proto/service_grpc_pb';

const grpcClient = new GrpcClient(
  'localhost:1234',
  grpc.credentials.createInsecure()
);
@badsyntax
Copy link
Author

I've added a bunch of examples on how to use TypeScript with gRPC here: https://github.com/badsyntax/grpc-js-types

@akshayjshah
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment