In my last post, we started implementing a minimal Smalltalk-like language by creating message data structures corresponding to each of the functions in our little demo slice of the Arduino Audio library API, specifically just the SGTL5000 audio codec controller class. The next step in implementing an object-oriented language is really the core of our language semantics: message dispatch. We communicate with an object by sending it messages, and then it chooses freely and without restriction what implementation, algorithm, or behavior to manifest when it receives the messages. This is called message dispatch. We will implement a very simple message dispatch system where upon receiving each message the object just invokes the corresponding function. So, for instance, the AudioControlSGTL5000_ENABLE message will call the AudioControlSGTL5000.enable() function. Not very exciting, I suppose, but it will get the job done!
Here is the code generator:
import Foundation
extension CppGenerator
{
public func generateModule(_ input: URL, _ output: URL)
{
do
{
let source = try String(contentsOf: input)
let includeFile = input.lastPathComponent
let className = try self.parser.findClassName(input, source)
let functions = try self.parser.findFunctions(source)
guard functions.count > 0 else
{
return
}
try self.generateModuleHeader(output, includeFile, className, functions)
try self.generateModule(output, className, functions)
}
catch
{
print(error)
}
}
func generateModuleHeader(_ outputURL: URL, _ includeFile: String, _ className: String, _ functions: [Function]) throws
{
print("Generating \(className)Module.h...")
let outputFile = outputURL.appending(component: "\(className)Module.h")
let result = try self.generateModuleHeaderText(includeFile, className, functions)
try result.write(to: outputFile, atomically: true, encoding: .utf8)
}
func generateModuleHeaderText(_ includeFile: String, _ className: String, _ functions: [Function]) throws -> String
{
let date = Date() // now
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
let dateString = formatter.string(from: date)
let moduleName = self.makeModuleName(className)
let requestName = self.makeRequestName(className)
let responseName = self.makeResponseName(className)
return """
//
// \(className)Module.h
//
//
// Created by Clockwork on \(dateString).
//
#ifndef \(className)Module_h_
#define \(className)Module_h_
#include <Arduino.h>
#include "Audio.h"
#include "\(className)Messages.h"
class \(moduleName)
{
public:
\(moduleName)(\(className) *component) : logic(component) {}
\(className) *logic;
\(responseName) *handle(\(requestName) *request);
};
#endif
"""
}
func generateModule(_ outputURL: URL, _ className: String, _ functions: [Function]) throws
{
print("Generating \(className)Module.cpp...")
let outputFile = outputURL.appending(component: "\(className)Module.cpp")
let result = try self.generateModuleText(className, functions)
try result.write(to: outputFile, atomically: true, encoding: .utf8)
}
func generateModuleText(_ className: String, _ functions: [Function]) throws -> String
{
let date = Date() // now
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
let dateString = formatter.string(from: date)
let moduleName = self.makeModuleName(className)
let requestName = self.makeRequestName(className)
let responseName = self.makeResponseName(className)
let cases = self.generateModuleCases(className, functions)
return """
//
// \(className)Module.cpp
//
//
// Created by Clockwork on \(dateString).
//
#include "\(className)Module.h"
\(responseName) *\(moduleName)::handle(\(requestName) *request)
{
switch (request->type)
{
\(cases)
}
return new \(responseName)(\(responseName)_ERROR, NULL);
};
"""
}
func generateModuleCases(_ className: String, _ functions: [Function]) -> String
{
let cases = functions.map { self.generateModuleCase(className, $0) }
return cases.joined(separator: "\n\n")
}
func generateModuleCase(_ className: String, _ function: Function) -> String
{
let caseName = self.generateRequestTypeEnum(className, function)
let requestName = self.makeRequestCaseName(className, function)
let responseName = self.makeResponseName(className)
let responseCaseName = self.makeResponseCaseName(className, function)
let methodName = self.makeMethodName(className, function)
let setResult: String
let makeResponse: String
let resultParameter: String
if let returnType = function.returnType
{
setResult = "\(returnType) result = "
makeResponse = "\(responseCaseName) response = \(responseCaseName)(result);"
resultParameter = "(void *)&response"
}
else
{
setResult = ""
makeResponse = ""
resultParameter = "NULL"
}
let parameters: String = self.generateModuleArguments(function.parameters)
let parametersCast: String
if function.parameters.isEmpty
{
parametersCast = ""
}
else
{
parametersCast = "\(requestName) *parameters = (\(requestName) *)request->body;"
}
return """
case \(caseName):
{
\(parametersCast)
\(setResult)this->logic->\(methodName)(\(parameters));
\(makeResponse)
return new \(responseName)(\(caseName), \(resultParameter));
}
"""
}
func generateModuleArguments(_ parameters: [FunctionParameter]) -> String
{
let arguments = parameters.map { self.generateModuleArgument($0) }
return arguments.joined(separator: ", ")
}
func generateModuleArgument(_ parameter: FunctionParameter) -> String
{
return "parameters->\(parameter.name)"
}
}
There isn't a lot to say here because it's very similar to the code generator we wrote in the last post. A couple of differences are that we generate two files, a .h and a .cpp, and the generateModuleCase() is more complicated that other functions. We essentially pre-generate most of each line of the main body into variables and paste them together in the body. Can you think of a cleaner way to do this, one that maintains correctness and adds clarity, without becoming too much more complex?
Here is the .h code that this generates for the SGTL5000 audio codec controller:
#ifndef AudioControlSGTL5000Module_h_
#define AudioControlSGTL5000Module_h_
#include <Arduino.h>
#include "Audio.h"
#include "AudioControlSGTL5000Messages.h"
class AudioControlSGTL5000Module
{
public:
AudioControlSGTL5000Module(AudioControlSGTL5000 *component) : logic(component) {}
AudioControlSGTL5000 *logic;
AudioControlSGTL5000Response *handle(AudioControlSGTL5000Request *request);
};
#endif
And the .cpp code:
#include "AudioControlSGTL5000Module.h"
AudioControlSGTL5000Response *AudioControlSGTL5000Module::handle(AudioControlSGTL5000Request *request)
{
switch (request->type)
{
case AudioControlSGTL5000Request_ENABLE:
{
bool result = this->logic->enable();
AudioControlSGTL5000EnableResponse response = AudioControlSGTL5000EnableResponse(result);
return new AudioControlSGTL5000Response(AudioControlSGTL5000Request_ENABLE, (void *)&response);
}
case AudioControlSGTL5000Request_VOLUME:
{
AudioControlSGTL5000VolumeRequest *parameters = (AudioControlSGTL5000VolumeRequest *)request->body;
bool result = this->logic->volume(parameters->left, parameters->right);
AudioControlSGTL5000VolumeResponse response = AudioControlSGTL5000VolumeResponse(result);
return new AudioControlSGTL5000Response(AudioControlSGTL5000Request_VOLUME, (void *)&response);
}
return new AudioControlSGTL5000Response(AudioControlSGTL5000Response_ERROR, NULL);
};
I call the implementation of an object a Module. This is a terrible name, but I don't have a better one yet. There will be one module for each Audio library class. The module has just one function, handle(), which handles message dispatch. It does the basic job of a message dispatcher, which is to pick an implementation, algorith, or behavior. It does this with a simple C++ switch statement on the message type field. It then does a cast to get the message-specific data structure out of the type-erased void *. It does the appropriate function call for the message. Then it get the return value and packages it up into a new message to send back to the caller, which is does by just returning that message from handle(). If something goes wrong, in particular if it's give a message that it doesn't know how to handle, then it returns an error message instead.
Now in real Smalltalk, message dispatch is a bit more complicated. In particular, if the object doesn't know how to handle the message then it will do some more handling of that scenario. This is how inheritance is handled, for instance. We don't really need any of that complexity for our little language, so I'll skip it.
I'd like to talk a little bit more about what we've achieved so far, because it might seem like not a whole lot since there was not very much code involved in getting here. We have now created a virtual machine. It might not look too much like a Smalltalk or Java or Python virtual machine. This is because our virtual machine instruction set is so far limited to just controlling SGTL5000 audio codecs! Does your favorite virtual machine have an instruction to do that? I would guess not, it's probably more focused on doing integer math. Usually when you talk about a virtual machine, you might think about some low-level questions such as whether the word size is 32-bit or 64-bit (or something else). Well, as it turns out, we have written a PORTABLE virtual machine! The instruction set is the message types, which consist of an "int type" and a "void *body". You could call the type field the operator and the body field the operator. The size of int and void * are machine-specific in C++, so if you are using a 32-bit Arduino, this will be a 32-bit virtual machine, and on a 64-bit Arduino this will be a 64-bit virtual machine. We don't need to care about such minutiae. This way of thinking about virtual machines is due to historical reasons. We're skipping a lot of historical baggage by not recapitulating the entire history of computing machines is our virtual machine.
By choosing an instruction set based around I/O (so far) instead of computations, we are taking the next step into what we are calling "effective programming". We are make the effects (for instance, I/O) the first class entities in our language rather than the computations. You might say that this is the opposite of functional programming, but I would cavalierly suggest that perhaps it is actually the dual. Since functions are the dual of objects and our effects are objects, I think this is a reasonable assertion. Don't believe me? Google it and see what you find!
So we've perhaps made the world's smallest Smalltalk virtual machine (while missing many essential features of Smalltalk and not being at all compatible with existing virtual machines, of course). What's next? I think we'd better write something to test this, so I'll get to that in my next post.