QBASIC meets Azure Functions

Before we continue, I'd like to make a clear disclaimer:

This is a bad idea, and under no circumstances would you want to run a program built with a thirty-one year old legacy programming language in production.

Wait, what about JavaScript, which is around for over twenty-seven years and is fully supported in Azure Functions? Regardless, I thought it would be cool to go to the ultimate extreme and run code written in QBASIC as an Azure Function.

Luckily, bringing legacy code into the serverless world is made possible using Custom Handlers, a feature of Azure Functions introduced a while ago, which allows for pretty much any executable to serve as a handler, as long as it runs as a HTTP server listening on a specific port, and understands the HTTP constructs.

TL;DR

Here is a quick demo of what this post is all about:

The source code is available on GitHub
GitHub - faniereynders/serverless-qbasic
Contribute to faniereynders/serverless-qbasic development by creating an account on GitHub.

According to the documentation, custom handlers are best suited for implementing a function app in a language not supported out-of-the-box, such as Go, Rust or even QBASIC :) and the mechanism works by either-

  1. sending an envelope message wrapping the complete context of the request to a POST endpoint (which matches the name of the function), and expecting a specific message structure back as the response from the handler for other bindings downstream, or
  2. acting like a transparent proxy to pass through the original request to the external handler and returning the raw response as is.
The default behaviour is wrapping the request as per point 1 above, and although one could shamelessly use custom handlers as a reverse proxy, it is however not built for that purpose and some features like HTTP/2 and Web Sockets are not supported, and one are only limited to HTTP bindings (for now).

The experiment

As explained in the video we have a custom handler written in BASIC that listens to a specific port on start-up and relays requests to a /greet endpoint:

FUNCTION doServer(BYVAL Port AS USHORT) AS INTEGER
  VAR server = NEW nettobacServer(Port) 
  WITH *server
    WHILE 0 = LEN(INKEY())
    
      '... omitted for brevity
      
        IF LEN(dat) ANDALSO newData(con, dat) THEN EXIT WHILE
      NEXT : SLEEP 10
    WEND
  END WITH
  DELETE server : RETURN 0
END FUNCTION

FUNCTION newData(BYVAL Con AS n2bConnection PTR, BYREF Dat AS STRING) AS INTEGER

  DIM httpmethod AS string = StrReplace(Dat,mid(Dat, INSTR(Dat," ")),"")

  SELECT CASE httpmethod
  CASE "POST"
  
    dim body as string = Mid(Dat, INSTR(Dat,"{"))
    dim payload as jsonItem = JsonItem(body)
    dim query as jsonItem = JsonItem(payload["Data"]["req"]["Query"])
    dim greetRequest as string = "/greet"

    IF MID(Dat, 6, len(greetRequest)) = greetRequest THEN
      dim result as jsonItem = jsonItem()
      dim outputs as jsonItem = jsonItem()
      dim body as jsonItem = jsonItem()
      dim queueMessage as jsonItem = jsonItem()

      body.addItem("message", "Hello, " & urlDecode(query[0].value) & "!")
      queueMessage.addItem("message", "Hello from Queue, " + urlDecode(query[0].value) + "!")

      outputs.addItem("res", body)
      outputs.addItem("queue", queueMessage)

      result.addItem("Outputs", outputs)
      result.addItem("ReturnValue", body)
      
      Con->nPut(HTTP(200, result.toString()))
    ELSE
      Var e = "{""error"": ""not found""}"
      Con->nPut(HTTP(404, e))
    END IF

  '... ommited for brevity
  
  END SELECT : RETURN 0
END FUNCTION

END doServer(val(Command(1))) 'pass the first argument as the port

The application above is compiled to a single executable using FreeBASIC and is included into the Azure Function app as shown below:

| /greet
|   function.json
| host.json
| local.settings.json
| handler.exe
Note that the endpoint /greet in the code preceding code snippet matches the folder name "greet" which denotes the function name and should match.

The function.json contains all the binding information, and as per the example in the video, it contains an input binding to HTTP requests, and output bindings to HTTP responses and Azure Storage Queues:

{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    },
    {
      "type": "queue",
      "name": "queue",
      "direction": "out",
      "queueName": "data",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

It is important to note that the FUNCTIONS_WORKER_RUNTIME property is set to custom in the local.settings.json file:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "custom",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true"
  }
}

In the host.json file, we need to tell Azure Functions where to find the custom handler:

Note that enableForwardingHttpRequest is set to false because we don't want to proxy to the handler directly.
{
  "version": "2.0",
  //...
  
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[2.*, 3.0.0)"
  },
  
  "customHandler": {
    "enableForwardingHttpRequest": false,
    "description": {
      "defaultExecutablePath": "handler.exe",
      "workingDirectory": ".",
      "arguments": [
        "%FUNCTIONS_CUSTOMHANDLER_PORT%"
      ]
    }
  }
}

As you can see from the host.json file above, the port to listen on for the custom handler is specified by passing it as an argument using the environmental variable %FUNCTIONS_CUSTOMHANDLER_PORT%.

We can run the Function App by executing the following command in the terminal:

func start

This will start the host and listen for GET and POST HTTP requests on the endpoint provided by the host called /api/greet. When a request is received, it will trigger the configured input HTTP binding, run the handler, and relay the response as an output HTTP response as well as sending a message on the storage queue.

Be sure to check out the documentation of custom handlers on the Azure Functions documentation site, and if you have not done it yet, please subscribe to my channel on YouTube.

The source code of this experiment is available on GitHub.