DEV Community

drflamemontgomery
drflamemontgomery

Posted on

I Built a GCC Plugin for Struct Inheritance

If you have entered the world of programming, there is a fair chance that you have heard of C or learned C. As a language, C is very robust and simple language but it is lagging behind the newer languages that have more features and buttons. Which is why I took it upon myself to begin a journey of adding more features via GCC Plugins.

Boilerplate

To begin with, we need a c file to contain all our plugin code. So we will begin with the basic plugin boilerplate.

// plugin.c
#include "gcc-plugin.h"
#include "plugin-version.h"

#include "diagnostic.h"

// This is required for GCC to accept the plugin
int plugin_is_GPL_compatible;

// This is where we will register the plugin hooks  
int plugin_init(struct plugin_name_args *plugin_info,
                struct plugin_gcc_version *version) {
  // It's good practice to check for a compatible gcc version
  if (!plugin_default_version_check(version, &gcc_version)) {
    return 1;
  }

  // A little message so that we know the plugin loads
  warning(UNKNOWN_LOCATION, "Hello from %s", plugin_info->base_name);

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

With our plugin.c boilerplate, it's time to create a Makefile so that we can compile our plugin

# Makefile
HOST_GCC=g++
TARGET_GCC=gcc
PLUGIN_SOURCE_FILES= plugin.c

# Get the GCC plugin directory for referencing later
GCCPLUGINS_DIR:= $(shell $(TARGET_GCC) -print-file-name=plugin)
CXXFLAGS+= -I$(GCCPLUGINS_DIR)/include -fPIC -fno-rtti -O2

default: embedstruct.so

embedstruct.so: $(PLUGIN_SOURCE_FILES)
    $(HOST_GCC) -shared $(CXXFLAGS) $^ -o $@

.PHONY: clean
clean:
    rm -f embedstruct.so
Enter fullscreen mode Exit fullscreen mode

We can now build our project but it's useless to build it without a test program.

// test.c
#include <stdio.h>

int main(void) {
  printf("Hello World!\n");
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Now to add an extra Makefile rule to build the test program.

# Makefile
...

test: embedstruct.so
    $(TARGET_GCC) -fplugin=./embedstruct.so test.c -o test

...

clean:
    rm -f embedstruct.so
    rm -f test
Enter fullscreen mode Exit fullscreen mode

Initial Test

Now that we have our boilerplate out of the way, we can test to see if our plugin compiles and loads correctly

$ make
g++ -shared -I/usr/local/lib/gcc/x86_64-pc-linux-gnu/16.0.0/plugin/include -fPIC -fno-rtti -O2 plugin.c -o embedstruct.so
$ make test
gcc -fplugin=./embedstruct.so test.c -o test
cc1: warning: Hello from embedstruct
$ ./test
Hello World!
Enter fullscreen mode Exit fullscreen mode

Success! Our code compiles and runs correctly.

Adding the Attributes

Now that our foundation is out of the way and working, we can start by adding our attribute so that GCC doesn't throw an error every time we try to use it.

Step 1

Register our plugin to the attribute registration callback

int plugin_init(...) {
  ...

  register_callback(
    plugin_info->base_name, // Name of our plugin
    PLUGIN_ATTRIBUTES, // The callback to hook into
    register_attributes, // Our callback we will create
    NULL // We don't have any user data
  );

  ...
}
Enter fullscreen mode Exit fullscreen mode

Step 2

Define an attribute_spec for our attribute

static struct attribute_spec embed_struct_attr = {
  "embed_struct", // Name of attribute used in C code
  0, // minimum of 0 arguments
  0, // maximum of 0 arguments
  true, // This attribute only applies to declarations 
  false, // doesn't apply to type
  false, // doesn't apply to function types
  false, // doesn't affect type identity
  handle_embed_struct_attr, // callback to attribute processing
  NULL, // we don't have any exclusions
}; 
Enter fullscreen mode Exit fullscreen mode

Step 3

Add boilerplate attribute handling callback

static tree handle_embed_struct_attr(tree *node, tree name, tree args,
                                     int flags, bool *no_add_attrs) {
  // Shockingly, most of the code that we write
  // will not be in this block
  warning(UNKNOWN_LOCATION, "attribute 'embed_struct' not implemented");
  return NULL_TREE;
}

Enter fullscreen mode Exit fullscreen mode

Step 4

Register our attribute to GCC

static void register_attributes(void *event_data, void *data) {
  // Tell GCC that we have a custom attribute
  register_attribute(&embed_struct_attr);
}
Enter fullscreen mode Exit fullscreen mode

Step 5

Add tree.h to the list of headers

Testing

Let's modify our test.c code so that we use our custom attribute

#include <stdio.h>

// Parent struct type
struct s1 {
  int field1;
  int field2;
};

// Child struct type
struct s2 {
  // Our parent type with the embed_struct attribute
  [[gnu::embed_struct]]
  struct s1 super;

  int field3;
};

int main(void) {
  struct s2 test;
  test.super.field1 = 0;
  test.super.field2 = 1;
  test.field3 = 2;

  printf("super.field1: %d\n", test.super.field1);
  printf("super.field2: %d\n", test.super.field2);
  printf("field3: %d\n", test.field3);

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Now to compare before and after registering the attribute

# Before
$ make test
gcc -fplugin=./embedstruct.so test.c -o test
cc1: warning: Hello from embedstruct
test.c:13:10: warning: 'gnu::embed_struct' scoped attribute directive ignored [-Wattributes]
   13 |   struct s1 super;
      |          ^~
$ ./test
super.field1: 0
super.field2: 1
field3: 2
Enter fullscreen mode Exit fullscreen mode
# After
$ make test
gcc -fplugin=./embedstruct.so test.c -o test
cc1: warning: Hello from embedstruct
test.c:13:10: warning: attribute 'embed_struct' not implemented
   13 |   struct s1 super;
      |          ^~
$ ./test
super.field1: 0
super.field2: 1
field3: 2
Enter fullscreen mode Exit fullscreen mode

The difference isn't much, however the warning we get after registering is the warning that WE decided to create because we haven't finished the attribute

Finish Type Pass

To implement our new type, we need to do 4 steps.

3.

  1. Remove the embedstruct attribute

Step 1

Hook into the FINISH_TYPE callback. This will allow us to change the type structure.

int plugin_init(...) {
  ...

  register_callback(
    plugin_info->base_name, // Name of our plugin
    PLUGIN_FINISH_TYPE, // The callback to hook into
    finish_type, // Our callback we will create
    NULL // We don't have any user data
  );

  ...
}
Enter fullscreen mode Exit fullscreen mode

Step 2

Check each field for the embedstruct attribute

static void finish_type(void *event_data, void *data) {
  // Get the current type data
  tree type = (tree)event_data;

  // We don't like segmentation faults so we will check
  // for NULL pointers
  if (!type)
    return;

  // We only want this to work inside of `struct` types
  if (TREE_CODE(type) != RECORD_TYPE)
    return;

  // Get the list of every struct field
  tree *prev = &TYPE_FIELDS(type);

  while (*prev) {
    tree field = *prev;

    // If this isn't a declared struct field then we don't
    // want to test it
    if (TREE_CODE(field) != FIELD_DECL) {
      prev = &DECL_CHAIN(field);
      continue;
    }

    // Check if our attribute is attached
    if (lookup_attribute("embed_struct", DECL_ATTRIBUTES(field))) {
      // TODO add anonymous structure at same location
    }

    prev = &DECL_CHAIN(field);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3

Add an anonymous struct of the same type and the same location. Easiest way to do this is to copy the existing field without it's declared name and add it to the field list

static tree copy_field_anonymous(tree field, tree context) {
  // Create a declaration node
  tree new_field = build_decl(DECL_SOURCE_LOCATION(field), FIELD_DECL,
                              NULL_TREE, TREE_TYPE(field));

  // Copy the field data
  DECL_FIELD_BIT_OFFSET(new_field) = DECL_FIELD_BIT_OFFSET(field);
  DECL_ALIGN_RAW(new_field) = DECL_ALIGN_RAW(field);

  SET_DECL_OFFSET_ALIGN(new_field, DECL_OFFSET_ALIGN(field));
  DECL_NOT_FLEXARRAY(new_field) = DECL_NOT_FLEXARRAY(field);
  DECL_FIELD_OFFSET(new_field) = DECL_FIELD_OFFSET(field);

  DECL_SIZE(new_field) = DECL_SIZE(field);
  DECL_SIZE_UNIT(new_field) = DECL_SIZE_UNIT(field);

  // Point to the same attributes for same effects
  DECL_ATTRIBUTES(new_field) = DECL_ATTRIBUTES(field);

  // Add this to the correct context (sort of similar to a parent)
  DECL_CONTEXT(new_field) = context;

  // Remove this from any list so we can move it wherever we want
  DECL_CHAIN(new_field) = NULL_TREE;

  // Let the compiler know that this was a computed declaration
  DECL_ARTIFICIAL(new_field) = 1;

  return new_field;
}


static void finish_type(void *event_data, void *data) {
  ...
    if (lookup_attribute("embed_struct", DECL_ATTRIBUTES(field))) {
      // Copy the field as an anonymous struct in the same context
      tree anon_field = copy_field_anonymous(field, DECL_CONTEXT(field));

      // Add anonymous field to the list
      DECL_CHAIN(anon_field) = DECL_CHAIN(field);
      DECL_CHAIN(field) = anon_field;

      prev = &DECL_CHAIN(anon_field);

      // TODO remove attribute
      continue;
    }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Step 4

While technically unnecessary, I still prefer to remove the attributes after processing.

static void remove_attribute(tree node, const char *attribute) {
  // Iterate over attribute list
  for (tree *attr_ptr = &DECL_ATTRIBUTES(node); *attr_ptr;) {
    if (is_attribute_p(attribute, get_attribute_name(*attr_ptr))) {
      // Remove attribute
      *attr_ptr = TREE_CHAIN(*attr_ptr);
    } else {
      // Check next attribute
      attr_ptr = &TREE_CHAIN(*attr_ptr);
    }
  }
}

static void finish_type(void *event_data, void *data) {
  ...
      prev = &DECL_CHAIN(anon_field);

      // We only need to remove from one of them because
      // their attributes are linked
      remove_attribute(anon_field, "embed_struct");

      continue;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Step 5

Remove the unnecessary warnings

warning(UNKNOWN_LOCATION, "attribute 'embed_struct' not implemented");
warning(UNKNOWN_LOCATION, "Hello from %s", plugin_info->base_name);

Final Test

Now that we have finished our plugin, let's update our test program to check whether it works

#include <stdio.h>

// Parent struct type
struct s1 {
  int field1;
  int field2;
};

// Child struct type
struct s2 {
  // Our parent type with the embed_struct attribute
  [[gnu::embed_struct]]
  struct s1 super;

  int field3;
};

int main(void) {
  struct s2 test;
  test.super.field1 = 0;
  test.super.field2 = 1;
  test.field3 = 2;

  printf("super.field1: %d\n", test.super.field1);
  printf("super.field2: %d\n", test.super.field2);
  printf("field1: %d\n", test.field1);
  printf("field2: %d\n", test.field2);
  printf("field3: %d\n", test.field3);

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Time for the big reveal

$ make test
g++ -shared -I/usr/local/lib/gcc/x86_64-pc-linux-gnu/16.0.0/plugin/include -fPIC -fno-rtti -O2 plugin.c -o embedstruct.so
gcc -fplugin=./embedstruct.so test.c -o test
$ ./test
super.field1: 0
super.field2: 1
field1: 0
field2: 1
field3: 2
Enter fullscreen mode Exit fullscreen mode

As you can see, field1 is linked to super.field1, and field2 is linked to super.field2. Now you can do better OOP in C without being forced into the C++ domain.

Summary

Using GCC plugins, you can modify the language to achieve some nice features. Is it worth doing? Probably not, but programming isn't always about doing things the easy way. You can check out my GitHub repository for this project (under a different name. I should probably change it sometime to a better name, but oh well).
gcc-flattenstruct
If I get time in future, I may extend this to have a few more features and better control over the layout.

Note.

Generative AI was used as a starting block for this project however it failed to do proper tasks and the resulting code is mostly mine.

Top comments (0)