Build system

What do we need to think about to manage big source files?

We’ve compiled a small editor so far. But Some bad signs are beginning to appear.

These ideas are useful to manage big source files.

Divide a C source file into two parts.

When you divide C source file into several parts, each file should contain only one thing. For example, our source has two things, the definition of TfeTextView and functions related to GtkApplication and GtkApplicationWindow. It is a good idea to separate them into two files, tfetextview.c and tfe.c.

Now we have three source files, tfetextview.c, tfe.c and tfe3.ui. The 3 of tfe3.ui is like a version number. Managing version with filenames is one possible idea but it may make bothersome problem. You need to rewrite filename in each version and it affects to contents of source files that refer to filenames. So, we should take 3 away from the filename.

In tfe.c the function tfe_text_view_new is invoked to create a TfeTextView instance. But it is defined in tfetextview.c, not tfe.c. The lack of the declaration (not definition) of tfe_text_view_new makes error when tfe.c is compiled. The declaration is necessary in tfe.c. Those public information is usually written in header files. It has .h suffix like tfetextview.h And header files are included by C source files. For example, tfetextview.h is included by tfe.c.

All the source files are listed below.

tfetextview.h

#include <gtk/gtk.h>

#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)

void
tfe_text_view_set_file (TfeTextView *tv, GFile *f);

GFile *
tfe_text_view_get_file (TfeTextView *tv);

GtkWidget *
tfe_text_view_new (void);

tfetextview.c

#include <gtk/gtk.h>
#include "tfetextview.h"

struct _TfeTextView
{
  GtkTextView parent;
  GFile *file;
};

G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);

static void
tfe_text_view_init (TfeTextView *tv) {
}

static void
tfe_text_view_class_init (TfeTextViewClass *class) {
}

void
tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
  tv -> file = f;
}

GFile *
tfe_text_view_get_file (TfeTextView *tv) {
  return tv -> file;
}

GtkWidget *
tfe_text_view_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
}

tfe.c

#include <gtk/gtk.h>
#include "tfetextview.h"

static void
app_activate (GApplication *app, gpointer user_data) {
  g_print ("You need a filename argument.\n");
}

static void
app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
  GtkWidget *win;
  GtkWidget *nb;
  GtkWidget *lab;
  GtkNotebookPage *nbp;
  GtkWidget *scr;
  GtkWidget *tv;
  GtkTextBuffer *tb;
  char *contents;
  gsize length;
  char *filename;
  int i;
  GtkBuilder *build;

  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe3/tfe.ui");
  win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
  gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
  nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
  g_object_unref (build);
  for (i = 0; i < n_files; i++) {
    if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
      scr = gtk_scrolled_window_new ();
      tv = tfe_text_view_new ();
      tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
      gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
      gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);

      tfe_text_view_set_file (TFE_TEXT_VIEW (tv),  g_file_dup (files[i]));
      gtk_text_buffer_set_text (tb, contents, length);
      g_free (contents);
      filename = g_file_get_basename (files[i]);
      lab = gtk_label_new (filename);
      gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
      nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
      g_object_set (nbp, "tab-expand", TRUE, NULL);
      g_free (filename);
    } else if ((filename = g_file_get_path (files[i])) != NULL) {
        g_print ("No such file: %s.\n", filename);
        g_free (filename);
    } else
        g_print ("No valid file is given\n");
  }
  if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
    gtk_widget_show (win);
  } else
    gtk_window_destroy (GTK_WINDOW (win));
}

int
main (int argc, char **argv) {
  GtkApplication *app;
  int stat;

  app = gtk_application_new ("com.github.ToshioCP.tfe", G_APPLICATION_HANDLES_OPEN);
  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
  stat =g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);
  return stat;
}

The ui file tfe.ui is the same as tfe3.ui in the previous section.

tfe.gresource.xml

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/com/github/ToshioCP/tfe3">
    <file>tfe.ui</file>
  </gresource>
</gresources>

Make

Dividing a file makes it easy to maintain source files. But now we are faced with a new problem. The building step increases.

Now build tool is necessary to manage it. Make is one of the build tools. It was created in 1976. It is an old and widely used program.

Make analyzes Makefile and executes compilers. All instructions are written in Makefile.

sample.o: sample.c
    gcc -o sample.o sample.c

