The project requires all contributions to have their source code formatted using appropriate formatting tools to reduce the potential impact of stylistic changes to a structured “diff” view of code.
In addition to those automatically enforceable stylistic choices the project also prefers contributions to follow a set of architectural guidelines that are chosen to reduce undefined or unexpected behavior and make code easier to reason about during reviews or maintenance.
The following guidelines have been established as good practices to reduce some common potential for errors or naive coding practices that have lead to such errors in the past:
Always Initialize Variables - Depending on the programming language, language standard, compiler, and platform, rules for when and how variables are initialized automatically might differ. Thus some variables might have random values at program start and naive code might interpret any value but “0” to mean “correctly initialized” and run into unexpected behavior.
Do not declare or initialize multiple variables on the same line, do not mix declarations and initializations
int i, v = 0; // BAD - v is initialized to 0, i is uninitialized
int i = 0; // GOOD - i is explicitly declared and initialized
int v = 0; // GOOD - v is separately declared and initialized
Do not use “0” as a valid enumeration value by default - in many cases an enum requires an explicit choice to be made (only one value of a set of “valid” values can be set) and “not making a choice” should not be considered valid
enum state { ACTIVE, // BAD - zero-initialized enum potentially
INACTIVE, // leads to implicit state changes.
DELETED
};
enum state { INVALID, // GOOD - zero-initialized enum produces
ACTIVE, // an invalid value by default, avoiding
INACTIVE, // an implicit state change.
DELETED
};
enum state { ACTIVE = 1, // GOOD, zero-initialization fails because
INACTIVE = 2, // it’s not a valid enum value to begin with.
DELETED = 3
};
Use natural language for variables and types - expressive code is easier to reason about for maintainers and reviewers and also reduces the mental burden when returning to the same code after even a short absence, as code “says what it does” and variables “tell what they represent”.
int c = 1; // BAD: What does “c” represent?
int count = 2; // BAD: Count of “what”?
int num_bytes = 3; // GOOD: “Num(ber) of bytes”
bool valid; // BAD: Meaning is ambiguous
bool has_valid_key; // GOOD: Describes state of element in an object
bool is_valid; // GOOD: Describes state of element itself
bool did_send_packet; // GOOD: Describes state of transaction.
float dur = 1.0; // BAD: Unit of duration unknown
float duration_ms = 5.0; // GOOD: Unit of duration encoded within name
// GOOD: Function signature communicates the unit of the delay explicitly.
start_transition_with_delay(transition_type *transition, float delay_ms);
Use compound types rather than individual variables - information like the size or dimension of an object, a timescale, a position in space, should be logically encoded in a compound type and only “unwrapped” when consumed
// GOOD: Function signature communicates the unit of the delay explicitly.
start_transition_with_delay(transition_type *transition, float delay_ms);
// EVEN BETTER: Time values encoded as pieces of a fraction (1/1000th of a
// second representing a “microsecond”) and explicitly passed to
// the function.
typedef struct {
int_64_t time_value;
int_32_t time_scale;
} time_unit;
start_transition_with_delay(transition_type *transition, time_unit delay);
// BAD: Behavior encoded in unrelated booleans, maybe even conflicting
start_transition(transition_type *transition, bool ignore, bool abort);
// GOOD: Function signature requires more meaningful enum rather than bool
enum transition_abort_mode {
TRANSITION_ABORT_INVALID, TRANSITION_ABORT_ALL,
TRANSITION_ABORT_NEW, TRANSITION_ABORT_NONE
};
// Signature:
start_transition(transition_type *transition, enum transition_abort_mode);
Do not use unnecessary abbreviations - if a class implements an "Advanced Output", call it AdvancedOutput, not AdvOutput. The same applies to variables holding an instance of the class. Code is read more often than it is written (and indeed the writing part is just the final outcome of a much longer reasoning process) and optimising for the latter incurs a debt on the former.
Name functions after what they do rather than how they do it - a function’s signature is effectively its “interface” and should provide a hint about which functionality it provides to the caller. The actual implementation should not be relevant to the caller and exposing this information can actually have a net-negative effect as the caller might try to “out-think” the API.
Prefer pure functions and non-mutable variables - side-effects are harder to keep track of and can lead to confusing results, particularly when following the prior rule. This also ties into a later rule about avoiding global scope, as global (and shared) variables tend to encourage writing code that surreptitiously changes global shared scope, which makes code harder to test and reason about.
[!NOTE] This obviously does not apply to methods where the implementation detail is a technical need of the API user e.g., for factory methods where the user has data in a specific place that it needs to be loaded from, such as
MyData::loadFromUrl(const std::string &url)orMyData::loadFromFile(const std::filesystem::path &path)where the “what” is essentially intertwined with the “how”.
Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
Write code that is guaranteed to work, not code that doesn’t seem to break.
The last point cannot be overstated because it is a common source of friction in long-lived software projects. Neither source code nor documentation can be a full representation of the “theory” behind the code and indeed both can never be more than an incomplete “snapshot” of one possible implementation of this theory.
But without a decent understanding of the theory behind the code (or the architectural concerns that went into the current design) one cannot correctly ascertain which changes will be “in line” with the way current code works and might instead violate important principles of it.
And the more such “naive” code (mainly in the form of hacks and workarounds) is piled on, the more difficult the code becomes to work with, culminating in a code base where even shipping “simple” new features becomes a tough exercise (as the code starts to behave in unpredictable ways due to all the violations of the original “theory”).
[!NOTE] This principle becomes more obvious with less abstract examples: An image-editing program will have a set of considerations and constraints that went into its design (code and user interface) and just adding the ability to decode video files will not make the program able to edit video, which is an entirely separate discipline with potentially opposing needs and considerations.
And indeed any new feature added to the photo editor now has to potentially contend with the reality that it might be faced with a video file instead, adding even more bits and pieces to unrelated parts of the program to handle a scenario the foundation of the program was conceptually (and explicitly) not built for.
OBS Studio currently contains code written in the following languages:
Continuous integration code is mostly based on Powershell, Zsh or Bash scripts, and Python 3.
While C++ and ObjectiveC/C++ are supersets of the C language, the project prefers to treat them individually with their own rules, conventions, and language standards. Thus rules that apply to “C” do not necessarily apply to these languages and indeed some rules will be replaced for those languages.
For “pure” C code the project follows the Linux Kernel Coding Style (https://github.com/torvalds/linux/blob/master/Documentation/process/coding-style.rst). Parts of the guidelines that relate to Emacs, kernel-level allocators, or macros only available in the Linux kernel source code do not apply.
[!IMPORTANT] The current C language standard of the project is C17 without GNU extensions.
Some additional notes:
clang-format. The formatting generated by it supersedes any rules in the guidelines.The project treats C++ as its own language and not just as “C with classes”. The associated code style guidelines are based on the Google C++ Code Style Guide (https://google.github.io/styleguide/cppguide.html) with changes to some aspects, as listed below.
[!IMPORTANT] The current C++ language standard of the project is C++17 without GNU extensions. A move to C++20 is currently being considered.
Additions and changes (in the order the associated topics appear in the Google document):
#pragma once is sufficientstd::vector.cpplintconst to signal that they logically mutate some state.camelCase for instance methods, functions, as well as variables. This also aligns with Qt’s code style.UPPERCASE names, but new constants should follow the new convention of kCamelCase.OBS” namespace for refactoring of application code for the time being.clang-format. The formatting generated by it supersedes any rules in the guidelines.std::vector)Some additional notes:
std::regex).
std::array over C arrays)class enum instead of enum for enumeration values and do not use enum as “integer” value aliases.[!IMPORTANT] The “
static” keyword is one of the more confusing aspects of modern C++, particularly compared to C. The meaning of the keyword changes depending on whether it is used for a function, a global variable, a function-scope variable, a class method, or a class member.In general limit its use in C++ code to describe “storage duration” of variables or for class methods (e.g. factory methods). Use thread-safe functions to initialize a function-local static variable or class member (to prevent possible race conditions when the variable is initialized). Also be mindful of the “Static Initialization Order Fiasco”. Otherwise all the common established pitfalls and issues of global variables apply.
constexprdefinitions do not need to use the static keyword.constexprimplies const and const implies static storage duration by default.Be careful when mixing
staticandinline, as the inline keyword (short for “defined in-line”) allows the same definition to exist in multiple translation units (and thus is allowed to violate the one-definition rule, or “ODR”), enabling the linker to deduplicate all instances of the same function, which is the exact opposite of what “static” requires (local visibility and thus a distinct copy of the function in each translation unit).
Many of Qt’s core concepts were invented years before C++ added support for similar ideas, which means that much code interacting with Qt library functions and QObject-based instances requires violating some of the core language principles outlined above:
delete on a widget owned by a parent widget. Do not create widgets without attaching them to a parent widget.std::string instances.Prefer to include the headers that the implementation itself needs (e.g. because a type or definition is used directly in the implementation) and do not rely on another header possibly having included the same file already. This follows the “include what you use” rule.
When including headers, use the following order:
// Interface definition or “counterpart” of current file
#include “interface.h”
// File in the same directory as the current file _if_ header belongs to the
// same “implementation”.
#include “file_in_working_directory.h”
// First party dependency from the same larger project that is “linked” with
// the implementation
#include <first_party_dependency/type_or_interface.h>
// Third party dependency not part of the same project and “linked” with the
// implementation.
#include <third_party_dependency/type_or_interface.h>
// C++ standard library includes
#include <string>
// C standard library includes
#include <sys/socket.h>
[!NOTE] This specific order of includes helps in identifying potentially “broken” headers without “masking” the issue by including potential dependencies first.
That issue can be avoided by following a simple procedure when creating a new interface and implementation pair: The initial implementation should always include the “counterpart” interface header first and should compile just fine even with the “empty” implementation.
The same applies to any first party and third party library headers: Including one by itself should not result in compilation issues or should not require any standard library headers being included first (if that’s the case, the corresponding library header is badly designed).
Putting the standard library includes last (and only including them if the implementation needs them) helps expose such malformed header files. While this scheme relies on convention (rather than enforcement by the language) it leads to self-contained headers and a cleaner set of includes.
Additional rules for this guideline:
Some header files will require breaking these rules. Well-known examples include:
windows.h might be required very early in an implementation, particularly when relying on the magic WIN32_LEAN_AND_MEAN define, as nested includes of the same header might break compilation in severe ways otherwise..libprocstat.h requires additional headers to be included in a specific order before its own inclusion (as documented in its man page).For Objective-C/C++ code the Google Objective-C Style Guide (https://google.github.io/styleguide/objcguide.html) should be followed, which itself is based on Apple’s Cocoa Coding Guidelines and Programming with ObjC Conventions.
[!IMPORTANT] The current Objective-C/C++ language standard of the project is Objective-C 2.0.
The additions and changes to the Google Style Guide are as follows (in the order the associated topics appear in the Google document):
camelCase for functions.g prefix for global variables in file scope is permitted.#import for all includes, follow the include order as specified for C/C++.clang-format. The formatting generated by it supersedes any rules in the guidelines.For Swift code the Google Swift Style Guide (https://google.github.io/swift/) should be followed as well as Apple’s Swift API Guidelines (https://www.swift.org/documentation/api-design-guidelines/).
[!IMPORTANT] The current Swift language standard of the project is Swift 6.
The additions and changes to the Google Style Guide are as follows (in the order the associated topics appear in the Google document):
swift-format. The formatting generated by it supersedes any rules in the guidelines.Some additional notes:
passRetained to pass an opaque pointer with an incremented reference count to a C API.takeRetained to take ownership of that reference count in Swift code and allow normal lifetime management to take over.passUnretained and takeUnretained to pass/receive pointers without influencing their reference count.Use extension to implement protocols in code blocks separate from the core type implementation.
class MyType {
// Basic implementation
fileprivate let memberVariable: String
init(argumentOne: String) {
self.memberVariable = argumentOne
}
}
extension MyType : SomeProtocol {
func someProtocolMethod(argument: String) -> String {
return “\(memberVariable) \(argument)”
}
}
Prefer functional patterns as much as possible over C-style loops.
When a loop is still preferable, use the native Range type and enumerated loops if a counter value is required:
for i in 0..<someValue {
doTheThing()
}
for (i, theThing) in theCollection.enumerated() {
doTheThing(with: theThing);
print("I am on iteration \(i) here...")
}
0NSString, NSDictionary, NSArray, NSSet, and othersString, Dictionary, Array, Set, and othersCMake underwent a big philosophical change with version 3 of the build system generator, which established the notion of “targets” with associated “target properties” that describe a target’s needs, including compiler arguments, linker flags, preprocessor definitions, and more.
The project’s build system was rewritten a few years ago to make full use of these “modern” CMake patterns and approaches, and thus requires all new CMake code to follow the same principles:
UPPER_SNAKE_CASE for cache variables, global variables, and file-local variables
snake_case for function names and function-local variables_UNDERSCORE_PREFIX for "private" variables that need to exist in the global or file-local scope[!NOTE] Macros differ from functions in that they do not have a function-local scope. A macro body is effectively put into the scope it is called from and thus shares the variable scope.
${} dollar expansion to pass their valueIt is important to remember that CMake does not represent “the build system”, but rather is a “build system generator” that translates the abstract dependency tree of “targets” into an actual build system or IDE project.
Both file formats are predominantly used for build system configuration or continuous integration, but are still subject to a limited set of rules:
camelCase for variable names
dash-case for the job names.UPPER_SNAKE_CASE names.