Here I will describe how I built a TCP Server with protobuf and NaCL using ASSA ABLOY saltchannel implementation.
This is hyperthetical used for communication between IoT devices and backend services.
This description is an idea, a concept of how to use go and c together.
My application will integrate saltchannel using its own library and some Go wrappers.
Directory layout would look something like this
| gatewayserver/
| -------------/saltwrapper/
| -------------/-----------/export.go
| -------------/-----------/salt_wrapper.go
| -------------/-----------/type.go
| -------------/-----------/salt_wrapper.h
| -------------/-----------/salt_wrapper.c
| -------------/main.go
| -------------/handle.go
| -------------/conn.go
The idea of this hyperthetical application is to communicate with an IoT device another group is building.
The idea is the IoT device would communicate using Protobuf to a backend service and the backend service would deliver information to the IoT device for whatever needs it would want.
Possible use-cases for this device would be a remote key, monitoring of gauges, etc.
To make this work we would need to download and build the salt-channel-c library from Github, then to make that happen we choose libsodium, at the moment we just have everything in our Golang project.
Our main (Go) application would start a net. Listener on a port of your choice, pass this off to our wrapper, an example code of this would be something like this
localAddr, _ := net.ResolveTCPAddr("tcp", ":7777")
// Check for error in your own code, for my sanity I will skip all error checking if not strictly needed for logic, when I'm explaining stuff.
listener, _ := net.ListenTCP("tcp", localAddr)
// acceptance loop for incoming connections
for {
conn, _ := listener.AcceptTCP()
go handleConnection(conn)
}
This should be pretty self-explaining, we try to resolve a port on our machine, we get back the object pass it to our Listener which in turn will become a socket on our O/S and we start a main loop just waiting for incoming connections.
In our loop, when we receive an incoming connection, we pass it to our go routine. A side-note you could do something better or clever, but my choice is naivë solution.
Our handleConnection function starts up the salt-channel library by using a pre-defined saltwrapper. SaltServer(conn) and then we use the library in another for-loop to read the bytes going over the wire.
func handleConnection(conn *net.TCPConn) {
defer conn.Close()
sc, _ := saltwrapper.SaltServer(conn)
fromReadChannel := make(chan []byte, 2) // buffered channel
LOOP:
for {
iotPublicKey := sc.SaltGetKey() // The device will present its Public Key
_, err := sc.SaltReceiveMessage() // we check if we can receive from the salt-channel
if err != nil {
break LOOP
}
buffer := <-fromReadChannel
var request iotdevice.Request // this would the Protobuf as golang source code
proto.Unmarshal(buffer, &request)
switch request.Action.(type) {
case iotdevice.AnAction1:
// We do not really care about these actions for this article
case iotdevice.AnAction2:
// We do not really care about these actions for this article
}
}
}
Above code describe with much simplicity the action of our application.
Now we will turn our focus on the salt-channel wrapper implementation and how we integrate into go.
As described by the beautiful directory layout ASCII art, we know we are going to have a folder inside our Go project that will include all the wrapper material to communicate between C and Go.
As salt-channel would have a server and client mode we need two functions for that.
We do that by creating a type SaltChannel and depending on the need we have two constructors that will return a SaltChannel with the proper mode configuration.
The salt-channel-c implementation would have a Read, Send, Disconnect, GetKey (public key)
type SaltChannel struct {}
func (sc *SaltChannel) Read(b []byt) (int, error) {}
func (sc *SaltChannel) SaltSendMessage(b []byte) (int, error) {
return sc.Write(b)
}
func (sc *SaltChannel) Write(b []byte) (int, error) {
buffer := C.CBytes(b)
ptr := (C.salt_handle_t)(sc.Ptr)
rv := C.salt_send_message(ptr, (*C.uint8_t)(buffer), (C.uint16_t)(len(b)))
if rv != C.SALT_SUCCESS || C.salt_error(ptr) != C.SALT_ERR_NONE {
return 0, fmt.Errorf("sendmessage error: %d %d\n", rv, C.salt_error(ptr))
}
return len(b), nil
}
func (sc *SaltChannel) SaltReceiveMessage(outCh chan []byte) (int, error) {
var offset C.uint16_t
var msglength C.uint16_t
ptr := (C.salt_handle_t)(sc.Ptr)
tmp := make([]byte, 1600) // 1600 is a bit bigger than what SaltChannel needs, I think they require 1522 bytes.
bufferC := C.CBytes(tmp) // Here we allocate a C byte array
rv := C.salt_receive_message(ptr, (*C.uint8_t)(bufferC), (C.uint16_t)(1600), &offset, &msglength)
if rv != 0 || C.salt_error(ptr) != C.SALT_ERR_NONE {
return 0, fmt.Errorf("some clever message")
}
b := C.GoBytes(bufferC,1600) // here we translate back to Go bytes
buf := b[offset: offset+msglength]
outCh <- buf
return (int)(len(buf)), nil
}
// Freeloader function (global) disconnect
func SaltDisconnect() {
C.salt_disconnect(nil)
}
// Constructors for SaltChannel
func SaltServer() *SaltChannel {}
func SaltClient() *SaltChannel {}
As our folder includes .c and .h files we would need to enable C compilation from our .go source code, at the very top of our salt_wrapper.go we would include GCC compilation options.
We also include some proxy functions to pass data between C and Go.
/*
#cgo CFLAGS: -I${SRCDIR}/../salt-channel-c/src
#cgo LDFLAGS: -L${SRCDIR}/../ -lsalt
#cgo LDFLAGS: -lsodium
#include "salt_wrapper.h"
// These will be defined in Go and used in C
extern salt_ret_t ProxyWrite(void*, const uint8_t *, uint16_t);
extern salt_ret_t ProxyRead(void*, const uint8_t *, uint16_t);
uint16_t socket_write(void *socket, const uint8_t *buffer, uint16_t size)
{
return ProxyWrite(socket, buffer, size);
}
uint16_t socket_read(void *socket, const uint8_t *buffer, uint16_t size)
{
return ProxyRead(socket, buffer, size);
}
*/
import "C"
As you probably can see, you need to move data back and forth between Go and C layers, and to point out where the required files are for the compiler to do its job.
CGO is nice when you have a unified C library your company is using and building, so all have the same interface and expectations, but it is not quite Go.