In my last post, we implemented message dispatch, the core feature of a Smalltalk-like object-oriented language. Before we start getting wild with objects, I think it would be prudent to test our code. My approach to this is as boring as possible: writing a C++ wrapper to our object-oriented message dispatch system. So yeah, we took some procedural C++ code and wrapped it in a pure OO abstraction layer, and now we're unwrapping it back to boring old C++ again?!? You get it! Why? Because tests should be as boring as possible. Tests should tests things that should obviously work. If you're not sure if you're test is going to pass, then it's not a test, it's an experiment.
I wrote a code generator for this, of course. I call my class that wraps the message dispatch for an object a "Universe". This is a bad name, but I don't have a better one yet. Here is the code:
import Foundation
extension CppGenerator
{
public func generateUniverse(_ 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.generateUniverseHeader(output, includeFile, className, functions)
try self.generateUniverse(output, className, functions)
}
catch
{
print(error)
}
}
func generateUniverseHeader(_ outputURL: URL, _ includeFile: String, _ className: String, _ functions: [Function]) throws
{
print("Generating \(className)Universe.h...")
let outputFile = outputURL.appending(component: "\(className)Universe.h")
let result = try self.generateUniverseHeaderText(includeFile, className, functions)
try result.write(to: outputFile, atomically: true, encoding: .utf8)
}
func generateUniverseHeaderText(_ 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 universeName = self.makeUniverseName(className)
let moduleName = self.makeModuleName(className)
let functions = self.generateFunctionDeclarations(functions)
return """
//
// \(universeName).h
//
//
// Created by Clockwork on \(dateString).
//
#ifndef \(className)Universe_h_
#define \(className)Universe_h_
#include "\(className)Messages.h"
#include "\(className)Module.h"
class \(universeName)
{
public:
\(universeName)(\(moduleName) *handler) : module(handler) {}
\(moduleName) *module;
\(functions)
};
#endif
"""
}
func generateFunctionDeclarations(_ functions: [Function]) -> String
{
let results = functions.compactMap { self.generateFunctionDeclaration($0) }
return results.joined(separator: "\n")
}
func generateFunctionDeclaration( _ function: Function) -> String
{
let returnTypeText: String
if let returnType = function.returnType
{
returnTypeText = returnType
}
else
{
returnTypeText = "void"
}
let parameterText: String
if function.parameters.isEmpty
{
parameterText = ""
}
else
{
let parameters = function.parameters.map { self.generateParameter($0) }
parameterText = parameters.joined(separator: ", ")
}
return " \(returnTypeText) \(function.name)(\(parameterText));"
}
func generateUniverse(_ outputURL: URL, _ className: String, _ functions: [Function]) throws
{
let universeName = self.makeUniverseName(className)
print("Generating \(universeName).cpp...")
let outputFile = outputURL.appending(component: "\(universeName).cpp")
let result = try self.generateUniverseText(className, functions)
try result.write(to: outputFile, atomically: true, encoding: .utf8)
}
func generateUniverseText(_ 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 universeName = self.makeUniverseName(className)
let functions = self.generateFunctions(className, functions)
return """
//
// \(universeName).cpp
//
//
// Created by Clockwork on \(dateString).
//
#include "\(universeName).h"
\(functions)
"""
}
func generateFunctions(_ className: String, _ functions: [Function]) -> String
{
let results = functions.compactMap { self.generateFunction(className, $0) }
return results.joined(separator: "\n\n")
}
func generateFunction(_ className: String, _ function: Function) -> String
{
let signature = self.generateFunctionSignature(className, function)
let body = self.generateFunctionBody(className, function)
let lines: [String] = body.split(separator: "\n").map { String($0) }.filter
{
line in
return !line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
let trimmedBody = lines.joined(separator: "\n")
return """
\(signature)
{
\(trimmedBody)
}
"""
}
func generateFunctionSignature(_ className: String, _ function: Function) -> String
{
let universeName = self.makeUniverseName(className)
let returnTypeText: String
if let returnType = function.returnType
{
returnTypeText = returnType
}
else
{
returnTypeText = "void"
}
let parameterText: String
if function.parameters.isEmpty
{
parameterText = ""
}
else
{
let parameters = function.parameters.map { self.generateParameter($0) }
parameterText = parameters.joined(separator: ", ")
}
return "\(returnTypeText) \(universeName)::\(function.name)(\(parameterText))"
}
func generateParameter(_ parameter: FunctionParameter) -> String
{
return "\(parameter.type) \(parameter.name)"
}
func generateFunctionBody(_ className: String, _ function: Function) -> String
{
let requestName = self.makeRequestName(className)
let requestEnumCaseName = self.generateRequestTypeEnum(className, function)
let requestCaseName = self.makeRequestCaseName(className, function)
let responseName = self.makeResponseName(className)
let responseCaseName = self.makeResponseCaseName(className, function)
let errorName = self.makeErrorName(className)
let requestBodyInit: String
let requestBodyParameter: String
let requestBodyDelete: String
if function.parameters.isEmpty
{
requestBodyInit = ""
requestBodyParameter = "NULL"
requestBodyDelete = ""
}
else
{
let arguments = function.parameters.map { self.generateArgument($0) }
let argumentList = arguments.joined(separator: ", ")
requestBodyInit = """
\(requestCaseName) *requestBody = new \(requestCaseName)(\(argumentList));
"""
requestBodyParameter = "(void *)requestBody"
requestBodyDelete = """
delete requestBody;
"""
}
let responseCast: String
let returnValue: String
let deleteResponseBody: String
let returnHandler: String
if let returnType = function.returnType
{
responseCast = "\(responseCaseName) *result = (\(responseCaseName) *)response->body;"
returnValue = "\(returnType) returnValue = result->value; // Because this is a value type, this will be a copy."
deleteResponseBody = "delete result;"
returnHandler = "return returnValue;"
}
else
{
responseCast = ""
returnValue = ""
deleteResponseBody = ""
returnHandler = ""
}
return """
\(requestBodyInit)
\(requestName) *request = new \(requestName)(\(requestEnumCaseName), \(requestBodyParameter));
\(responseName) *response = this->module->handle(request);
if response->type == \(errorName)
{
Serial.println("ERROR in \(className).\(function.name)(). Program halted.");
while(1)
{
// We can't return because we don't have a valid return value, so we enter an infinite loop instead.
}
}
\(responseCast)
\(returnValue)
delete request;
\(requestBodyDelete)
\(deleteResponseBody)
delete response;
\(returnHandler)
"""
}
func generateArgument(_ argument: FunctionParameter) -> String
{
return "\(argument.name)"
}
}
Nothing much interesting about the code generator, it's pretty much the same as the others. There are some interesting bits in the generated code, though. Here is the generated header file:
#ifndef AudioControlSGTL5000Universe_h_
#define AudioControlSGTL5000Universe_h_
#include "AudioControlSGTL5000Messages.h"
#include "AudioControlSGTL5000Module.h"
class AudioControlSGTL5000Universe
{
public:
AudioControlSGTL5000Universe(AudioControlSGTL5000Module *handler) : module(handler) {}
AudioControlSGTL5000Module *module;
bool enable();
bool volume(float left, float right);
};
#endif
And here is the generated C++ implementation code:
#include "AudioControlSGTL5000Universe.h"
bool AudioControlSGTL5000Universe::enable()
{
AudioControlSGTL5000Request *request = new AudioControlSGTL5000Request(AudioControlSGTL5000Request_ENABLE, NULL);
AudioControlSGTL5000Response *response = this->module->handle(request);
if response->type == AudioControlSGTL5000Response_ERROR
{
Serial.println("ERROR in AudioControlSGTL5000.enable(). Program halted.");
while(1)
{
// We can't return because we don't have a valid return value, so we enter an infinite loop instead.
}
}
AudioControlSGTL5000EnableResponse *result = (AudioControlSGTL5000EnableResponse *)response->body;
bool returnValue = result->value; // Because this is a value type, this will be a copy.
delete request;
delete result;
delete response;
return returnValue;
}
bool AudioControlSGTL5000Universe::volume(float left, float right)
{
AudioControlSGTL5000VolumeRequest *requestBody = new AudioControlSGTL5000VolumeRequest(left, right);
AudioControlSGTL5000Request *request = new AudioControlSGTL5000Request(AudioControlSGTL5000Request_VOLUME, (void *)requestBody);
AudioControlSGTL5000Response *response = this->module->handle(request);
if response->type == AudioControlSGTL5000Response_ERROR
{
Serial.println("ERROR in AudioControlSGTL5000.volume(). Program halted.");
while(1)
{
// We can't return because we don't have a valid return value, so we enter an infinite loop instead.
}
}
AudioControlSGTL5000VolumeResponse *result = (AudioControlSGTL5000VolumeResponse *)response->body;
bool returnValue = result->value; // Because this is a value type, this will be a copy.
delete request;
delete requestBody;
delete result;
delete response;
return returnValue;
}
An interesting part of this code is that the Universe functions allocate all of the memory for the Requests, and they delete all of the memory for the Requests and the Responses. There is a contract that must be maintained between the Universe and the Module regarding memory allocation. The Universe allocates and deallocates responses. The Module allocates responses and the Universe deallocates then. The Universe does not otherwise use heap allocation at all, only stack allocation. Additionally, message types (requests and responses) do not include any pointer types in their data structures. By this I mean specifically the message data types for each function, for instance AudioControlSGTL5000_VOLUME does not contain any pointers. The base Request and Response types do use void * for type erasure, but these classes are part of the implementation of message dispatch, not actually part of the API for the SGTL5000 audio codec controller library. So since no pointers are passed around in messages and the Universe does not use heap allocation, we do not need a garbage collector at all! C++ stack allocation already acts as a garbage collector for function calls. All of our long-lived data is isolated into objects and all of our long-lived data is function calls that already handle allocation and deallocation In a way, we have have a generational garbage collection with two generations, but got it without having to write it, through careful design.
Now all that's left to do is write a test app using our handy C++ testing framework:
#include <Arduino.h>
#include "Audio.h"
#include "AudioControlSGTL5000Module.h"
#include "AudioControlSGTL5000Universe.h"
AudioControlSGTL5000 codec;
AudioControlSGTL5000Module module(&codec);
AudioControlSGTL5000Universe universe(&module);
void setup()
{
universe.enable();
universe.volume(0.5, 0.5);
}
void loop()
{
}
It may look like we did a lot of work for not a lot of benefit. We've reproduced the SGTL5000 part of our original tonesweep example, but slightly more complex. However, this is just a test to make sure that all of our message passing plumbing is working. Reproducing the starting point is indeed the immediate objective, but not the final goal.
Where should we go from here? Well I think that in order to write our cool new programming language, we need to have more functionality in our language. We need to generate a few more modules. At least an output or two, and perhaps an effect or two. Then we can write a language that can produce multiple programs that do different things and are interactive. Yay! In the next post, I'll show you how all of this code generation stuff is wrapped up into a nice tool already and we'll generate enough code to write a better demo program.