In my last post, we extracted the API from the part of the Arduino Audio library, the SGTL5000 audio codec controller, so that we could do some code generation to automatically write C++ code that uses this library. Now we finally get to do a little bit of code generation. I don't want to overwhelm you with the number of wild ideas I have, so we're just going to generate one header file to start.

In order to do code generation we will eventually need some starting template that tells the code generator what specific program it needs to generate. This template is essentially a programming language, and the code generator is essentially a compiler (or you could call it a transpiler). We don't have to decide on all the details of the language right now, as we have a bit of low-level plumbing to do first just to get things going. However, I don't want to write a procedural language like C or C++. If we wanted to do that, we could just already be using C++. No, I want to write a Smalltalk-style object-oriented language, because I think this is cooler, more fun, and less work. You may say, "But C++ is object-oriented!" and I certainly don't want to get in an argument with you in the comments section of an Internet post. I will simply point you to my good friend Will Cook's much better post on the subject, "A Proposal for Simplified, Modern Definitions of "Object" and "Object Oriented".

I will now simplify greatly for you what makes an object in Smalltalk. An object is a tiny computer (possibly with I/O) that you communicate with by sending it a message. The difference between a procedure call (as in procedural languages, referred to as a function call in C++) and a message is that with a procedure call, the caller decides which specific procedure to call, which is to say which implementation, algorithm, or behavior will happen next. If you want a different behavior, simply call a different procedure! With a message, the caller does not know what will happen, the object decides. If you recall from earlier in this paragraph, an object is a tiny computer, so ANYTHING can happen for ANY message. The object has 100% choice in the implementation, algorithm, or behavior. What freedom! What chaos! This anarchy is also true of all I/O. With a pure computation (such as with a pure functions), if you have the source code then you can examine the implementation and reason about what it might do. I once took a whole class just about looking at the code for an algorithm and determining how much time or space it is likely to take. How can we reason about I/O? For instance, if we prompt a human to answer "yes" or "no" to a question, how long will it take? We have no idea. The sun might explode or burn out. They might response with "antelope". They might trip over the power cable and our computation never finishes. This is why pure functional languages do not allow this kind of recklessness and stick to just silently doing computations and never telling us the answers. Unfortunately, for historical reaosns, they may still run out of memory or perhaps crash due division by zero. Since objects and I/O are both imbued with godlike powers, it makes sense to represent I/O as objects. Our audio codec controller is very much concerned with I/O, so we'll make it into an object.

So enough silliness, let's write a really tiny baby Smalltalk! The first thing we need to do in order to send messages is to create some messages. Messages are data. So for each function that we want to eventually call in our SGTL5000 controller, we will need a data structure that represents our intention for the object to call that function for us. Of course, whether or not that actually happens is up to the object, which we won't create until later. This process of turning functions into messages, more generally actions into things, could be called reification, if you feel like calling it something. We're almost ready to code, but I need to bring up one more sticky point before I show you the code, and this is that the C++ type system lacks the kind of polymorphism that we need. Specifically, we want to be able to give an object some sort of generic Message type and for it to then figure out, at runtime, which specific message it is, so it can do the right thing (if it wants to). So we're going to do some C++ trickery to create a bunch of message types and also two generic types. Okay, now check out the code!

//
//  CppGeneratorMessages.swift
//
//
//  Created by Dr. Brandon Wiley on 4/13/23.
//

import Foundation

extension CppGenerator
{
    public func generateMessages(_ input: URL, _ output: URL)
    {
        do
        {
            let source = try String(contentsOf: input)
            let className = try self.parser.findClassName(input, source)

            let functions = try self.parser.findFunctions(source)

            guard functions.count > 0 else
            {
                return
            }

            try self.generateMessages(output, className, functions)
        }
        catch
        {
            print(error)
        }
    }

    func generateMessages(_ outputURL: URL, _ className: String, _ functions: [Function]) throws
    {
        print("Generating \(className)Messages.h...")

        let outputFile = outputURL.appending(component: "\(className)Messages.h")
        let result = try self.generateMessagesText(className, functions)
        try result.write(to: outputFile, atomically: true, encoding: .utf8)
    }

