Sunday, May 20, 2018

Writing Google Cloud Functions in C

Serverless is a new hot topic in the IT world. As with every new technology, the idea was obvious in theory (easy way of migrating software without live migration of whole virtual machines), but very few people knew how to implement it in practice. Today we have AWS Lambda, Google Cloud Functions and Azure Functions, and will probably see many other in the near future.

Google Cloud is a technology I currently work with, and Google Cloud Functions are a very handy tool. If you just need to create a simple RESTful service, you don't have to write a backend application and set up a Kubernetes cluster to run it. All you need to do is to write a piece of code which handles a request and sends a response, and the Cloud Functions will take care of all the dirty work. They free you from the burden of deploying, monitoring, scaling and load balancing your application. You can focus entirely on writing software.

As always, the comfort comes at a price. Currently the only programming language supported by Cloud Functions is JavaScript. Does it mean that you cannot use a library written in another language? If you have a nice piece of C code and want to use it, are you doomed to manually build an infrastructure based on Compute Engine instances with Linux and Apache? Fortunately, the answer is no.

A few years ago in my post about JavaScript I described Emscripten, a C/C++ to JavaScript compiler. It has matured since, and it can produce a very good and stable code. Let me show you an example of how it can be used to create a Cloud Function from an ANSI C code.

Let's start with a simple program to display current time and date:
#include <stdio.h>
#include <time.h>

const char *now() {
  time_t t = time(NULL);
  return ctime(&t);
}

int main() {
  printf("%s\n", now());
  return 0;
}
Now let's convert it to JavaScript using Emscripten:
emcc -O2 -o main.js main.c
and run it with Node.js:
node main.js
To make our program run with Cloud Functions we need to make a separate file (I called it extern.c) containing only now() function:
#include <stdio.h>
#include <time.h>
#include "emscripten.h"

EMSCRIPTEN_KEEPALIVE
const char *now() {
  time_t t = time(NULL);
  return ctime(&t);
}
As you can see, there are some changes to the original source code:
- there is no main() function
- there is additional #include "emscripten.h" directive
- the now() function is prefixed with EMSCRIPTEN_KEEPALIVE. It tells the compiler that the code is in use and should not be removed during compilation (the optimizer tries to identify and remove dead code).

Let's build an external library from our code:
emcc -O1 -s 'EXTRA_EXPORTED_RUNTIME_METHODS=["ccall", "cwrap"]' -o extern.js extern.c
This creates a module extern.js, which can be included into a Node.js application:
var module = require('./extern.js');
var now = module.cwrap('now', 'string', []);

console.log(now());
Module.cwrap returns a function wrapper, which calls our code exported from C. The first argument is the name of the C function, the second one is the type returned by the function, and the third one contains the list of the argument types passed to it.

A word of warning: When using Node.js, you can use ccall instead of cwrap, as described in Emscripten documentation. The difference is that ccall doesn't return a function, but a scalar value, so you will log it with console.log(now) instead of console.log(now()). However, ccall will not work correctly with current version of Google Cloud Functions - module.ccall('now', 'string', []) will always return the time when the Cloud Function was created instead of current one. Also, don't use optimization levels higher than O1 when compiling external modules with Emscripten.

Because Google Cloud Functions are powered by Node.js, it is very easy to convert our Node.js application into a Cloud Function:
var module = require('./extern.js');
var now = module.cwrap('now', 'string', []);

exports.time = (req, res) => {
  res.send(now());
}
Save the code above as index.js and put it into a zip file together with extern.js (you can find an example file here). Now go to Google Cloud Platform panel, choose "Cloud Functions" and click "Create function". Change the default function name to "time". Leave memory allocation settings unchanged. 256MB may seem an overkill, but changing it to a lower value may actually add a performance penalty to your application. Underneath the Cloud Functions there are still virtual instances running, and low-memory virtual machines get less CPU time. Set the trigger type to "HTTP trigger" - it means that the function will be run on demand when an http request comes. Pay attention to the URL field - it's the address you can use to call your function. Choose "ZIP upload" as the source of the code. You will need a Bucket as a temporary upload storage - you can create it through the "Storage" menu in Google Cloud Platform panel. Finally, when asked about function to execute, enter "time":
Creating a function takes a while. When it's ready, you can go to the URL which was displayed when the function was created (for example https://us-central1-myproject.cloudfunctions.net/time) and you should see the current time in response.

You can call the Cloud Functions from a website with AJAX. What I mean is that for example you can create a page displaying a simple clock, which will refresh every second (remember to change the url https://us-central1-myproject.cloudfunctions.net/time to yours):
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> 
<script src="jquery.min.js"></script>
<script>
$(document).ready(function() {
  setInterval(function() {
    $.ajax({
      url: 'https://us-central1-myproject.cloudfunctions.net/time',
      method: 'GET',
      crossDomain: true,
      success: function(data) {
        $('#time').text(data);
      }
    });
  }, 1000);
});
</script>
</head>
<body>
<p id="time"></p>
</body>
</html>
This alone, however, will not work, due to cross-origin protection mechanisms. In short, a web browser will not allow AJAX request from your website to an endpoint located in a different domain, if it does not explicitly allow it. To fix it, you need to add Access-Control-Allow-Origin response headers to index.js:
var module = require('./extern.js');
var now = module.cwrap('now', 'string', []);

exports.time = (req, res) => {
  res.set('Access-Control-Allow-Origin', 'http://www.mypage.com');
  res.set('Access-Control-Allow-Methods', 'GET');
  res.send(now());
}
Replace http://www.mypage.com with the domain name of your website where the clock page is located.

No comments: