A Brief Introduction to JUCE - WRodewald/HPEQ GitHub Wiki

This page gives a very short introduction to Juce and explains some oddities. Juce is not a library in the usual sense, rather it's a framework that comes with source code, project management tools and some other stuff.

Project Management

JUCE Projects are defined in a .jucer file that can be opened with the JUCE management tool Projucer. The .jucer file contains information about the following things:

  • User Source Files
    What .h and .cpp files are part of the project. Where are they and whether or not they should be compiled. (In most cases, only .cpp files are compiled and .h files are not. However, there are a few examples where you want to deviate from this rule. One is actually used in JUCE)
  • JUCE Modules
    JUCE is split up in modules. Most modules are just one .h and one .cpp file. Those internall include other .cpp files. This is done to keep stuff simple but there is rarely a reason to mess with modules anyway. All that's important is that the .jucer files contains information about what juce modules are required in the project and where they are located. For example, an audio plugin depends on juce_core (as does every other juce project), juce_gui_basics, juce_audio_plugin_clients and a few others.
  • Exporter I'll explain that later
  • Configuration With every module that is added to a project, the .jucer file will store configuration fields. For instance, a plugin project will have a bunch of configuration fields about name of the plugin, plugin IDs, whether it's a synth or an effect and many more.

Exporters

The .jucer files are really light weight (you can open them with a text editor to have a look) and only contain cross-platform information. In order to actually compile a plugin, the Projucer will generate a platform specific IDE-project, XCode on Mac, Visual Studio on Windows and so on. This generation is what JUCE calls an Exporter. The benefit of this approach is that, hopefully, there is only one file to configure the project that afterwards can be compiled on many different platforms. In the Projucer, exporters can be configured to contain a few more information. For instance, you might need to add additional library dependencies, header search paths, compiler configurations and so on. Those configurations are usually done in the IDE (XCode, VS) but in the case of JUCE should be done in the Projucer in order to keep the project cross platform. The IDE projects will be generated in the Builds/ directory.
It's important to understand that what's in the Build directory is only a temporary part of the project. This also means that we don't need to include the path in the Git repository as it is auto-generated on local machine after cloning the repository. (So, Builds is ignored in this repo)

The JuceLibraryCode Oddity

This might get a bit messy but it's a crucial part of understanding how Juce works in detail. Feel free to ignore this paragraph.

Juce is no library and the modules that are included in a project as source and header files in the IDE project depend on informations that are only available when we compile a project. For instance, the plugin module of JUCE needs to know that the plugin is going to be called. And this "knowing" is static, which means that it's hard written into the binary by the time of compiling. JUCE solves this with the means of C++ macros. The informations are set in the .jucer file via the Projucer and when an IDE project is generated, an additional header file is generated that simply defines all the variables. Might contain something like this:

#ifndef  JucePlugin_Desc
 #define JucePlugin_Desc                   "HPEQ"
#endif

#ifndef  JucePlugin_Manufacturer
 #define JucePlugin_Manufacturer           "AKTTUBerlin"
#endif