    func generateMessagesText(_ 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 requestName = self.makeRequestName(className)
        let responseName = self.makeResponseName(className)
        let requestEnums = self.generateRequestEnumsText(className, functions)
        let responseEnums = self.generateResponseEnumsText(className, functions)

        return """
        //
        //  \(className)Messages.h
        //
        //
        //  Created by Clockwork on \(dateString).
        //

        #ifndef \(className)Messages_h_
        #define \(className)Messages_h_

        #include <Arduino.h>

        class \(requestName)
        {
            public:
                \(requestName)(int type, void *body)
                {
                    this->type = type;
                    this->body = body;
                }

                int type;
                void *body;
        };

        class \(responseName)
        {
            public:
                \(responseName)(int type, void *body)
                {
                    this->type = type;
                    this->body = body;
                }

                int type;
                void *body;
        };

        \(requestEnums)

        \(responseEnums)

        #endif
        """
    }

    func generateRequestEnumsText(_ className: String, _ functions: [Function]) -> String
    {
        let typeEnum = generateRequestTypeEnums(className, functions)
        let enums = functions.compactMap { self.generateRequestEnumCase(className, $0) }
        return typeEnum + "\n\n" + enums.joined(separator: "\n\n")
    }

    func generateRequestTypeEnums(_ className: String, _ functions: [Function]) -> String
    {
        let typeEnums = functions.map { self.generateRequestTypeEnum(className, $0) }
        return "enum \(className)RequestType {\(typeEnums.joined(separator: ", "))};"
    }

    func generateResponseEnumsText(_ className: String, _ functions: [Function]) -> String
    {
        let typeEnum = generateResponseTypeEnums(className, functions)
        let enums = functions.compactMap { self.generateResponseEnumCase(className, $0) }
        return typeEnum + "\n\n" + enums.joined(separator: "\n\n")
    }

    func generateResponseTypeEnums(_ className: String, _ functions: [Function]) -> String
    {
        let responseName = self.makeResponseName(className)
        let typeEnums = ["\(responseName)_ERROR"] + functions.map { self.generateResponseTypeEnum(className, $0) }
        return "enum \(className)ResponseType {\(typeEnums.joined(separator: ", "))};"
    }

    func generateRequestTypeEnum(_ className: String, _ function: Function) -> String
    {
        let requestName = self.makeRequestName(className)
        return "\(requestName)_\(function.name.uppercased())"
    }

    func generateResponseTypeEnum(_ className: String, _ function: Function) -> String
    {
        let responseName = self.makeResponseName(className)
        return "\(responseName)_\(function.name.uppercased())"
    }

    func generateRequestEnumCase(_ className: String, _ function: Function) -> String?
    {
        if function.parameters.isEmpty
        {
            return nil
        }
        else
        {
            let requestClassName = self.makeRequestCaseName(className, function)
            let constructorParams = self.generateRequestConstructorParameters(function)
            let requestSetters = generateRequestSetters(function)
            let requestParameters = generateRequestEnumParameters(function)

            return """
            class \(requestClassName)
            {
                public:
                    \(requestClassName)(\(constructorParams))
                    {
            \(requestSetters)
                    }

            \(requestParameters)
            };
            """
        }
    }

    func generateRequestEnumParameters(_ function: Function) -> String
    {
        let enums = function.parameters.map { self.generateRequestEnumParameter($0) }
        return enums.joined(separator: "\n")
    }

    func generateRequestEnumParameter(_ parameter: FunctionParameter) -> String
    {
        return "        \(parameter.type) \(parameter.name);"
    }

    func generateRequestConstructorParameters(_ function: Function) -> String
    {
        let enums = function.parameters.map { self.generateRequestConstructorParameter($0) }
        return enums.joined(separator: ", ")
    }

    func generateRequestConstructorParameter(_ parameter: FunctionParameter) -> String
    {
        return "\(parameter.type) \(parameter.name)"
    }

    func generateRequestSetters(_ function: Function) -> String
    {
        let enums = function.parameters.map { self.generateRequestSetter($0) }
        return enums.joined(separator: "\n")
    }

    func generateRequestSetter(_ parameter: FunctionParameter) -> String
    {
        return "            this->\(parameter.name) = \(parameter.name);"
    }

    func generateResponseEnumCase(_ className: String, _ function: Function) -> String?
    {
        let responseCaseName = self.makeResponseCaseName(className, function)

        if let returnType = function.returnType
        {
            return """
            class \(responseCaseName)
            {
                public:
                    \(responseCaseName)(\(returnType) value)
                    {
                        // value is a value type and not a pointer, so this->value should be a copy.
                        this->value = value;
                    }

                    \(returnType) value;
            };
            """
        }
        else
        {
            return nil
        }
    }
}

