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
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-
- 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
- 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 thatenableForwardingHttpRequest
is set tofalse
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.