Carbon/C++ Interop: Importing C/C++ object-like macros
Table of contents
Abstract
This proposal addresses importing object-like C/C++ macros into Carbon.
Problem
C/C++ object-like macros are used to define constants, conditional compilation, header guards, etc. and are common for low-level, cross-platform libraries, with C like APIs. Despite the recommendations for replacing them in C++ with safer techniques (for example constexpr), they remain to be present in C++ code bases. Of particular interest for the interoperability are cases such as macros present in the APIs of the standard C++ library (for example error codes in <errno.h>), which can’t be easily changed, but remain to be widely used in low-level C++ libraries. A mechanism for properly importing and handling these macros is necessary for a seamless interoperability.
Background
Macros in C++
Macros are defined with the #define directive and processed by the preprocessor, which replaces the occurrences of the defined identifier with a replacement-list. There are two types of macros: object-like and function-like macros. The directive #undef undefines a defined macro.
Object-like macros
#define identifier replacement-list (optional)
Replacement-lists can have elements that are:
- Literals: integer, floating-point, string, char, bool etc.
- Non-literal types: structs, classes etc.
- Identifiers: pointing to other macros, variables, types, keywords etc.
- Operators:
+, -, <<, >>, |etc. with one or more operands.
Function-like macros
#define identifier(parameters) replacement-list (optional)
#define identifier(parameters, ...) replacement-list (optional)
#define identifier(...) replacement-list (optional)
The syntax of function-like macros is similar to the syntax of a function call. They accept a list of arguments and replace their occurrence in the replacement-list.
As this is only a text replacement, it can lead to unexpected behaviour if the arguments are not properly separated with brackets.
In function-like macros, the operators # and ## enable:
-
#operator (stringification)- the arguments of the macro are converted to a string literal without expanding the argument. When#is used in front of a parameter in a macro definition (#param), the preprocessor replaces#paramwith the argument tokens as a string literal. -
##operator (concatenation or token pasting)- two tokens are merged in a single token during a macro expansion. For example, whena##bis used as part of the macro definition, the preprocessor mergesaandb, removing any white spaces in between to form a single token. When some of the tokens on either side of the##operator are parameter names, they are replaced by the actual argument before the execution of##. This is used for example to create new identifiers like variables, function names etc. and in general to avoid creating repeated boilerplate code.
Predefined macros
There are also predefined macros available in every translation unit. Examples include: __cplusplus, __FILE__, __LINE__, __DATE__, __TIME__ etc.
Swift / C interop [GitHub][documentation]
Swift supports importing object-like C macros as global constants. Macros that use integer, floating-point and string literals are supported. Also simple operators like +, -, <<, >> etc. between literals or macros are supported.
Function-like macros are not supported. Instead, using Swift functions and generics is recommended.
Proposal
An object-like macro that evaluates to a constant expression is imported from C++ as a constant in Carbon. For example:
C++:
#define BUFFER_SIZE 4096
Carbon:
let a: i32 = Cpp.BUFFER_SIZE; // Cpp.BUFFER_SIZE is imported as an int value of type i32 with a value of 4096
Details
Namespace
The macro is evaluated in the global Cpp namespace and accessible as Cpp.BUFFER_SIZE.
Constant type
The type of the imported constant is deduced by Clang by evaluating the constant expression and then mapped to a Carbon type following the existing Carbon <-> C++ type mapping rules.
Constant value
The value is deduced by evaluating the tokens of a replacement list as a constant expression.
Constant expressions
The replacement list in the object-like macro expanding to a constant expression can contain:
- Operators: arithmetic:
+, -, *, /; bitwise:|, &, ^, <<, >>; logical:||, &&; comparison:<, >, <=, >=, ==; casts etc, with arbitrary number of operands.
For example:
#define ADDITION 1+2+3
- Chained macros: macros that expand to another macro which evaluates to a constant.
For example:
#define VALUE 123
#define MY_VALUE VALUE
- Enum constants and
constexprvariables: if a macro’s replacement list refers to a named constant, such as an enum constant or aconstexprvariable, it is imported as an alias rather than as a literal value. This allows Carbon to preserve the specific type of the constant (such asColorin the example below). In the case ofconstexprvariables, importing as an alias also preserves addressability (that the constant is an lvalue), which would be lost if only the value were imported.
For example:
C++:
enum class Color { Red = 1, Green = 2 };
#define GREEN_COLOR Color::Green
constexpr int kValue = 123;
#define VALUE kValue
Carbon:
// Cpp.GREEN_COLOR is an alias to Cpp.Color.Green which has a type Cpp.Color.
let b: Cpp.Color = Cpp.GREEN_COLOR;
// Cpp.VALUE is an alias to kValue.
let a: i32 = Cpp.VALUE;
The macro will be evaluated by default in the global namespace (for example Cpp.VALUE). Evaluating it in a child namespace (Cpp.SomeNamespace.VALUE) may also be possible, though details about that are outside of the scope of this proposal.
Empty macros
The macro should have at least one value in the replacement-list so that it’s imported. For example, the following macro won’t have a Carbon equivalent:
#define EMPTY
Implementation
-
Name lookup: When a C++ macro name is encountered in Carbon it is looked-up before any other name. Following the C++ rules, this allows the macro to be found in case there is a non-macro with the same name (for example named variable).
-
Macro import: If a macro is found, it is imported as a constant to Carbon, by parsing the tokens of the replacement list to a constant expression and evaluating the result.
For example, given a macro:
#define MY_MACRO <some tokens>
The following constexpr will be (effectively) generated and imported:
constexpr inline decltype(auto) __carbon_import_MY_MACRO = (MY_MACRO);
That is, we would try to generate such a constexpr and import the macro if that succeeds.
Rationale
This work contributes to the Carbon’s goal for seamless interoperability with C++ (Interoperability with and migration from existing C++ code), by enabling reusing and migrating existing macros, while adhering to the best practices to use constant variables.
Alternatives considered
-
Implementation
a) Reuse Swift/C implementation
- Reason to do this: use existing implementation.
- Reason not to do this: this implementation is much more limited, longer and harder to maintain than the proposed one, as it manually scans the replacement tokens and tries to evaluate them, instead of using Clang for that. Limitations include for example type of supported operators, number and type of operands etc. Also, due to the licensing, it is not directly available.
b) Manually scanning tokens (own implementation)
- Reason not to do this: this is a much longer implementation compared to the proposed one, with much more limitations compared to using Clang to do that.
-
Importing macros that refer to enum constants and
constexprvariables as constant values, instead of aliases in Carbon- Reason to do this: This would be a simpler implementation and consistent with how other object-like macros are imported.
- Reason not to do this: This would not preserve the enum’s type (for example,
Color::Greenwould be imported as an integer literal2instead of as a constant of typeColor) or the addressability ofconstexprvariables (which are lvalues).
Open questions
Object-like macros
The support for the following cases still needs to be clarified:
- non-constant variable name
int x;
#define VAL x
- types:
#define SHORT_TYPE short
- Statements or keywords:
#define RET return
#define FOREVER for(;;)
- Expanding macros in child namespaces (
Cpp.SubNamespace.MACRO)
Function-like macros
The support for function-like macros will still need to be discussed in detail. The current proposal could be extended for function-like macros in the following direction:
Given a function-like macro:
#define MY_MACRO(a, b) ((a) + (b))
A function template whose body invokes the macro could be (eventually) generated and imported:
constexpr decltype(auto) __carbon_import_MY_MACRO(auto a, auto b) {
return (MY_MACRO(a, b));
}
Predefined macros
Details on the support for predefined macros remains open as well.