It's short, less than 300 lines. The generateMessageText() function is the main business of this code. It's just a big String with Swift string interpolation to plug in all the variables. For more complicated stuff like lists of things, I generate that in its own variable and then plug it into the main text. This code relies on a few functions like makeRequestName() that are defined in another file. The details are important, but what is important is the idea that you try to separate out the generation of names into their own reusable functions. The names for things need to be used in several places and match exactly. This has been a big problem in previous versions of this code where I wrote the names out by hand in each place. People are really bad at being meticulously consistent, so I say just don't try! This only really needs to be one big function, the functions in this file are very used anywhere else. I broke it up into smaller functions because I find it easier to think about in chunks. First I wrote the make text generator, and then all of the smaller functions. Often when I find a bug, I just need to edit one short two or three line function, rather than looking through a bunch of code to find the line that is wrong.

So here's the code that is generated, edited down to just show enable() and setVolume(), our chosen example functions from the previous post:

#ifndef AudioControlSGTL5000Messages_h_
#define AudioControlSGTL5000Messages_h_

#include <Arduino.h>

class AudioControlSGTL5000Request
{
    public:
        AudioControlSGTL5000Request(int type, void *body)
        {
            this->type = type;
            this->body = body;
        }

        int type;
        void *body;
};

class AudioControlSGTL5000Response
{
    public:
        AudioControlSGTL5000Response(int type, void *body)
        {
            this->type = type;
            this->body = body;
        }

        int type;
        void *body;
};

enum AudioControlSGTL5000RequestType
{
    AudioControlSGTL5000Request_ENABLE,
    AudioControlSGTL5000Request_VOLUME
};

class AudioControlSGTL5000VolumeRequest
{
    public:
        AudioControlSGTL5000VolumeRequest(float left, float right)
        {
            this->left = left;
            this->right = right;
        }

        float left;
        float right;
};

enum AudioControlSGTL5000ResponseType
{
    AudioControlSGTL5000Response_ERROR, 
    AudioControlSGTL5000Response_ENABLE, 
    AudioControlSGTL5000Response_VOLUME, 
};

class AudioControlSGTL5000EnableResponse
{
    public:
        AudioControlSGTL5000EnableResponse(bool value)
        {
            // value is a value type and not a pointer, so this->value should be a copy.
            this->value = value;
        }

        bool value;
};

class AudioControlSGTL5000VolumeResponse
{
    public:
        AudioControlSGTL5000VolumeResponse(bool value)
        {
            // value is a value type and not a pointer, so this->value should be a copy.
            this->value = value;
        }

        bool value;
};

#endif

Okay let's get into this code. First, we have our generic messages types: AudioControlSGTL5000Request and AudioControlSGTL5000Response. I had these include the name of the class (AudioControlSGTL5000) extracted from our parser and instead of just one message type, I have a separate Request type for sending messages that initiate function calls on the audio codec controller and a Response type for the return values from those functions. This may not end up being necessary, but keeping them named distinctly helped me reason my way through making the code correct. These generic types are where we will do some C++ trickery. Each generic message type has a "type" field to tell us which specific message type it contains and then a "body" field that contains the actual data. The type field is a enum (which in C++ is just a special type of int) and the body field is a "void *". This is a special type used in C++ for "type erasure". In order words, we can put anything we want in here and C++ will forget what type it was to start with, but it's okay because we remember due to the type field.

Next, we define the enum for the type field, which is just some special names for ints that the compiler will remember for us. Each message get its own enum name: AudioControlSGTL5000Request_ENABLE and AudioControlSGTL5000Request_VOLUME.

Then we define data structures for each of the messages, for instance, for AudioControlSGTL5000VolumeRequest. This just verbatim copies the function call arguments for the volume() function, because that's what we need to send to the object for it to call the corresponding function for us. But wait, where is the data structure for enable()? We don't need one, because it doesn't take any arguments, so just the type field is sufficient.

Then we do the same thing for the responses for each function. First we make an enum. Ah, but ho! This enum has an extra element, AudioControlSGTL5000Response_ERROR. What is this for? Well messages, unlike function calls (some might say), can always fail. Remember an object is a tiny computer, we have no idea if it will do the right thing or create a smoking crater in the moon. So any time we send a message, we might get an error back. Then we make the response data structures, which are very short since C++ functions can only return 1 or 0 values. In the case of 0 return values, we don't need a data structure, just the type field is sufficient.

So that's it! We used code generation to create a bunch of messages corresponding to the interesting functions on our audio codec controller. In the next post, we'll make implementation (using code generation, of course) of an audio codec controller object that doesn't do anything sketchy and just simply calls the correct function when it receives the corresponding message.

Code Generation for Arduino Audio - From Functions to Objects