In this post I will show how to use Bazel with Protocol buffers to share types between Typescript, Java and C++ in the same application.

Multiple Languages

Bazel is a language agnostic build system. This means Bazel can build your entire tech stack, even if it’s made up of very different programming languages.

Most mainstream programming languages are already supported by Bazel.

In my demo application I currently have code written in Typescript (Angular + NodeJS), Java and C++.

One typical challenge when working in different languages is that it can be difficult to synchronize types across language boundaries.

A typical example is frontend written in Typescript and a backend written in Java. If the shape of the objects returned from Java fall out of sync with the client code, the application will fail at runtime.

I have seen this happen numerous times. If you catch it too late, you have to scramble to fix bugs in production as a result.

Wouldn’t it be nice if we could catch this at compile time instead?

Protocol Buffers

It turns out we can use Protocol Buffers to address this issue.

You can think of Protocol Buffers as a language neutral standard for defining schemas. Even though the syntax is different, a protocol buffer is similar to defining schemas in json or xml.

Below is an example of a Protocol buffer.

syntax = "proto3"; option java_multiple_files = true; option java_package = "angular_samples.api"; message Person { string name = 1; string address = 2; } message Persons { repeated Person persons = 1; }

The idea is that you define “messages” that make up the schema for the object you are describing. In this sample I have created a person object with two properties; name and address. The number on the right is just a running sequential number used during serialization.

Notice the singular Person message vs the plural Persons message. Person defines individual objects with a name and address property. Persons defines an object with an array or Person objects. The array is returned by the persons property.

Now that we have the schema, how can we use it to synchronize types across multiple languages.

The answer is code generation. We can’t use a Protocal buffer directly in our code, but we can include Bazel rules that will generate code for supported languages.

In the following code listing I will show Bazel rules for Typescript, Java and C++:

package(default_visibility = ["//visibility:public"]) load("@build_bazel_rules_typescript//:defs.bzl", "ts_proto_library") proto_library( name = "person_proto", srcs = ["person.proto"], ) ts_proto_library( name = "person_ts_proto", deps = [":person_proto"], ) java_proto_library( name = "person_java_proto", deps = [":person_proto"], ) cc_proto_library( name = "person_cc_proto", deps = [":person_proto"] )

The first rule, proto_library, is the base rule that points to the file (person.proto) where the schema is defined.

Next we have language specific rules that depend on proto_library. The result of running those rules is generated language specific code. In this case C++, Java and Typescript objects are created.

Since the objects are generated from the same schema, we can trust that the definitions are kept in sync.

Generated Code

The language specific code is actually a bit more than just plain old objects. The generated code comes with a builder api that can be used to create objects.

Below I have included examples in Java and C++:

C++
#include "person-service.h" const char* getPeople() { Persons* persons = new Persons(); Person* person1 = persons->add_persons(); person1->set_name("Joe"); person1->set_address("Test Street1"); Person* person2 = persons->add_persons(); person2->set_name("Peter"); person2->set_address("Test Street2"); Person* person3 = persons->add_persons(); person3->set_name("Jane"); person3->set_address("Test Street3"); Person* person4 = persons->add_persons(); person4->set_name("Mary"); person4->set_address("Test Street4"); google::protobuf::util::JsonPrintOptions options; std::string result; MessageToJsonString(*persons, &result, options); const char * res = result.c_str(); delete persons; return res; }
Java
package backend.api; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import angular_samples.api.Person; import angular_samples.api.Persons; import com.googlecode.protobuf.format.JsonFormat; public class PersonServlet extends HttpServlet { JsonFormat jsonFormat = new JsonFormat(); @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { Person person1 = Person.newBuilder().setName("Joe").setAddress("Test Street1").build(); Person person2 = Person.newBuilder().setName("Peter").setAddress("Test Street2").build(); Person person3 = Person.newBuilder().setName("Tom").setAddress("Test Street3").build(); Person person4 = Person.newBuilder().setName("Jack").setAddress("Test Street4").build(); Person person5 = Person.newBuilder().setName("Bob").setAddress("Test Street5").build(); Persons persons = Persons.newBuilder() .addPersons(person1) .addPersons(person2) .addPersons(person3) .addPersons(person4) .addPersons(person5).build(); response.setContentType("application/json"); response.getWriter().print(jsonFormat.printToString(persons)); } }

In my demo application the Java and C++ code return objects that are consumed by the Typescript frontend.

The Typescript proto rule generates IPerson and IPersons interfaces that can be used to make sure the client and backend are in sync.

If the objects fall out of sync, you will get a compile time error instead of a runtime error.

See example below:

Component
this.res = this.addressBookService.getEntries().pipe(map((persons: Array<IPerson>) => { // IPerson is generated by Bazel from person.proto return persons.map((person: IPerson) => ({firstName: person.name, streetAddress: person.address})) }))
Service
import {Http} from '@angular/http'; import {Injectable} from '@angular/core'; import {IPersons, IPerson} from '../../models'; import {map} from 'rxjs/operators'; import {Observable} from 'rxjs'; @Injectable() export class AddressBookService { http: Http; constructor(http: Http) { this.http = http; } getEntries(): Observable<Array<IPerson>> { return this.http .get('/api/people') .pipe(map(res => res.json()), map((res:IPersons) => res.persons)); } }

If you are interested in trying this yourself, just download my project from Github.

In a future article I will discuss how to combine this with gRPC