Omniverse ABI Checker#
The ABI checker is intended to analyze Carbonite plugins and compare them against a historical record to check if anything has changed would cause an ABI break.
The historical record of ABI is stored as a .cpp file which lists all types used in the
interface. This includes the full chain of types referenced from any interface function until we
reach primitive or stdint types.
This allows effectively anything pulled in from an interface to be validated, including Kit-style
virtual interfaces, structs and typedefs.
Any code that is not relevant to ABI is not recorded in the historical record, so inline methods
will be omitted from the historical record.
This also allows us to validate that the we are not pulling any types from external libraries that
don’t fall into our ABI-stability model, such as std::string.
The historical record is intended to be committed to git so that changes can be reviewed in
merge requests.
When a historical record exists, the ABI checker compares each type referenced in every interface
function to historical record to verify that they are equivalent from an ABI perspective.
This does a deep comparison of all types, so every type they reference is recursively compared as
well.
The types in the interface do not need to be identical to the types in the historical record; they
just need to be equivalent from an ABI perspective. Replacing a uint32_t with a typedef that
evaluates to uint32_t will pass validation.
Once the validation step has passed, any changes to the interface will be written to the historical
record. It is important to keep the historical record up to date because functions can be added to
an interface without breaking the ABI.
If a breaking change is required, the ABI can be force-updated and the historical record will be rewritten. The changes to the ABI can be reviewed in a merge request.
When a breaking ABI change is detected, this will be reported through junit, so the result can be viewed on the gitlab tests UI.
Implementation#
The ABI checker is integrated into premake from the abi_check.lua module.
-- Initializes the ABI module and also adds the --abi-check and --abi-update parameters to premake.
dofile(carbSdkPath..'/tools/abi-check/abi_check.lua')
-- Create an ABI checker instance.
-- First parameter points to the Carbonite SDK directory
-- The second parameter points to the directory where the ABI record will be written to.
-- This directory is expected to be checked into git, so future runs can compare against it.
-- The third parameter points to the directory where the junit.xml result files will be written.
local abi_checker = create_abi_checker(carbSdkPath, "abi", "_build/abi_junit")
The ABI checker is built into Carbonite’s premake-tools lua module. This module has an optional
abi_checker argument that will cause each plugin specified to automatically run the abi checker
step.
dofile('tools/premake-tools.lua')
-- ABI checking will be enabled on all plugins.
local plugin_tools = create_plugin_tools(workspaceDir, abi_checker)
Once the premake side has been set up, the CI builds just need to pass the --abi-check parameter
to the build and have the junit directory added to the reports in your CI yaml.
This will enable full ABI checker functionality.
.build:
stage: build
artifacts:
paths:
- _build/abi_junit/*.xml
The current version of the ABI checker will not fail the CI pipeline if it detects an ABI break.
If you want the pipeline to fail when the ABI is broken, you can use the script found here:
tools/abi-check/junit-passed.py, which will read through the produced xml files and fail if
any test cases have failed. This should be run after the build so that you can collect all ABI
breakages in one CI run.
What is Considered ABI Breakage#
The tool considers the ABI to be broken when code linked against a previous version of an interface is no longer binary compatible with the current interface. For a Carbonite interface, this means:
No existing functions can be removed from an interface.
The ordering of functions in an interface cannot change.
Signatures of functions in an interface cannot change in any way other than benign type alias changes.
New functions can be added to the end of an interface. * This also applies to kit-style interfaces.
For any type used in a function of Carbonite interface, this means:
- The width of the type cannot change.
ABI-safe ways to expand structs may be added in the future.
- The interpretation of the type cannot change.
Changing the ordering of members in a struct is an interpretation change.
For the current implementation, a change from signed to unsigned integer of the same width is considered and ABI break.
- The alignment of the type cannot change.
Checking for this is not implemented yet.
- Struct padding cannot change.
Checking for this is not implemented yet.
Interfaces should not be modifying struct padding in general.
Structs and interfaces cannot be expanded if they have been inherited from.
The ABI Checker currently only enforced backward compatibility. Future versions will enforce forward compatibility, which will just enforce that expanded APIs are gated behind a version increase to prevent a plugin that’s too old from being loaded. Currently this needs to be done manually. Note that this only applies to Carbonite and Kit-style interfaces; omni.bind interfaces cannot be expanded once they are released.
Example ABI Breakage Detections#
These are some sample breakages from the unit tests:
Example 1: parameter type changed:
ERROR: types are incompatible 'int32_t' and 'int64_t'
code from header: /tmp/tmpqkaou51q/include/ITestAbiCheck.h:6:
6 struct ITestAbiCheck
7 {
8 CARB_PLUGIN_INTERFACE("test::abi_check::ITestAbiCheck", 1, 0)
9
10 > void (*foo)(int64_t b);
11
12 };
code from spec: /tmp/tmpqkaou51q/abi/test_abi_check_ITestAbiCheck.cpp:10:
10 struct ITestAbiCheck
11 {
12 > void (*foo) (int32_t a);
13 };
Example 2: type change in a struct used by an ABI function:
ERROR: types are incompatible 'int32_t' and 'int64_t'
code from header: /tmp/tmp9fda6jtp/include/ITestAbiCheck.h:7:
7 struct TestStruct
8 {
9
10 > int64_t foo;
11 const char* bar;
12 char baz[32];
13
14 };
code from spec: /tmp/tmp9fda6jtp/abi/test_abi_check_ITestAbiCheck.cpp:10:
10 struct TestStruct
11 {
12 > int32_t foo;
13 const char *bar;
14 char baz[32];
15 };
Type is part of the ABI from type chain:
from /tmp/tmp9fda6jtp/include/ITestAbiCheck.h:21:
21 > other::TestStruct (*foo)();
Exmaple 3: changes to a Kit-style interface used in a Carbonite interface:
ERROR: 1 methods were removed from the struct
code from header: /tmp/tmpm4h11lcw/include/ITestAbiCheck.h:6:
6 class IFoo
7 {
8
9 virtual void foo() = 0;
10 virtual const char* bar(const char* fmt, ...) = 0;
11
12 };
code from spec: /tmp/tmpm4h11lcw/abi/test_abi_check_ITestAbiCheck.cpp:10:
10 class IFoo
11 {
12 virtual void foo () = 0;
13 virtual const char * bar (const char *fmt, ...) = 0;
14 virtual void * baz (const char *, void (*callback) (const char *, void *)) = 0;
15 };
Type is part of the ABI from type chain:
from /tmp/tmpm4h11lcw/include/ITestAbiCheck.h:16:
16 > IFoo* (*getFoo)();
Example 4: enum type changes:
ERROR: types are incompatible 'unsigned int' and 'unsigned long'
code from header: /tmp/tmpma61fczm/include/ITestAbiCheck.h:8:
8 enum TestEnum
9 {
10 A,
11 B,
12 C,
13 D = 0x200000000000,
14 };
code from spec: /tmp/tmpma61fczm/abi/test_abi_check_ITestAbiCheck.cpp:10:
10 enum TestEnum
11 {
12 A,
13 B,
14 C,
15 };
Type is part of the ABI from type chain:
from /tmp/tmpma61fczm/include/ITestAbiCheck.h:23:
23 > other::TestEnum (*foo)();
Example 5: adding methods to an inherited struct:
ERROR: method 'added' was added to 'IFoo' which is embedded in an ABI struct
code from header: /tmp/tmp8z45xfks/include/ITestAbiCheck.h:6:
6 class IFoo
7 {
8
9 virtual void foo() = 0;
10 virtual const char* bar(const char* fmt, ...) = 0;
11 virtual void* baz(const char*, void (*callback)(const char*, void*));
12 virtual void added(int32_t a) = 0;
13
14 };
code from spec: /tmp/tmp8z45xfks/abi/test_abi_check_ITestAbiCheck.cpp:10:
10 class IFoo
11 {
12 virtual void foo () = 0;
13 virtual const char * bar (const char *fmt, ...) = 0;
14 virtual void * baz (const char *, void (*callback) (const char *, void *)) = 0;
15 };
Type is part of the ABI from type chain:
from /tmp/tmp8z45xfks/include/ITestAbiCheck.h:15:
15 > class IFoo2: public IFoo
16 {
17 };
from /tmp/tmp8z45xfks/include/ITestAbiCheck.h:21:
21 > IFoo2* (*getFoo)();
Current limitations#
- Omni.bind interfaces are not supported yet.
This will be supported eventually.
Interface versioning is not enforced by the ABI checker yet.
- Field alignment is not yet checked.
This only matters when Alignas() is used.
- Templates are not fully supported.
A lot of validation is skipped on templated types.
Templates will never be fully supported because the tool can’t evaluate constexpr functions.