The sample of Malefile above consists of three elements, sample.o, sample.c and gcc -o sample.o sample.c.

The rule is:

If a prerequisite modified later than a target, then make executes the recipe.

In the example above, if sample.c is modified after the generation of sample.o, then make executes gcc and compile sample.c into sample.o. If the modification time of sample.c is older then the generation of sample.o, then no compiling is necessary, so make does nothing.

The Makefile for tfe is as follows.

all: tfe

tfe: tfe.o tfetextview.o resources.o
    gcc -o tfe tfe.o tfetextview.o resources.o `pkg-config --libs gtk4`

tfe.o: tfe.c tfetextview.h
    gcc -c -o tfe.o `pkg-config --cflags gtk4` tfe.c
tfetextview.o: tfetextview.c tfetextview.h
    gcc -c -o tfetextview.o `pkg-config --cflags gtk4` tfetextview.c
resources.o: resources.c
    gcc -c -o resources.o `pkg-config --cflags gtk4` resources.c

resources.c: tfe.gresource.xml tfe.ui
    glib-compile-resources tfe.gresource.xml --target=resources.c --generate-source

.Phony: clean

clean:
    rm -f tfe tfe.o tfetextview.o resources.o resources.c

You only need to type make.

$ make
gcc -c -o tfe.o `pkg-config --cflags gtk4` tfe.c
gcc -c -o tfetextview.o `pkg-config --cflags gtk4` tfetextview.c
glib-compile-resources tfe.gresource.xml --target=resources.c --generate-source
gcc -c -o resources.o `pkg-config --cflags gtk4` resources.c
gcc -o tfe tfe.o tfetextview.o resources.o `pkg-config --libs gtk4`

I used only very basic rules to write this Makefile. There are many more convenient methods to make it more compact. But it will be long to explain it. So I want to finish explaining make and move on to the next topic.

Rake

Rake is a similar program to make. It is written in Ruby code. If you don’t use Ruby, you don’t need to read this subsection. However, Ruby is really sophisticated and recommendable script language.

Rake has task and file task, which is similar to target, prerequisite and recipe in make.

require 'rake/clean'

targetfile = "tfe"
srcfiles = FileList["tfe.c", "tfetextview.c", "resources.c"]
rscfile = srcfiles[2]
objfiles = srcfiles.gsub(/.c$/, '.o')

CLEAN.include(targetfile, objfiles, rscfile)

task default: targetfile

file targetfile => objfiles do |t|
  sh "gcc -o #{t.name} #{t.prerequisites.join(' ')} `pkg-config --libs gtk4`"
end

objfiles.each do |obj|
  src = obj.gsub(/.o$/,'.c')
  file obj => src do |t|
    sh "gcc -c -o #{t.name} `pkg-config --cflags gtk4` #{t.source}"
  end
end

file rscfile => ["tfe.gresource.xml", "tfe.ui"] do |t|
  sh "glib-compile-resources #{t.prerequisites[0]} --target=#{t.name} --generate-source"
end

The contents of the Rakefile is almost same as the Makefile in the previous subsection.

Rakefile might seem to be difficult for beginners. But, you can use any Ruby syntax in Rakefile, so it is really flexible. If you practice Ruby and Rakefile, it will be highly productive tools.

Meson and ninja

Meson is one of the most popular building tool despite the developing version. And ninja is similar to make but much faster than make. Several years ago, most of the C developers used autotools and make. But now the situation has changed. Many developers are using meson and ninja now.

To use meson, you first need to write meson.build file.

project('tfe', 'c')

gtkdep = dependency('gtk4')

gnome=import('gnome')
resources = gnome.compile_resources('resources','tfe.gresource.xml')

sourcefiles=files('tfe.c', 'tfetextview.c')

executable('tfe', sourcefiles, resources, dependencies: gtkdep)

Now run meson and ninja.

$ meson _build
$ ninja -C _build

Then, the executable file tfe is generated under the directory _build.

$ _build/tfe tfe.c tfetextview.c

Then the window appears. And two notebook pages are in the window. One notebook is tfe.c and the other is tfetextview.c.

I’ve shown you three build tools. I think meson and ninja is the best choice for the present.

We divided a file into some categorized files and used a build tool. This method is used by many developers.