Getting custom C++ attribute specifiers from AST

I use non-standard attribute specifier sequences in my C++ code to annotate functions with specific properties.

I now would like to use Clang (via the Python bindings to libclang) to extract and analyze information about these attributes, however I have not found a way to get this information.

I am not sure if this is something that is not available in the Python bindings or if unknown attributes are filtered out at a higher level, but I am a bit worried because I do not see them in the AST at all.

Example that produces an attribute in the AST:

[[noreturn]] void foo() {
    while (true) {};
}
$ clang -Xclang -ast-dump -fsyntax-only test.cpp -Wno-attributes
TranslationUnitDecl 0xff31a8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0xff3a80 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0xff3740 '__int128'
|-TypedefDecl 0xff3af0 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0xff3760 'unsigned __int128'
|-TypedefDecl 0xff3e68 <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
| `-RecordType 0xff3be0 '__NSConstantString_tag'
|   `-CXXRecord 0xff3b48 '__NSConstantString_tag'
|-TypedefDecl 0xff3f00 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0xff3ec0 'char *'
|   `-BuiltinType 0xff3240 'char'
|-TypedefDecl 0x1030bd8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag [1]'
| `-ConstantArrayType 0x1030b80 '__va_list_tag [1]' 1 
|   `-RecordType 0xff3ff0 '__va_list_tag'
|     `-CXXRecord 0xff3f58 '__va_list_tag'
`-FunctionDecl 0x1030c78 <test.cpp:1:14, line:3:1> line:1:19 foo 'void ()'
  |-CompoundStmt 0x1030df8 <col:25, line:3:1>
  | |-WhileStmt 0x1030dd8 <line:2:5, col:19>
  | | |-CXXBoolLiteralExpr 0x1030db8 <col:12> 'bool' true
  | | `-CompoundStmt 0x1030dc8 <col:18, col:19>
  | `-NullStmt 0x1030df0 <col:20>
  `-CXX11NoReturnAttr 0x1030d18 <line:1:3>

The AST contains a CXX11NoReturnAttr

Example that produces no attribute in the AST:

[[custom::noreturn]] void foo() {
    while (true) {};
}
$ clang -Xclang -ast-dump -fsyntax-only test.cpp -Wno-attributes
TranslationUnitDecl 0xef01a8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0xef0a80 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0xef0740 '__int128'
|-TypedefDecl 0xef0af0 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0xef0760 'unsigned __int128'
|-TypedefDecl 0xef0e68 <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
| `-RecordType 0xef0be0 '__NSConstantString_tag'
|   `-CXXRecord 0xef0b48 '__NSConstantString_tag'
|-TypedefDecl 0xef0f00 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0xef0ec0 'char *'
|   `-BuiltinType 0xef0240 'char'
|-TypedefDecl 0xf2dbd8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag [1]'
| `-ConstantArrayType 0xf2db80 '__va_list_tag [1]' 1 
|   `-RecordType 0xef0ff0 '__va_list_tag'
|     `-CXXRecord 0xef0f58 '__va_list_tag'
`-FunctionDecl 0xf2dc78 <test.cpp:1:22, line:3:1> line:1:27 foo 'void ()'
  `-CompoundStmt 0xf2dda0 <col:33, line:3:1>
    |-WhileStmt 0xf2dd80 <line:2:5, col:19>
    | |-CXXBoolLiteralExpr 0xf2dd60 <col:12> 'bool' true
    | `-CompoundStmt 0xf2dd70 <col:18, col:19>
    `-NullStmt 0xf2dd98 <col:20>

Questions

  1. Is there a way to get this information out of the Clang frontend at all?
  2. Is there a way to access it through the Python bindings?
  3. Am I committing a cardinal sin by using non-standard attributes and should I abandon this practice before being arrested?

EDIT:

$ clang --version                               
clang version 10.0.0-4ubuntu1 
Target: x86_64-pc-linux-gnu

That isn’t a ‘custom attribute’, that is an ‘unsupported, and thus not added to the AST attribute’. You ignored the warning that told you about that with -Wno-attributes.

So:

  1. Is there a way to get this information out of the Clang frontend at all?
  2. Is there a way to access it through the Python bindings?

Not as an AST element. You could potentially use the location information for the declaration and poke backwards and look it up in the SourceManager, but that seems like it won’t be particularly good for what you want.

  1. Am I committing a cardinal sin by using non-standard attributes and should I abandon this practice before being arrested?

Arrested? No. Completely unsupported and not really possible? Yes. Typically we send folks to clang::annotate for this sort of information(Compiler Explorer).

1 Like

Thanks, clang::annotate looks exactly like what I need.

I know that there is not really anything like a custom, attribute, I was just hoping that I could somehow access implementation defined (or in this case undefined) attributes via the AST by just getting some kind of “UnknownAttribute” token, but the clang::annotate looks perfect.

Maybe I can even alias clang::annotate("specific property") somehow to make it a bit nicer to use (preferably without macros).

Erich is correct – we don’t retain unknown attributes in the AST. However, you could always make it a known attribute by writing an attribute plugin: Clang Plugins — Clang 16.0.0git documentation

(The reason we don’t retain unknown attributes in the AST is because of difficulties with representing arbitrary token soup for the attribute arguments. e.g., [[custom::whatever(/foo.bar.baz/)]] – the best we could do is keep around the entire list of tokens we read, but that’s not very helpful when the attribute argument is an arbitrary expression.)

2 Likes

Thanks, that might be even more helpful than the clang::annotate attribute in my case. While clang::annotate is in theory enough for my static analysis needs, the only way I was able to alias the attribute to protect from spelling mistakes in the string parameter was to use a #define like

#define MY_INFORMATION [[clang::annotate("my information")]]

MY_INFORMATION void foo() {
    while (true) {};
}

Which on the other hand mystifies the whole construct and does not clearly show that we only add an attribute, not something else.

This works as well, but I find it a bit verbose:

#define MY_INFORMATION "my information"

[[clang::annotate(MY_INFORMATION)]] void foo() {
    while (true) {};
}

So I will check out how much effort it is to build and internally distribute such a plugin as suggested by Aaron, if that is not too difficult I will definitely give it a try, though I will have to see how to get the information through the Python bindings as well.

FWIW, I think you’ll find users doing that in practice anyway, but in a slightly more complicated form. Because no other compiler will recognize that attribute, users will typically try to hide the attribute when it’s not supported. e.g.,

#if __has_cpp_attribute(your::awesome_attribute)
#define AWESOME_ATTRIBUTE(X) [[your::awesome_attribute(X)]]
#else
#define AWESOME_ATTRIBUTE(X)
#endif

AWESOME_ATTRIBUTE("it's the best") void func();
1 Like