Generating Code with QFace - Part 1

QFace provides a set of IDL parsers which can help you convert easy-to-read interface definitions into compiling code!

It was written by a Qt/QML developer so it provides many useful features that are Qt-compatible. It was also recently ingested into the Qt Infotainment package

IDL - Interface Description Language

An IDL helps you describe code in a language independent way.

We can refer to the "meta-language" that QFace provides as QFace

Let's Describe!

Let's think about describing a Human Resources service that manages Employees.

We can call this service – the EmployeeService.

Let's define an object, or struct, called Employee that encapsulates one individual that works for this company. This struct can define the employee's name, age, and role. An employee's Role can be a list of positions within the company, i.e. Engineers, Managers, or Executives.

This EmployeeService might have a list of employees, or employeeList, which can be a list of type Employee.

This EmployeeService may also have two operations, one to hire and one to fire an employee. Let's call these methods hireEmployee(..) and fireEmployee(..); respectively. Both methods take a parameter of type Employee.

We can also create a signal that can be emitted whenever a new employee is hired, employeeHired(..). We can pass more context into this signal, e.g. the Employee that was hired.

Let's write this scenario as a QFace file:

module qml.guide.example 1.0

interface EmployeeService {
    int employeeCount
    list<Employee> employeeList
    
    void hireEmployee(Employee employee)
    void fireEmployee(Employee employee)
    
    signal employeeHired(Employee employee)
}

struct Employee {
    string firstName
    string lastName
    int age
    Role role
}

enum Role {
    Unknown,
    SoftwareEngineer,
    HardwareEngineer,
    QualityEngineer,
    ProjectManager,
    Manager,
    Executive
}

An important thing to remember at this stage is that the file above does follow QFace language rules, so every keyword (module, interface, struct, enum) is provided by QFace. The collection of the possible keywords that QFace provides will be referred to as the grammar of QFace.

Please read about the grammar that QFace supports here.

Getting started with the QFace generators

QFace generators are written in python3.

Start by using pip (python package manager) to install qface and its dependencies:

python3 -m pip install qface

Once qface is done installing, check the version:

$ python3 -m pip freeze | grep qface
qface==2.0.0

I have noticed some API changes between 1.X and 2.0, so please keep that in mind if the version of qface you are using is newer than this tutorial

Let's code!

Save the above EmployeeService and friends example to a file called qml.guide.example.qface. Notice how we try to keep the file name the same as the module name. This simplifies searching for right module when you have hundreds of QFace documents.

qml.guide.example.qface should look like this:

module qml.guide.example 1.0

interface EmployeeService {
    int employeeCount
    list<Employee> employeeList
    
    void hireEmployee(Employee employee)
    void fireEmployee(Employee employee)
    
    signal employeeHired(Employee employee)
}

struct Employee {
    string firstName
    string lastName
    int age
    Role role
}

enum Role {
    Unknown,
    SoftwareEngineer,
    HardwareEngineer,
    QualityEngineer,
    ProjectManager,
    Manager,
    Executive
}

Big Picture

This quasi-architecture-flow diagram is straight from the QFace manual. As you can see, we need to take our QFace Document (Interface Definition) which describes an EmployeeService, and feed that into Your Generator which uses the QFace (Generator Library) to finally produce Your Code.

Your Generator

Let's continue our escapade by writing the Your Generator part of the diagram above. We need to write a python3 script that uses the qface package we just downloaded from pip.

Open a new file and name it generator.py:

#!/usr/bin/env python3

from qface.generator import FileSystem, Generator

print("My QFace Generator goes here!")

Run it and make sure you have no errors. This confirms if pip installed the qface package correctly:

python3 generator.py

You can also set the script to executable so you can forever omit the python3 prefix to your command (offtopic: works because of the shebang):

chmod u+x generator.py
./generator.py

Now that we know qface is installed correctly, let's continue writing our generator. The contents of this generator are inspired by the QFace manual:

#!/usr/bin/env python3

from qface.generator import FileSystem, Generator

def generate(input, output):
    # parse the interface files
    system = FileSystem.parse(input)
    # ...

# call the generation function
generate('qml.guide.example.qface', 'generated_code_output')

What's going on here?

Let's start from the last line:

generate('qml.guide.example.qface', 'generated_code_output')

We are sending in our QFace document from above, and a folder name of where our final code output will be placed.

Inside the generate method, we are using qface.FileSystem to parse the QFace document and produce a variable called system which is of type System. This variable (shown in the diagram as ctx) is a python object that holds the grammar of our QFace document.

By reading the API documentation of System, we can see it has a property called modules.

We can use python's pprint package to print out the modules property to see what's inside:

#!/usr/bin/env python3

from qface.generator import FileSystem, Generator
from pprint import pprint

def generate(input, output):
    # parse the interface files
    system = FileSystem.parse(input)
    pprint(system.modules)

# call the generation function
generate('qml.guide.example.qface', 'generated_code_output')

You should see something like this:

qface uses language profile: EProfile.FULL
odict_values([<<class 'qface.idl.domain.Module'> name=qml.guide.example>])

Notice our module name is displayed, qml.guide.examples.

It is now easy to see the type hierarchy present inside the System type:

  • System
  • -> Module(s)
  • ->-> Interface(s)
  • ->-> Struct(s)
  • ->-> Enum(s)

All of these types are stored as lists of dictionaries (maps) so we can easily iterate through them and print out the grammar of our QFace document:

#!/usr/bin/env python3

from qface.generator import FileSystem, Generator
from pprint import pprint

def generate(input, output):
    # parse the interface files
    system = FileSystem.parse(input)
    
    for module in system.modules:
        print("module.name = " + module.name)

        for interface in module.interfaces:
            print("interface.name = " + interface.name)

        for struct in module.structs:
            print("struct.name = " + struct.name)

        for enum in module.enums:
            print("enum.name = " + enum.name)

# call the generation function
generate('qml.guide.example.qface', 'generated_code_output')

Run this and you will see familiar names:

$ ./generator.py

qface uses language profile: EProfile.FULL
module.name = qml.guide.example
interface.name = EmployeeService
struct.name = Employee
enum.name = Role

Success! Our generator has reached it's first milestone. Sit back and reflect on what just happened.

We took a seemingly unknown IDL and turned it into grammatically significant types: modules (as a collection of interfaces, structs, and enums).

Just imagine the potential of evolving a simple generator like this to something grander!

Stay tuned for the next post in this series!