Those lines do the following. The first `#ifndef' checks if JucePlugin_Desc is not defined. If true, the #define block defines the JucePlugin_Desc with content "HPEQ". The #endif block just closes the #ifndef block. Defines work like this: Before the plugin is compiled, the so called preprocessor parses through the source code and includes. If he (personification of preprocessors I guess) finds a #define, he'll note down the name of the define and the content. If the define is used in the code later on, the define-name will be replaced with it's content. So the following

std::cout << "The name of the plugin is " << JucePlugin_Desc;

will be replaced with

std::cout << "The name of the plugin is " << "HPEQ";

(Feel free to ignore the syntax of c++ text I/O cout).

Here is where is gets interesting and messy: The defines are set in the header file but all JUCE module source and header files need to know those defines before being parsed by the compiler. Otherwise, they won't be replaced. But when you do something like this in your code:

#include "juce_gui_basics.h"

// some non existing function that prints the plugin name:
juce::printPluginName();

The defines might be defined but very well might not be. We have to guarantee that that at this line, the plugin name was already #define'd. In order to do this, we never include a juce header file and we don't directly compile juce source files. Instead, the Projucer created proxy files, for instance include_juce_audio_devices.cpp:

#include "AppConfig.h"
#include <juce_audio_devices/juce_audio_devices.cpp>

I removed a comment but those files are really that short. What's they do is including the appconfig files that contains all the information-setting #define statements and afterwards include the actual file. All source files that are actually compiled are included like this to guarantee that the "information injection" works. The juce_audio_devices.cpp is not compiled directly but only by including them in the cpp file. This is something you rarely see, a source file including another source file. The headers are included similar. Instead of having one proxy-include file per header, JUCE throws them all in a file that include AppConfig.h and all necessary headers.

TL;DR the JuceLibraryCode path contains auto generated, temporary files that define plugin configuration, and include all JUCE intern source and header files in order "inject" information into the framework when we write a JUCE project. As with the Builds directory, those files are generated when exporting an IDE project and don't have to be included in the git repository. Always include juce files via the proxy-include file JuceHeader.h, never include the module-specific headers like juce_gui_basics.h

JUCE IDE Project Setup

For plugins, a IDE project generated by the Projucer contains multiple targets. Targets in C++ are compile-units that can be an executable or a dynamic or static library. What JUCE does is splitting the "shared" code that is reused in VST, AU, AAX,... plugins one target called HPEQ_SharedCode. This target also contains our user code. However, the target only builds a static library. This library is than used in the per-plugin target (HPEQ_VST, HPEQ_StandalonePlugin). I'm not sure how XCode handles this but as far as I know it's only necessary to build a VST target and the required SharedCode target will be build as well.

Bonus Round: Source and Header Files and Building in general

A C++ project contains header and source files. They are most often using suffix .h or .hpp for the header and .cpp or sometimes .mm for source files. (.mm is OSX specific stuff) A IDE will compile source files, if not configured otherwise by the user. Header files are ignored unless we include them in a source file. A IDE does the following to create a C++ binary: First, the preprocessor parses through the code and executed #defines, #include and so on. #include in cpp simply means that the code in the file is inserted at the line of the include. More on that later. The compiler then takes the source files with resolved include and define statements and compiles them into object files (.o). Object files already contain machine code with symbols that label chunks of code. A simple example is a function that is compiled and is present in the .o file with a "symbol" instead of the name. The linker then does it's job and grabs all the .o files and puts it together in one executable binary.

There are two things that can go wrong when we include files. First, the compiler might complain about stuff like Classes being declared more then once. Second, the linker might complain about symbols missing or being defined more the once. Here is what that means:

A header file might define a class. Looks like this


class SomeFilter
{
public:
SomeFilter(); // <-- constructor
float tick(float input); // <-- filter function
}

A note on the choice of words: Declaring means that we tell the user that there is a class SomeFilter with the function tick. Defining means that we define how that function actually looks like. In most cases, you declare in header files but define in source files.
When we use the class above in multiple files, the definition above might be inserted in the same source unit more then once. Common case: file A include SomeFilter.h and file B. File B includes SomeFilter.h as well. C++ doesn't like that, even when the declaration stays the same as it comes from the same header file. In order to fix this, we have to encapsulate the definition in a "include guard":

#ifndef SOMEFILTER_FILE
#define SOMEFILTER_FILE
class SomeFilter
{
public:
SomeFilter(); // <-- constructor
float tick(float input); // <-- filter function
}
#endif

What that does is only defining the class in the block, when SOMEFILTER_FILE is not already defined. And SOMEFILTER_FILE is defined once the class is "defined". Alternatively #pragma once does the same thing and is a more modern approach. Put it at the beginning of the file and be done with it.

Now to the second issue: the linker. We might have the situation where we compile the source file that goes with the header file more then once. That happens every once in a while. The compiler will not notice as the compiler goes through ever source unit individually. However, the compiler will create the object file with the same symbols and the linker afterwards will try to combine object files containing the same symbol. That's also not good. To prevent this, in C++ land you have to make sure that every source file (.cpp) is compiled once. That means: don't include source files from another source file. The included file will be compiled twice. The exception is when we manually mark the included source file as "not being compiled".

The second common linker error is simpler to understand: If we declared a function float SomeFilter::tick(float input) but we never actually defined the implementation of the function in a source file, this will lead to the linker to complain about a symbol not being defined. Exception: The compiler ignored functions when you don't call them in your code. (Actually, the compiler might complain as well.)

TL;DR Contents of a header file should be surrounded in an include guard to prevent the content from being declared more then once. Header should only contain declarations (there IS a function Foo::func) and source files should only contain definitions (this is HOW Foo:func is implemented). Source files shouldn't include other source files.

Inline

Very brief: "Inlined" functions look like normal functions but the compiler doesn't compile them as functions. Instead, the compiler simply replaces the call float out = SomeFilter::tick(float in); with the implementation of the function. Inline function have to be declared and defined in header files. Rare but important exception since inlines are heavily used in Audio C++ in order to prevent performance overhead. The overhead comes from the fact that normal functions usually mean that the CPU takes a break to jump between different statements in the machine code.