Cloud native C
Tags: go, c, cgo, cloudnative
Published: 2021-08-19 18:37:53

## Low level cloud SDKs

When you think about writing a cloud native app you probably think about higher level programming languages like Java, JavaScript, .NET, Python, Ruby, etc.

But what if you have an old C app? And you are preparing to either rehost it or even better replatform it?

Well, none of the main cloud providers offer C SDKs. AWS goes as low as C++. Which is nice. 

But! All main cloud providers actually do offer Go SDKs. And with Go and its cgo tool we are already at home.

## c + go = cgo

I have prepared a really simple C application which calls Go functions to upload and download an object to/from AWS S3 bucket.

The code is uploaded to my GitHub repo:

{% github lukaszbudnik/cloud-native-c no-readme %}

The `Makefile` is really simple, but I'm going to quickly review it:

```make
OS:= $(shell uname -s)

ifeq ($(OS), Linux)
	build_cmd = gcc -pthread -o app app.c storage.a
endif
ifeq ($(OS), Darwin)
	build_cmd = gcc storage.a app.c -framework CoreFoundation -framework Security -o app
endif

all: app

app: storage app.c
	$(build_cmd)

storage: storage.go
	go build -buildmode=c-archive storage.go
```

The `storage` rule compiles and builds the `storage.go` as a C library. It generates 2 artifacts:
- `storage.h` - which contains definitions needed by the C program to call Go functions
- `storage.a` - compiled C library

Other than that `storage.go` is just a normal go program (it has to have an empty `main` function). It also uses `go.mod` to manage dependencies (AWS SDK Go v2 libraries). 

The `app` rule compiles and builds the `app.c` application. As there are small differencies between `gcc` on Mac and Linux I'm passing different paramters to it. For MacOS it links CoreFoundation and Security frameworks and for Linux it links pthread library. Both MacOS and Linux are built automatically on GitHub using actions running on `macos-10.15` and `ubuntu-20.04`.

_Edit: Support for Windows 2019 with MinGW was also added. Windows binaries are also built automatically on GitHub using `windows-2019` runner. See repo for more details._

## Source code - C

How does the application look like then?

First let's take a look at the C code:

```c
#include <stdio.h>
#include <string.h>
#include "response.h"
#include "storage.h"

int main(int argc, char **argv)
{
  if (argc != 3)
  {
    fprintf(stderr, "This app requires exactly 2 parameters first is the path to local file and second is the S3 bucket.\n");
    fprintf(stderr, "Example:\n%s app.c my-bucket-name\n", argv[0]);
    return WRONG_PARAMETERS_ERROR;
  }

  int res = PutObject(argv[1], argv[2]);

  if (res > 0)
  {
    fprintf(stderr, "There was an error uploading the file.\n");
    return res;
  }

  char *content = GetObject(argv[1], argv[2]);
  if (content == NULL)
  {
    return FILE_DOWNLOAD_ERROR;
  }

  printf("Content of the uploaded file is:\n%s\n", content);
  Free(content);

  return SUCCESS;
}
```

Nothing special. I included two header files. 

* `storage.h` - generated by cgo, contains C definitions of the `PutObject()`, `GetObject()` and `Free()` functions.  These functions take and return C data types.
* `response.h` - contains definitions of success and error codes. This header file is also included in Go application.

As you can see, the above source code is super simple, plain old C.

## Source code - Go

Now let's take a look at the Go code.

The go code is almost plain go apart of these 4 lines:

```go
// #include <stdio.h>
// #include <stdlib.h>
// #include "response.h"
import "C"
```

The above directives are responsible for:
* exposing any types/methods/directives defined in stdio and stdlib as C. You can reference them as: `C.char` (char data type) or `C.free()` (method to release allocated memory), etc.
* exposing any types/methods/directives defined in custom response.h header as C. For example: `C.SUCCESS` or `C.FILE_UPLOAD_ERROR`.
* exposing special cgo methods as C. For example to convert  a `*C.char` to go `string` you can use: `C.GoString()` function, to create a C string (`*C.char`) you can use `C.CString()`, etc. Check [cgo](https://pkg.go.dev/cmd/cgo) documentation for more. 

Two points to note.

1. It's much easier to convert types in Go then in C. So my `PutObject()`, `GetObject()`, and `Free()` functions take and return C types. Thanks to this I call these functions from C just like they were native C functions.
2. Important thing is that all the memory allocated by `C.*` functions needs to be freed using `C.free()` function. That is why I also exposed `Free()` function so that all objects which were returned from Go to C app can be send back to Go to be freed.

Go lang app looks like this. The `//export Function` are required otherwise cgo won't include them in `storage.h` header file.

```go
//export PutObject
func PutObject(filenameC, bucketC *C.char) int {
	filename := C.GoString(filenameC)
	bucket := C.GoString(bucketC)

	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		fmt.Fprintf(os.Stderr, "configuration error: %v\n", err.Error())
		return C.CONFIGURATION_ERROR
	}
	(...)
}
//export GetObject
func GetObject(keyC, bucketC *C.char) *C.char {
	key := C.GoString(keyC)
	bucket := C.GoString(bucketC)
	(...)
	return C.CString(content)
}
//export Free
func Free(ptr *C.char) {
	C.free(unsafe.Pointer(ptr))
}
```

## Building and running

In order to build and run the application just call:

```shell
$ make all
go build -buildmode=c-archive storage.go
gcc storage.a app.c -framework CoreFoundation -framework Security -o app
$ ./app app.c my-bucket-name
File uploaded correctly, version id: Uzp7G2ehns7AtQdwpPrQoTQriXKbeldA
File version id: Uzp7G2ehns7AtQdwpPrQoTQriXKbeldA
Content of the uploaded file is:
#include <stdio.h>
#include <string.h>
#include "response.h"
#include "storage.h"
(...)
```

You are now well equipped to start coding cloud native apps in C!