We’ve compiled a small editor so far. But Some bad signs are beginning to appear.
gcc
and
glib-compile-resources
. We should control them by one
building tool.These ideas are useful to manage big source files.
When you divide C source file into several parts, each file should
contain 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
.
tfetextview.c
includes the definition and functions of
TfeTextView.tfe.c
includes functions like main
,
app_activate
, app_open
and so on, which relate
to GtkApplication and GtkApplicationWindowNow 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) {
g_print ("You need a filename argument.\n");
}
static void
app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
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_window_present (GTK_WINDOW (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>
Dividing a file makes it easy to maintain source files. But now we face a new problem. The building step increases.
tfe.ui
into
resources.c
.tfe.c
into tfe.o
(object
file).tfetextview.c
into
tfetextview.o
.resources.c
into
resources.o
.tfe
.Build tools manage the steps. I’ll show you three build tools, Meson and Ninja, Make and Rake. Meson and Ninja is recommended as a C build tool, but others are also fine. It’s your choice.
Meson and Ninja is one of the most popular building tool to build C language program. Many developers use Meson and Ninja lately. For example, GTK 4 uses them.
You need to make meson.build
file first.
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)
project
defines things about the
project. The first parameter is the name of the project and the second
is the programming language.dependency
function defines a dependency that is
taken by pkg-config
. We put gtk4
as an
argument.import
function imports a module. In line 5, the
gnome module is imported and assigned to the variable
gnome
. The gnome module provides helper tools to build GTK
programs..compile_resources
is a method of the gnome module
and compiles files to resources under the instruction of xml file. In
line 6, the resource filename is resources
, which means
resources.c
and resources.h
, and xml file is
tfe.gresource.xml
. This method generates C source file by
default.dependencies
. gtkdep
is used in the
compilation.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
A window appears. It includes a notebook with two pages. One is
tfe.c
and the other is tfetextview.c
.
For further information, see The Meson Build system.
Make is a build tool created in 1976. It was a standard build tool for C compiling, but lately it is replaced by Meson and Ninja.
Make analyzes Makefile and executes compilers. All instructions are written in Makefile.
For example,
sample.o: sample.c
gcc -o sample.o sample.c
Malefile above consists of three elements, sample.o
,
sample.c
and gcc -o sample.o sample.c
.
sample.o
is a target.sample.c
is a prerequisite.gcc -o sample.o sample.c
is a recipe. Recipes follow
tab characters, not spaces. (It is very important. Use tab, or make
won’t work as you expected).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 just type make
and everything will be done.
$ 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 with make and move on to the next topic.
You can download “Gnu Make Manual” from GNU website.
Rake is a similar program to make. It is written in Ruby language. If you don’t use Ruby, you don’t need to read this subsection. However, Ruby is really sophisticated and recommendable script language.
rake
.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"]
uifile = "tfe.ui"
rscfile = srcfiles[2]
objfiles = srcfiles.ext(".o")
gresource_xml = "tfe.gresource.xml"
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.ext(".c")
file obj => src do |t|
sh "gcc -c -o #{t.name} `pkg-config --cflags gtk4` #{t.source}"
end
end
file rscfile => uifile do |t|
sh "glib-compile-resources #{gresource_xml} --target=#{t.name} --generate-source"
end
The contents of the Rakefile
is almost same as the
Makefile
in the previous subsection.
rake clean
is typed on the command line.targetfile
. The task
default
is the final goal of tasks.targetfile
depends on objfiles
. The
variable t
is a task object.
t.name
is a target namet.prerequisites
is an array of prerequisites.t.source
is the first element of prerequisites.sh
is a method to give the following string to shell as
an argument and executes the shell.objfiles
. Each
object depends on corresponding source file.Rakefile might seem to be difficult for beginners. But, you can use any Ruby syntax in the Rakefile, so it is really flexible. If you practice Ruby and Rakefile, it will be highly productive tools.
For further information, see Rake tutorial for beginners.