# Go for Dart with Protocol Buffers

## Introduction

In this article, I propose a solution that enables a rapid expansion of the available APIs for Dart to be used on the server, by using existing drivers for Go. I begin by laying out the context of why this is needed. Then, I describe the solution. Finally, I do a walk around [mongo_go](https://pub.dev/packages/mongo_go), the package I implemented following this approach.

## The general context

Dart has proven its power on the UI front. However, it still does not have the traction it deserves on the server. There are at least the following two aspects worth considering.

One, it needs a complete server-side framework. There are already some packages that help a lot with this. There's [Shelf](https://pub.dev/packages/shelf). There is also the [grpc](https://pub.dev/packages/grpc) package. While it’s not only focused on server-side development, it also very well supports hosting gRPC services with Dart. There are also frameworks such as [Conduit](https://www.theconduit.dev) and more. Finally, I am the maintainer of [Dartaculous](https://gitlab.com/dartaculous/dartaculous). It is a different kind of full-stack Dart framework that leverages gRPC.

The second aspect is the lack of a set of official drivers to connect to services, like for instance database engines. I will be focusing on connecting to databases, but that is a subset of a broader issue. Normally we would need to also be able to connect to queue managers, caching services, and online APIs, amongst other examples. Even services with REST or gRPC endpoints are sometimes easier to use with language-specific APIs. An example is Firebase's Admin SDK, which has REST and gRPC endpoints, but also packages for C++, Go, and so on. Using external services that require a specific API to work with has been, by far, my greatest challenge when using Dart for the server. Good luck trying to connect Dart to an Oracle database server for instance (just for example, not that I want to). The story around open-source databases has, by nature, been a better one. For instance, there are community-developed packages for PostgreSQL and MySQL.

Other than that, Dart has all the features needed for a server-side language.

## Why Dart for the Server

If you have gotten here and are still reading this, you probably do not need any convincing as to why it is appealing to use Dart on the server. Eventually, you may skip this part. However, if you are wondering why not simply limit Dart's use to Flutter and use a language with good support on the server, like Go, here are the reasons.

Most of the applications I develop are line-of-business applications that carry many entities back and forth between the client and the server. This means that the size of my business model tends to be more than trivial. Using the same language for the client and the server means I get to reuse the same implementation of that business model on both ends, including validations.

Even more, there is further immense value in having most of the system represented in a single programming language. It adds flexibility. It significantly lowers the barrier to full-stack development. The benefits are analogous to the ones we get from using document databases instead of having to deal with the mapping between objects and relational. It is like we are removing a different kind of impedance mismatch.

## Our specific challenge

There is an excellent Dart community-developed driver for our database server of choice, MongoDB. It is [mongo_dart](https://pub.dev/packages/mongo_dart). However, of the few missing features of that driver, streams and transactions were crucial ones for us. These features are present in Mongo DB's official drivers for several languages with official support.

There is a pending issue with the community driver for some time for bringing sessions to it (issues [148](https://github.com/mongo-dart/mongo_dart/issues/148), [200](https://github.com/mongo-dart/mongo_dart/issues/200) and [274](https://github.com/mongo-dart/mongo_dart/issues/274)). So, I inferred that if its developers still have not added the feature by now, then it probably would not be trivial for me to dive into that code and do it myself. This led me to think “if only I could reuse an official driver and easily re-expose it for Dart, and by the way, if this could result in a reusable approach to similar issues...”.

## The Solution

### The Gist

A possible path to solve this issue would be to develop a microservice in Go. It would receive requests from Dart and then use Mongo DB's official driver for Go to execute those requests over the database. A good protocol to connect Dart to Go would be gRPC, using Protocol Buffers. If you are not familiar with gRPC, it is a high-performance RPC framework initially developed and currently stewarded by Google. Protocol Buffers are the data format with which messages are encoded. The structure of that data is defined by an interface definition language (IDL). The IDL is not specific to any programming language. However, at development time, we can compile the IDL definitions into types specific to each of the supported programming languages. For developers that use REST, the IDL is Protocol Buffers' equivalent to OpenAPI specification documents. Implementing a Go microservice would work, but it would require yet another service in the stack.

It just so happens that Go can consume C libraries. Even better, Go packages can be compiled into libraries that expose native C APIs. Both are a part of a feature in Go called [cgo](https://go.dev/blog/cgo). And it just so happens that Dart can invoke dynamic libraries that expose native C APIs. This feature is called [Dart FFI](https://dart.dev/guides/libraries/c-interop). FFI stands for Foreign Function Interface. So, an alternative path would be to have Dart call Go functions via FFI instead of implementing a Go microservice.

The challenge with having Dart call GO methods via Dart FFI is that Dart and Go have wildly different type-systems. The problem is compounded by the fact that both languages have garbage collectors, yet what lies in the middle are C-style structs that require manual memory allocation and deallocation. (We could also try to implement a Dart package that directly uses Mongo DB's C driver through FFI, but that would have been an even more daunting undertaking, and we would be losing out on Go routines - more on that later).

Why not use aspects of both approaches? We could have Dart and Go live under the same process and communicate with each other via FFI. However, instead of marshalling our Dart objects to C structs and then from those to Go objects, a much simpler approach would be to use Protocol Buffers as a *lingua franca* between both languages. With this approach, we still need to marshal from Dart to C and from C to Go, but with a major difference. We will no longer be converting each different instance of a Dart class to a C struct, and from a C struct to a Go object. Instead, we map the Dart object into a Protocol Buffer message, which is serialized into a byte array. That byte array is sent via Dart FFI to Go. Go then deserializes the byte array into a Protocol Buffer message and maps it into the final Go struct.

So, what are the advantages of this approach? Let’s say we’ve got a graph of objects and we want to pass that graph from Dart to Go. With Dart FFI without using Protocol Buffers, we would need to allocate memory for each of the objects in the graph, do all the conversions, invoke the Go function and then ensure we release all the allocated memory for all the objects we converted to C structs (I’m simplifying things). With Protocol Buffers, we map the objects into Protocol Buffer messages (which are also garbage-collectable objects) and then serialize those into a byte array. This means that the entire graph gets represented by a single array of bytes. Then, there is the single point of manual memory allocation for the array, so we can pass that array to Go. After completing the call, again we have a single point of memory deallocation for the array we passed. It is much easier to use this approach and much safer. Manual memory allocation has been a major source of vulnerabilities. So, it is always best if we can reduce the number of places where we do it to the absolute minimum.

In fact, by always passing arrays, we can have most functions in Go implemented with the same signature. This means we can implement a reduced set of reusable methods in Dart to call all the functions in Go. This reduces the potential for mistakes and for inadvertently introducing a vulnerability (or a memory leak, or a crash) when making calls to Dart FFI.

Because this method is so reusable, we published a Dart package meant precisely to aid the usage of FFI with Protocol Buffers to call Go functions. It is the [go_bridge](https://pub.dev/packages/go_bridge) package. There is a corresponding Go package to aid in receiving calls from Dart via FFI with Protocol Buffer messages. It is the [dart_bridge](https://pkg.go.dev/gitlab.com/squarealfa/dart_bridge) package. Now, here’s great news for you if you intend to use this technique. All the manual memory allocation and deallocation you will ever need to do are implemented for you in those packages. This means you may use Dart FFI without worrying about manual memory management.

### Asynchronous Execution

Dart is used, by default, as a single-threaded language, with event loops being used for asynchronous execution. Dart does have a feature called Isolates for multi-threading, but unlike other multi-threaded languages, it does not support memory sharing. It uses message passing instead. For the sake of the rest of this discussion, we may ignore Isolates, but here's an [excellent explanation](https://www.youtube.com/watch?v=vl_AaCgudcY&t=26s). Go, on the other hand, is famous for how easy it is to do multi-threading with Goroutines and channels. The challenge here is that once again we need to use two different execution models and fit them through a C API.

How to get asynchrony between Dart and Go, then? The adopted solution to this uses a Dart [ReceivePort](https://api.dart.dev/stable/2.18.5/dart-isolate/ReceivePort-class.html) together with a Dart [Completer](https://api.flutter.dev/flutter/dart-async/Completer-class.html). A `ReceivePort` is a Dart object that allows us to receive messages from other Dart isolates. What is so special about this object is that we can also use it to receive messages from other languages through the C API. Each `ReceivePort` object has a [SendPort](https://api.dart.dev/stable/2.18.5/dart-isolate/SendPort-class.html) attached to it. We can send the `SendPort` to Go (in reality we are sending its native port). Go can then send messages back through the `SendPort`. Those messages are handled by the `ReceivePort`. A `Completer` is a Dart object we can await until something calls its [complete](https://api.flutter.dev/flutter/dart-async/Completer/complete.html) method.

So, here's the sequence of events, from Dart's side:

1.  We create a `Completer` and a `ReceivePort`. We set up the `ReceivePort` to complete the `Completer` when the `ReceivePort` receives a message.
    
2.  We invoke a Go function, sending the `ReceivePort`'s `SendPort` and whatever data we want to send to Go.
    
3.  We await the completer and handle the result it returns.
    

Here is how it goes from Go's side (doesn't this sound cool?):

1.  The function called has a parameter with the `SendPort` it received from Dart and any other parameters containing whatever additional data.
    
2.  It creates a Goroutine passing the port and the data (this is a simplification as right now we are focusing on async execution and leaving the data marshalling aside for now).
    
3.  Whenever the Goroutine does its job, it sends a message through `SendPort`.
    

The following sequence diagram illustrates what we have just discussed:

![asynchronous execution sequence diagram](https://gitlab.com/dartaculous/dartaculous/-/raw/ff0ff5d93675ab8539d31ad7a398fecd325e73b4/mongo_go/docs/sequence.png align="left")

## mongo_go

From this point, it will be easier to explain how everything is put together by using a concrete implementation. For that purpose, we will be looking at a package called [mongo_go](https://pub.dev/packages/mongo_go) as a reference implementation. mongo_go is the first package that I implemented using the proposed approach in this post. It is in essence MongoDB's GO driver, re-exposed for Dart.

### The Go side

The first file we can look into is the facade that Go exposes for Dart. It is the [mongo_go.go](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/mongo_go.go) file. This file defines all the functions that Dart will call. There are all sorts of functions here, that allow us to connect to the DB server, get handles to databases, get handles to collections, insert documents, find documents, and so on. It is easy to identify which functions are meant to be called by Dart, as they all have a comment that instructs `cgo` to export them, in the form `//export [function name]`. Here is a very short snippet of the available methods with their implementation removed:

```go
//export initialize
func initialize(api unsafe.Pointer) { 
    /// implementation
}

//export connectMongo
func connectMongo(port int64, buffer *C.uchar, size int) {
    /// implementation
}

//export database
func database(port int64, buffer *C.uchar, size int) {
    /// implementation
}

//export collection
func collection(port int64, buffer *C.uchar, size int) {
    /// implementation
}

//export updateOne
func updateOne(port int64, buffer *C.uchar, size int) {
    /// implementation
}

//// many others
```

The first function we need to call from Dart is `initialize`. It needs to be called only once. This call is needed for Go to be able to send messages back to Dart. Dart must call this method at least once before calling any other function in this library. So, with this, let's get `initialize` out of the way and not think of it again.

Notice that all the functions shown above, except `initialize`, have the same signature. This does not mean that they receive the same types. It means that the type definitions are not visible in the signature of the functions, but they are present in the Protocol Buffer IDL definitions (more on that in a short while). The parameters are:

*   `port`: The `SendPort` that the function will use to send the return value to Dart's `ReceivePort`. If you're confused with all the ports by now, just think of it as "the communication channel through which Go will return its value to Dart".
*   `buffer`: The array of bytes containing the protocol-buffer encoded message.
*   `size`: The size of the array of bytes.
    

To export this package as a dynamic library that exports these functions as C native functions, we can run the following command in the terminal (you may look at the [package's Makefile](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/Makefile#L8)):

```sh
go build -buildmode=c-shared -o mongo_go.so
```

This will compile the Go package into `mongo_go.so`. It will also generate a [header file](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/mongo_go.h#L77) containing all the C function declarations. Here is its core:

```c
extern void initialize(void* api);
extern void connectMongo(GoInt64 port, unsigned char* buffer, GoInt size);
extern void disconnect(GoInt64 port, unsigned char* buffer, GoInt size);
extern void startSession(GoInt64 port, unsigned char* buffer, GoInt size);
extern void closeSession(GoInt64 port, unsigned char* buffer, GoInt size);
extern void startTransaction(GoInt64 port, unsigned char* buffer, GoInt size);
extern void commitTransaction(GoInt64 port, unsigned char* buffer, GoInt size);
extern void abortTransaction(GoInt64 port, unsigned char* buffer, GoInt size);
extern void database(GoInt64 port, unsigned char* buffer, GoInt size);
extern void dropDatabase(GoInt64 port, unsigned char* buffer, GoInt size);
extern void collection(GoInt64 port, unsigned char* buffer, GoInt size);
extern void listDatabaseNames(GoInt64 port, unsigned char* buffer, GoInt size);
extern void insertOne(GoInt64 port, unsigned char* buffer, GoInt size);
extern void insertMany(GoInt64 port, unsigned char* buffer, GoInt size);
extern void updateOne(GoInt64 port, unsigned char* buffer, GoInt size);
extern void updateMany(GoInt64 port, unsigned char* buffer, GoInt size);
extern void replaceOne(GoInt64 port, unsigned char* buffer, GoInt size);
extern void deleteOne(GoInt64 port, unsigned char* buffer, GoInt size);
extern void deleteMany(GoInt64 port, unsigned char* buffer, GoInt size);
extern void findOne(GoInt64 port, unsigned char* buffer, GoInt size);
extern void findOneAndDelete(GoInt64 port, unsigned char* buffer, GoInt size);
extern void findOneAndUpdate(GoInt64 port, unsigned char* buffer, GoInt size);
extern void findOneAndReplace(GoInt64 port, unsigned char* buffer, GoInt size);
extern void find(GoInt64 port, unsigned char* buffer, GoInt size);
extern void countDocuments(GoInt64 port, unsigned char* buffer, GoInt size);
extern void estimatedDocumentCount(GoInt64 port, unsigned char* buffer, GoInt size);
extern void aggregate(GoInt64 port, unsigned char* buffer, GoInt size);
extern void watch(GoInt64 port, unsigned char* buffer, GoInt size);
extern void createOneIndex(GoInt64 port, unsigned char* buffer, GoInt size);
extern void listIndexes(GoInt64 port, unsigned char* buffer, GoInt size);
extern void dropOneIndex(GoInt64 port, unsigned char* buffer, GoInt size);
extern void dropAllIndexes(GoInt64 port, unsigned char* buffer, GoInt size);
extern void bulkWrite(GoInt64 port, unsigned char* buffer, GoInt size);
```

From now on, we will use the `insertOne` function as a reference example. This corresponds to Mongo DB's [insertOne](https://www.mongodb.com/docs/manual/reference/method/db.collection.insertOne/) operation. That operation inserts a single document into a collection. Let's start building this function from scratch as if it didn't exist already. So, here's its empty definition:

```go
//export insertOne
func insertOne(port int64, buffer *C.uchar, size int) {
    /// implement insertOne
}
```

The first thing we need to do is to receive the data that an `insertOne` operation needs to receive. Without wasting too much time going into the detail of how all the properties we need are going to be used, let's just identify them:

*   A key identifying which collection we are adding the document to.
    
*   An optional key identifying the session under which we are inserting the document.
    
*   The document to be inserted.
    

All this data is to be received within the array of bytes pointed by the `buffer` parameter. So, how is that array of bytes supposed to be encoded? It's time for a detour so we go into the use of protocol buffer definitions.

### Protocol Buffer Definitions

For each of the Go functions, we will have a corresponding Protocol Buffer message definition that defines the structure of the input array of bytes (the `buffer` parameter) and, optionally, another message definition that defines the structure of the response message Go should send back to Dart.

That is the case for the `insertOne` method. This function has the following associated message definitions, found in the [insert_one.proto](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/proto/insert_one.proto) file:

```plaintext
syntax = "proto3";

option go_package = "./mongo_stubs";

message InsertOneRequest {
  bytes collectionOid = 1;
  bytes sessionOid = 2;
  bytes document = 3;
}

message InsertOneResult {
  bytes insertedId = 1;
}
```

Let's call the syntax of this file `Proto3` as it is version 3 of the Protocol Buffers Interface Definition Language. A complete reference to this IDL syntax can be found at https://developers.google.com/protocol-buffers/docs/proto3. It is a big coincidence, not a requirement, that all the fields of `InsertOneRequest` are byte arrays. We can use any of the types supported by `Proto3`. I am trying to keep us focused on the interplay between Dart and Go and not so much on the specifics of the end goal of using MongoDB. However, it doesn't hurt to quickly explain the meaning of each field in `InsertOneRequest`:

*   `collectionId` is a key identifying the collection to which the document is to be added. We will not go into detail about how we are identifying connections, databases and collections. However, its type is a MongoDB [ObjectId](https://www.mongodb.com/docs/manual/reference/method/ObjectId/). Because there is no `Proto3` type to represent an ObjectId, we are using a byte array.
    
*   `sessionId` is an optional key identifying which session the insert operation is to be executed under. It is also an ObjectId.
    
*   `document` is the actual document. We were lucky that MongoDB uses [BSON](https://www.mongodb.com/json-and-bson) to encode documents. We were even luckier that the developers who created the `mongo_dart` package also created a package to encode and decode Dart objects to BSON and back, called [bson](https://pub.dev/packages/bson). As you might have guessed by now, a BSON-encoded document is an array of bytes, which is why this field is also an array of bytes.
    

If any of the fields were, for example, another message type, an `int32`, a `string`, or any of the [supported types](https://developers.google.com/protocol-buffers/docs/proto3#scalar), it would have made sense to represent it in that type instead of an array of bytes. An example of a message definition that uses less generic types than an array of types is the [connectionRequest](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/proto/connection_request.proto) message.

As previously mentioned, Protocol Buffer definitions are independent of any specific language. However, we get to generate representations for all the supported languages. For Go, all we need to do is run the following, from the context of the Go package's directory (this is also in the [Makefile](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/Makefile#L12)):

```sh
protoc --go_out=. -Iproto proto/*.proto
```

If you don't have `protoc` installed, you can follow the instructions at https://developers.google.com/protocol-buffers/docs/gotutorial#compiling-your-protocol-buffers.

As a result, `protoc` compiler will generate a corresponding Go struct for each proto message (we will also do the same thing for Dart, but later). In our example, execution `protoc` over the `InsertOneRequest` message will result in the following [struct](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/stubs/insert_one_request.pb.go):

```go
type InsertOneRequest struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	CollectionOid []byte          `protobuf:"bytes,1,opt,name=collectionOid,proto3" json:"collectionOid,omitempty"`
	Context       *RequestContext `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"`
	Document      []byte          `protobuf:"bytes,3,opt,name=document,proto3" json:"document,omitempty"`
}
```

Notice how each of the fields defined in the `Proto3` message is also represented in the Go struct. Let's go back to our `insertOne` implementation and add continue working on it. We can now add the code that converts the array of bytes we received from Dart back into an instance of an `InsertOneRequest` struct.

```go
//export insertOne
func insertOne(port int64, buffer *C.uchar, size int) {
	
	// lets first create a variable, 'request',
	// to hold the struct
	var request mongo_stubs.InsertOneRequest
	
	// and here we deserialize the contents of 'buffer'
	// into properties of 'request'
	ffi.Unmarshal(unsafe.Pointer(buffer), size, &request)
    
    // we're still not done yet. a lot will still be added here
}
```

`ffi.Unmarshal` receives the array of bytes, its size and a pointer to an instance of the `InsertOneRequest` struct, and fills the properties of that instance with the values contained in the array. In other words, it deserializes the array into the struct referenced by the `request` local variable. This function belongs to a group of utility functions that are useful anytime we will need to develop any Go library that exposes its interface to Dart using Protocol Buffers. For that reason, they were packed into a separate Go package called [dart_bridge](https://pkg.go.dev/gitlab.com/squarealfa/dart_bridge), which you can use for your own Go for Dart packages.

At this point, our `insertOne` method can receive all the data it needs to receive from Dart. If we need to receive additional data, all we need to do is add more fields to the `Proto3` definition and re-run the `protoc` compiler. From this point on, we will not be making any other use of the `buffer` variable. This means that we can spawn a Goroutine to do the rest of the work. After that, we can immediately return from this function (we expect that as soon as we return, the Dart caller will immediately free the memory it allocated for the array of bytes pointed by `buffer`). So, here's how the code will look after spawning an empty Goroutine:

```go
//export insertOne
func insertOne(port int64, buffer *C.uchar, size int) {
	var request mongo_stubs.InsertOneRequest
	ffi.Unmarshal(unsafe.Pointer(buffer), size, &request)

	// At this point, 'request' refers to a Go struct of type
	// mongo_stubs.InsertOneRequest. This struct instance
	// is garbage-collectable by Go when it will no longer
	// be needed.

	// Its contents resulted from the unmarshalling of the 
	// array of bytes pointed by buffer.
	// That array of bytes is not garbage-collectable and
	// its memory must be freed by this function's caller.

	go func() {

		/// This is where we will do the work
		/// of inserting a document in the database
		/// and returning a value to Dart

		/// It's ok and expected for us to use 'request' here. 
		/// But, we must never try to use the 'buffer' inside
		/// the go routine. Here we must assume the caller
		/// of this method has already freed its memory.
		
	}()

	/// after returning the caller is expected to free 'buffer'.
}
```

We will finally have this function do what it is meant to do, to insert a document in a collection. Even though this iteration of the method is the one that does what the method is intended to do, it is the least significant in terms of understanding how we are creating the interaction between Dart and Go. So, it's OK if you just retain from this iteration that the lines of code we added are the ones that do stuff and don't worry if you don't understand precisely what they are doing:

```go
//export insertOne
func insertOne(port int64, buffer *C.uchar, size int) {
	var request mongo_stubs.InsertOneRequest
	ffi.Unmarshal(unsafe.Pointer(buffer), size, &request)

	go func() {

		/// This is the bit of code that actually 
		/// does the action. 
        ///

		ctx := context.Background()

		/// here we are getting a reference to the collection
		/// from the CollectionOid we received
		oid := helpers.BytesToOid(request.CollectionOid)
		coll, err := cs.GetCollectionProxy(oid)
		if err != nil {
			/// [left out for now]
			return
		}

		/// next, we try to get a reference to the session
		trxProxy, err := _getSessionProxy(coll.databaseProxy.connectionProxy, request.SessionOid)
		if err != nil {
			/// [left out for now]
			return
		}

		/// Finally we do the actual insert of the document
		r, err := coll.InsertOne(ctx, trxProxy, request.Document)

		/// we still need to handle the result

	}()
}
```

We are almost done and we are returning to a very relevant part of the discussion. To finish the function we need to either return an error or otherwise return a result. To send a message back to Dart, we will be creating an instance of a Go struct that represents the message as it was defined in a `Proto3` definition. For this method, we intend that when things go well, we will be sending back to Dart an `InsertOneResult` as we've seen previously defined in [insert_one.proto](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/go/proto/insert_one.proto) file. Here we're repeating only the relevant snippet:

```plaintext
message InsertOneResult {
  bytes insertedId = 1;
}
```

As we've learned previously, the result of the compilation performed by `protoc` is that we end up having a corresponding Go struct for each message defined in our `Proto3` files. So, once again, we will have a Go struct called `InsertOneResult`. So, to finish our method we will either send an error or we will send an `InsertOneResult`. For brevity, we will not go into detail about how to send an error, but limit to mention that we also represent errors as any other message type.

Here's the final version of the function:

```go
//export insertOne
func insertOne(port int64, buffer *C.uchar, size int) {
	var request mongo_stubs.InsertOneRequest
	ffi.Unmarshal(unsafe.Pointer(buffer), size, &request)

	go func() {
		ctx := context.Background()
		oid := helpers.BytesToOid(request.CollectionOid)

		coll, err := cs.GetCollectionProxy(oid)
		if err != nil {
			/// Sending an error
			helpers.SendErrorMessage(port, err)
			return
		}
		trxProxy, err := _getSessionProxy(coll.databaseProxy.connectionProxy, request.SessionOid)
		if err != nil {
			/// Sending an error
			helpers.SendErrorMessage(port, err)
			return
		}
		r, err := coll.InsertOne(ctx, trxProxy, request.Document)
		if err != nil {
			/// Sending an error
			helpers.SendErrorMessage(port, err)
			return
		}

		/// The function called here translates
		/// whatever is present in 'r' (the return value
		/// from MongoDB) into an instance of InsertOneResult
		m, err := marshalling.ToInsertOneResult(r)
		if err != nil {
			/// Sending an error
			helpers.SendErrorMessage(port, err)
			return
		}

		/// Finally, we send the InsertOneResult
		ffi.SendMessage(port, m)
	}()
}
```

It is extremely important to retain that the Goroutine *must* send a message back to Dart. This is either accomplished by calling `helpers.SendErrorMessage(...)`, by calling `ffi.SendMessage(...)`, or otherwise by calling any other function that sends a message to Dart.

Once again, because `ffi.SendMessage(...)` is expected to be commonly reused, it is another feature of the `dart_bridge` Go package. It encapsulates the message into a [Response](https://gitlab.com/dartaculous/dart_bridge/-/blob/v1.0.2/proto/response.proto) message and, internally, uses the [SendUInt8ArrayToPort(...)](https://gitlab.com/dartaculous/dart_bridge/-/blob/v1.0.2/ffi/dart_api_dl.go#L57) method. Here's the implementation (you don't need to understand it to use the `SendUInt8ArrayToPort(...)` function as long as you understand the meaning of its parameters and what it is meant to do):

```go

// #include <stdlib.h>
// #include "stdint.h"
// #include "include/dart_api_dl.c"
//
///// [Other C functions here removed for brevity]
//
// bool GoDart_Send_UInt8Array(Dart_Port_DL port, uint8_t* values, int64_t length) {
// 	Dart_CObject obj;
//   obj.type = Dart_CObject_kTypedData;
//   obj.value.as_typed_data.length = length;
//   obj.value.as_typed_data.type = Dart_TypedData_kUint8;
//   obj.value.as_typed_data.values = values;
// 	return Dart_PostCObject_DL(port, &obj);
// }

import "C"
import (
	"unsafe"
)

//// [other functions removed for brevity]

func SendUInt8ArrayToPort(port int64, value []uint8) { 
	/// low-level code implementation that uses 
	/// Dart SDK to sends the contents of 'value' 
	/// back to Dart via the 'port'
	C.GoDart_Send_UInt8Array(C.int64_t(port), (*C.uint8_t)(unsafe.Pointer(&value[0])), C.int64_t(len(value)))
}
```

If you remember the previous discussion about [asynchronous execution](#asynchronous-execution), you might remember that for Dart to receive a return message from Go, Dart creates a ReceivePort and we send the ReceivePort's SendPort to Go. The `port` parameter of the `SendUInt8ArrayToPort` is precisely that SendPort. This is the very first parameter in all the exported Go functions and we've finally reached the place where it is used. It is `SendUInt8ArrayToPort`'s function to send an array of bytes to that port. In other words, it is this method that ends up sending data back to Dart.

### The Dart Side

So, we have a Go library that poses itself as a C library and now we need to consume it from Dart. This means we need to integrate that library into our Dart package. So, let's begin by looking at the parts that matter in our [pubspec.yaml file](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/pubspec.yaml):

```yaml

### all other elements were removed for clarity

dependencies: 
  ffi: ^2.0.1
  go_bridge: ^1.3.0
  bson: ^3.0.0
  # all other dependencies removed for clarity
  
dev_dependencies:
  ffigen: ^7.2.0
  # all other dev-dependencies removed for clarity

ffigen:
  name: LibMongoProxy
  description: MongoProxy GO driver
  output: 'lib/src/lib_mongo_go.dart'
  headers:
    entry-points:
      - go/mongo_go.h
```

These additions to `pubspec.yaml` add FFI and FFIGen packages and configure FFIGen. This configuration instructs the tool to generate a Dart file in lib/src/lib_mongo_go.dart containing the FFI definitions corresponding to the header file generated by Go when we compiled our Go package as a C library. To run Dart FFI, all we need to do is run the following command from the terminal:

```sh
dart run ffigen
```

Every time we change Go's exports, we should run the above command.

Just like we compiled all our `Proto3` messages into Go structs, we also need to compile the same `Proto3` messages into Dart classes. We can run the following command from the terminal to do so:

```sh
protoc --dart_out=./lib/src/gen -Igo/proto go/proto/google/protobuf/*.proto go/proto/*.proto
```

Every time we change a `Proto3` definition, we should run the command above.

We can now start looking at Dart code. Just like we have a facade on Go's side, it is a good idea to also have a corresponding Dart library on Dart's side. We centralize all the calls to Go in this library. In our implementation, it is [mongo_go.dart](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/lib/src/mongo_go.dart).

To continue our walk around this package, we will continue looking at how to call Mongo DB's `insertOne` operation. As you might have guessed, resulting from the above execution of `protoc`, one of the Dart classes we will end with will be [InsertOneRequest](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/lib/src/gen/insert_one.pb.dart).

The user's entry point is the [Collection.insertOne](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/lib/src/collection.dart#L45) method:

```dart
Future<InsertOneResult> insertOne(
    Map<String, dynamic> document, {
    Session? session,
  }) async {
    final bson = BSON();
    final bytes = bson.serialize(document);

    final result = await p.insertOne(
      collectionId,
      bytes,
      sessionOid: session?.sessionId,
    );
    final ret = InsertOneResult.fromProto(result);
    return ret;
  }
```

Even though we spread the features through classes where they made sense to the user, Collection, Database, Connection, and so on, the actual calls to Go are concentrated in one Dart library, [mongo_go.dart](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/lib/src/mongo_go.dart), which in the example is aliased with p. Let's look at a relevant slice of the `mongo_go.dart` library for this walk around:

```dart
final nl = getNativeLibrary();

void initialize() {
  nl.initialize(NativeApi.initializeApiDLData);
}

/// [other functions here removed for brevity]

Future<InsertOneResult> insertOne(
  ObjectId collectionOid,
  BsonBinary document, {
  ObjectId? sessionOid,
}) async {
  final oid = collectionOid.toByteList();
  final soid = sessionOid?.toByteList();

  final request = InsertOneRequest(
      collectionOid: oid, sessionOid: soid, document: document.byteList);
  final response = await callGoFunc(
    request: request,
    goFunc: nl.insertOne,
    responseToFill: InsertOneResult(),
  );
  return response;
}

/// [other functions here removed for brevity]
```

Let's begin at the very first line, `final nl = getNativeLibrary();`. It gets a reference to the Dart FFI proxy that represents the native library we built with Go and assigns it to the `nl` variable. That variable is needed by all other functions as the access point to call Go.

Here is the implementation of the `getNativeLibrary` function (present in the [helpers.dart](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/lib/src/helpers.dart) library):

```dart
LibMongoProxy getNativeLibrary() {
  final dylib = bridge.getDynamicLibrary('mongo_go.so');
  final nl = LibMongoProxy(dylib);
  return nl;
}
```

Back to `mongo_go.dart`, The very first function we implemented is the call to initialize:

```dart
void initialize() {
  nl.initialize(NativeApi.initializeApiDLData);
}
```

Remember, it is this call that will enable Go to send messages back to Dart. This could be called from anywhere, as long as it is called before any other function in `mongo_go.dart`. We could have left it to the packages consuming this one. However, because all interactions with this package will necessarily begin with a connection request, we placed the call to the `initialize` function inside the `connect` method of the `Connection` class :

```dart
  static Future<Connection> connect(ConnectionSettings settings) async {
	// here's the call to initialize:
    p.initialize();

	// let's ignore the rest and skip to insertOne
    final oid = await p.connect(settings.toConnectionRequest());
    final connection = Connection._(connectionId: oid);
    return connection;
  }
```

We are finally going to look at how we implemented the call to the `insertOne` function we created in Go. Here's the method's definition in `mongo_go.dart` again:

```dart
Future<InsertOneResult> insertOne(
  ObjectId collectionOid,
  BsonBinary document, {
  ObjectId? sessionOid,
}) async {
  final oid = collectionOid.toByteList();
  final soid = sessionOid?.toByteList();

  final request = InsertOneRequest(
      collectionOid: oid, sessionOid: soid, document: document.byteList);
  final response = await callGoFunc(
    request: request,
    goFunc: nl.insertOne,
    responseToFill: InsertOneResult(),
  );
  return response;
}
```

Note how we are creating an instance of `InsertOneRequest`, the class that resulted from the compilation of the corresponding `Proto3` definition done by `protoc`.

The call to Go is then entirely done by a method called `callGoFunc`. It receives the object to be serialized to protocol buffers, receives the native Go function to be called and an object to be filled in (by deserializing it from protocol buffers) in case the function runs successfully (I will continue skipping error handling for this explanation). Even though the `callGoFunc` we are seeing is specific to this package, and it does error handling specific to this package, it, internally, calls a method with the same name from the [Go Bridge](https://pub.dev/packages/go_bridge) package that finally does the actual call to Go. All the calls we are doing to Go are following this pattern. Notice that this code doesn't worry about any FFI details as we've been able to sweep all of those worries under the Go Bridge package. Here's the signature of the final [callGoFunc](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/go_bridge/lib/helpers.dart#L71) that does the call to Go:

```dart
Future<TResponse> callGoFunc<TResponse extends GeneratedMessage>({
  required GeneratedMessage request,
  required void Function(int port, Pointer<UnsignedChar> buffer, int size)
      goFunc,
  required TResponse responseToFill,
  List<GeneratedMessage>? errorsToThrow,
}) {
	/// [Implementation left out of brevity]
}
```

It is within the implementation of this method that we can find the usage of a `ReceivePort` and a `Completer` as discussed previously under [asynchronous execution](#asynchronous-execution). This method also takes care of the memory allocation and deallocation of the array of bytes to be sent to Go. Because it's all inside the `go_bridge` package, these are all concerns you will no longer need to worry about to implement this approach as long as you use that package.

### The Complete Workflow

Putting it all together, we finally get the following workflow:

![complete execution workflow](https://gitlab.com/dartaculous/dartaculous/-/raw/ff0ff5d93675ab8539d31ad7a398fecd325e73b4/mongo_go/docs/complete_sequence.png align="left")

So, in a nutshell, what the callGoFunc does is:

*   Creates a completer that is completed with a message from a ReceivePort
    
*   Serializes the data to be sent with the request to a protobuffer array (and copies that array into a malloc allocated buffer).
    
*   Calls the Go function via FFI, sending it the sendPort of the ReceivePort, the buffer and its length (that is why every single Go function I am exporting has these exact 3 parameters).
    
*   The Go function deserializes the buffer into a Go struct, spawns a go routine, passes it the port and the Go struct containing the data of the request, and returns.
    
*   The Dart method cleans up the memory it previously allocated to store the buffer and awaits the completers.
    
*   In the meantime, when the go routine finishes doing whatever it needs to do, it uses the Dart SDK to send a completion message to the sender port it received.
    
*   When the Dart method's completer is complete, it either returns the result or throws an error if the received message represents an error.
    

## Streams

Some calls to Go may return big results. This may be the case with the [find](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/lib/src/collection.dart#L286) and [aggregate](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/mongo_go/lib/src/collection.dart#L507) methods. For such methods, there is a variant of the callGoFunc method called [getGoStream](https://gitlab.com/dartaculous/dartaculous/-/blob/mongo_go-v5.1.0/go_bridge/lib/helpers.dart#L99). This method does not return a single callback from Go, but a sequence of callbacks, from which it creates a Stream, thus creating the following workflow:

![Complete Workflow with Streams](https://gitlab.com/dartaculous/dartaculous/-/raw/ff0ff5d93675ab8539d31ad7a398fecd325e73b4/mongo_go/docs/stream_sequence.png align="left")

## Conclusion

If we take into account the size of this article, this might seem to be a complex design. However, that apparent complexity might be because of the great level of detail I went through. In practice, it took me a whole lot longer to come up with the ideas and implement the workflow (which is now reusable), than to implement the `mongo_go` MongoDB driver itself. After having everything set in place, the development of the driver was very speedy.

Hopefully, this approach would be a catalyst for the growth of Dart packages meant to be used on the server by reusing existing Go SDKs.
