Skip to content

Instantly share code, notes, and snippets.

@coder-mike
Last active May 26, 2022 22:02
Show Gist options
  • Save coder-mike/be2262e7e1fad7e53fdd9a95f0eb9188 to your computer and use it in GitHub Desktop.
Save coder-mike/be2262e7e1fad7e53fdd9a95f0eb9188 to your computer and use it in GitHub Desktop.
A toy example to illustrate how callback-based async code might look in Microvium

Toy Example of callback-based code in Microvium

See the corresponding blog post here.

The main objective here is to write a sendToServer function that is an example of an operation that hypothetically takes 3 steps to complete: powering on the modem, connecting to the server, and sending the data. In the spirit of writing non-blocking, single-threaded code, this uses callbacks to trigger the next step when the previous completes.

This example is written to run on the Microvium JavaScript engine, so it doesn't use any features that Microvium doesn't support.

The example is not fully comprehensive. For example, it doesn't include error checking or timeouts. It doesn't check if the modem is powered on, but just powers it on each time (it also doesn't power it off). But it's easy to imagine how modem.powerOn() could be changed to if (alreadyPoweredOn) ... , etc.

The example code here is structured into 4 layers:

  • main.mjs is just an example usage of the sendToServer function.
  • send-to-server.mjs is the implementation of sendToServer, basically the same as what was shown in the blog post.
  • modem.mjs implements a JavaScript wrapper around the C host firmware functions that actually control the modem.
  • events.mjs is an example event system to translate events from the host firmware into callback calls. This is not specific to the modem code but could underly all events (there is no mention of "modem" in event.mjs).

(in this gist I've prefixed the filenames with numbers just to keep them in order of these 4 layers)

The key point to notice in this example is that the implementation of modem.powerOn (and the other methods) do not block while the modem powers on. Instead, it starts off the process using startModemPowerOn and then waits for call from the host firmware to say when the modem is on. During this waiting, the VM is idle and the VM call stack is freed.

When the host firmware calls the JS method eventReceivedFromHost('modem-powered-on') then the VM wakes up, allocates the call stack, and triggers the next thing to be done

  • eventReceivedFromHost('modem-powered-on') calls powerOnCallback
  • powerOnCallback calls modem.connectTo
  • modem.connectTo calls the host firmware function startModemConnectTo
  • Then the VM goes idle again until the next event.

The main intention of this example is to communicate how easy it is to write the high-level, single-threaded, non-blocking logic such as that in sendToServer, compared with doing the same thing in C.

import { sendToServer } from './send-to-server.mjs
// Example usage
sendToServer("example.com/api", "Hello, World!");
import { modem } from './modem.mjs'
/**
* Function to send data to the server. Sends the given data to
* the server and then calls the callback when finished.
*/
export function sendToServer(url, data, callback) {
// Power the modem on and call powerOnCallback when done
modem.powerOn(powerOnCallback);
function powerOnCallback() {
// Connect to the URL and call connectedCallback when done
modem.connectTo(url, connectedCallback);
}
function connectedCallback() {
// Send data to the server and call callback when done (the
// final callback to say that sendToServer is complete)
modem.send(data, callback);
}
}
// Or it could be written using arrow functions, if that's familiar to you:
export function sendToServer(url, data, callback) {
modem.powerOn(() =>
modem.connectTo(url, () =>
modem.send(data, callback)));
}
import { onceEvent } from './events.mjs'
// Access 3 host firmware functions to actually do the work,
// at least for this illustration. I'm assuming here that the pre-aggreed
// IDs for these are 42, 43, and 44 respectively.
const startModemPowerOn = vmImport(42);
const startModemConnectTo = vmImport(43);
const startModemSendData = vmImport(44);
export const modem = {
// Power-on method
powerOn(callback) {
startModemPowerOn();
onceEvent('modem-powered-on', callback);
},
// Connect to URL method
connectTo(url, callback) {
startModemConnectTo(url);
onceEvent('modem-connected-on', callback);
},
// Send-data method
sendData(data, callback) {
startModemSendData(url);
onceEvent('modem-connected-on', callback);
}
}
const eventHandlers = {}
// To be called by host firmware each time something interesting happens
function eventReceivedFromHost(eventName, info) {
// Find all the handlers associated with this event name
const handlers = eventHandlers[eventName];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handler(eventName, info);
}
}
}
function addEventHandler(eventName, callback) {
let handlers = eventHandlers[eventName];
if (!handlers) {
handlers = [];
eventHandlers[eventName] = handlers;
}
handlers.push(callback);
}
function removeEventHandler(eventName, callback) {
const handlers = eventHandlers[eventName];
if (!handlers) return;
let newCount = 0;
// Remove handler from array
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] !== callback)
handlers[newCount++] = handlers[i];
}
handlers.length = newCount;
}
// Register the function `eventReceived` with some numeric ID for the host
// firmware to call it using `mvm_call`
vmExport(1234, eventReceivedFromHost);
/**
* Function to register a callback to be called to be invoked
* exactly once when an event is received from the host.
*/
export function onceEvent(eventName, callback) {
addEventHandler(eventName, innerCallback);
function innerCallback(eventName, arg) {
// Since this only calls the callback once, we deregister it
removeEventHandler(eventName, innerCallback);
// Call the callback
callback(eventName, arg);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment