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:
lukaszbudnik / cloud-native-c
cloud-native-c shows how to use AWS Go SDK from C using cgo
The Makefile
is really simple, but I'm going to quickly review it:
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:
#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 thePutObject()
,GetObject()
andFree()
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:
// #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) orC.free()
(method to release allocated memory), etc. - exposing any types/methods/directives defined in custom response.h header as C. For example:
C.SUCCESS
orC.FILE_UPLOAD_ERROR
. - exposing special cgo methods as C. For example to convert a
*C.char
to gostring
you can use:C.GoString()
function, to create a C string (*C.char
) you can useC.CString()
, etc. Check cgo documentation for more.
Two points to note.
- It's much easier to convert types in Go then in C. So my
PutObject()
,GetObject()
, andFree()
functions take and return C types. Thanks to this I call these functions from C just like they were native C functions. - Important thing is that all the memory allocated by
C.*
functions needs to be freed usingC.free()
function. That is why I also exposedFree()
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.
//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:
$ 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!
Top comments (0)