1 管理多个源文件
到目前为止,我们已经编译了一个小型编辑器。但一些不好的迹象已经开始出现。
- 我们只有一个C源文件,并将所有内容都放在其中。我们需要解决这个问题。
- 有两个编译器,gcc和glib-compile-resources。我们应该通过一种构建工具来控制它们。
这些思想对于管理大型源文件很有用。
2 将C源文件分成两部分
当你把C源文件分成几个部分时,每个文件应该包含一个东西。例如,我们的源代码有两件事,TfeTextView的定义和与GtkApplication和GtkApplicationWindow相关的函数。将它们分成两个文件,tfetextview.c和tfe.c是一个好主意。
- tfetextview.c包含了TfeTextView的定义和函数。
- tfe.c包含main、app_activate、app_open等函数,它们与GtkApplication和GtkApplicationWindow相关
现在我们有三个源文件:tfetextview.c、tfe.c和tfe3.ui。tfe3的3就像一个版本号。通过文件名管理版本是一种可能的想法,但它可能会带来麻烦。您需要在每个版本中重写文件名,它会影响引用文件名的源文件的内容。因此,我们应该从文件名中去掉3。
在tfe.c中调用函数tfe_text_view_new来创建一个TfeTextView实例。但是它是在tfetextview.c中定义的,而不是tfe.c。没有tfe_text_view_new的声明(而不是定义)会在编译tfe.c时出错。该声明在tfe.c中是必要的。这些公开信息通常写在头文件中。它有.h后缀,像tfetextview.h,头文件包含在C源文件中。例如,tfetextview.h包含在tfe.c中。
下面列出了所有的源文件。
/* filename: tfetextview.h */
1 #include <gtk/gtk.h>
2
3 #define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
4 G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
5
6 void
7 tfe_text_view_set_file (TfeTextView *tv, GFile *f);
8
9 GFile *
10 tfe_text_view_get_file (TfeTextView *tv);
11
12 GtkWidget *
13 tfe_text_view_new (void);
14
/* filename: tfetextview.c */
1 #include <gtk/gtk.h>
2 #include "tfetextview.h"
3
4 struct _TfeTextView
5 {
6 GtkTextView parent;
7 GFile *file;
8 };
9
10 G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
11
12 static void
13 tfe_text_view_init (TfeTextView *tv) {
14 }
15
16 static void
17 tfe_text_view_class_init (TfeTextViewClass *class) {
18 }
19
20 void
21 tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
22 tv -> file = f;
23 }
24
25 GFile *
26 tfe_text_view_get_file (TfeTextView *tv) {
27 return tv -> file;
28 }
29
30 GtkWidget *
31 tfe_text_view_new (void) {
32 return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
33 }
34
/* filename: tfe.c */
1 #include <gtk/gtk.h>
2 #include "tfetextview.h"
3
4 static void
5 app_activate (GApplication *app) {
6 g_print ("You need a filename argument.\n");
7 }
8
9 static void
10 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
11 GtkWidget *win;
12 GtkWidget *nb;
13 GtkWidget *lab;
14 GtkNotebookPage *nbp;
15 GtkWidget *scr;
16 GtkWidget *tv;
17 GtkTextBuffer *tb;
18 char *contents;
19 gsize length;
20 char *filename;
21 int i;
22 GtkBuilder *build;
23
24 build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe3/tfe.ui");
25 win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
26 gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
27 nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
28 g_object_unref (build);
29 for (i = 0; i < n_files; i++) {
30 if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
31 scr = gtk_scrolled_window_new ();
32 tv = tfe_text_view_new ();
33 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
34 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
35 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
36
37 tfe_text_view_set_file (TFE_TEXT_VIEW (tv), g_file_dup (files[i]));
38 gtk_text_buffer_set_text (tb, contents, length);
39 g_free (contents);
40 filename = g_file_get_basename (files[i]);
41 lab = gtk_label_new (filename);
42 gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
43 nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
44 g_object_set (nbp, "tab-expand", TRUE, NULL);
45 g_free (filename);
46 } else if ((filename = g_file_get_path (files[i])) != NULL) {
47 g_print ("No such file: %s.\n", filename);
48 g_free (filename);
49 } else
50 g_print ("No valid file is given\n");
51 }
52 if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
53 gtk_window_present (GTK_WINDOW (win));
54 } else
55 gtk_window_destroy (GTK_WINDOW (win));
56 }
57
58 int
59 main (int argc, char **argv) {
60 GtkApplication *app;
61 int stat;
62
63 app = gtk_application_new ("com.github.ToshioCP.tfe", G_APPLICATION_HANDLES_OPEN);
64 g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
65 g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
66 stat =g_application_run (G_APPLICATION (app), argc, argv);
67 g_object_unref (app);
68 return stat;
69 }
70
ui文件tfe.ui与tfe3文件夹内相同。在上一节中介绍。
# filename: tfe.gresource.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <gresources>
3 <gresource prefix="/com/github/ToshioCP/tfe3">
4 <file>tfe.ui</file>
5 </gresource>
6 </gresources>
分文件使维护源文件变得容易。但是现在我们面临着一个新的问题。building步骤增加。
- 编译ui文件tfe.ui。将其转换为resources.c。
- 将tfe.c编译为tfe。O(目标文件)。
- 编译tfetextview.c为tfetextview.o。
- 将resources.c编译为resources.o。
- 将所有目标文件链接到应用程序tfe。
构建工具管理这些步骤。我将向你展示三种构建工具,Meson和Ninja,Make和Rake。推荐将Meson和Ninja作为C编译工具,其他的也可以。这是你的选择。
3 Meson and Ninja
Meson和Ninja是构建C语言程序最流行的构建工具之一。最近许多开发人员使用Meson和Ninja。例如,GTK 4使用它们。
首先,你需要去编写 meson.build文件。
1 project('tfe', 'c')
2 # dependency = pkg-config
3 gtkdep = dependency('gtk4')
4
5 gnome=import('gnome')
6 resources = gnome.compile_resources('resources','tfe.gresource.xml')
7
8 sourcefiles=files('tfe.c', 'tfetextview.c')
9
10 executable('tfe', sourcefiles, resources, dependencies: gtkdep)
- 1:函数project定义关于项目的东西。第一个参数是项目名称,第二个参数是编程语言。
- 2:依赖函数定义了pkg-config使用的依赖。我们将gtk4作为参数。
5: import功能导入模块。在第5行中,导入了gnome模块,并将其赋值给变量gnome。gnome模块提供了构建GTK程序的辅助工具。
6: .compile_resources是gnome模块的一个方法,在XML文件的指令下将文件编译为资源。在第6行中,资源文件名是resources,即resources.c和resources.h,而xml文件是tfe.gresource.xml。这个方法默认生成C源文件。 - 8:定义源文件。
- 10:表示可执行函数,通过编译源文件生成目标文件。第一个参数是目标的文件名。以下参数均为源文件。最后一个参数是一个选项依赖项。gtkdep用于编译。
现在运行meson和ninja命令:
$ meson _build
$ ninja -C _build
这时,可执行文件tfe已经在_build目录中生成。
$ _build/tfe tfe.c tfetextview.c
出现一个窗口。它包括一个两页的笔记本。一个是tfe.c,另一个是tfetextview.c。
有关更多信息,请参阅The Meson Build system。
4 Make
Make是创建于1976年的构建工具。它是C编译的标准构建工具,但最近被Meson和Ninja取代。
Make分析Makefile文件,然后执行编译器。所有的指令都写在Makefile中。
例如,
sample.o: sample.c
gcc -o sample.o sample.c
上面的Malefile由三个元素组成:sample.o, sample.c和gcc -o sample.o sample.c。
- sample.o是一个目标。
- sample.c是先决条件。
- gcc -o sample.o sample.c是一个recipe。recipes遵循
制表符
,而不是空格。这很重要。使用tab,否则make不会像你预期的那样工作)。
规则是:
如果先决条件在目标之后修改,make就会执行recipe。
在上面的例子中,如果sample.c在sample.o生成后被修改。然后make执行gcc并将sample.c编译为sample.o。如果sample.c的修改时间较早于sample.o。那么不需要编译,所以make什么都不做。
tfe的Makefile如下所示:
1 all: tfe
2
3 tfe: tfe.o tfetextview.o resources.o
4 gcc -o tfe tfe.o tfetextview.o resources.o `pkg-config --libs gtk4`
5
6 tfe.o: tfe.c tfetextview.h
7 gcc -c -o tfe.o `pkg-config --cflags gtk4` tfe.c
8 tfetextview.o: tfetextview.c tfetextview.h
9 gcc -c -o tfetextview.o `pkg-config --cflags gtk4` tfetextview.c
10 resources.o: resources.c
11 gcc -c -o resources.o `pkg-config --cflags gtk4` resources.c
12
13 resources.c: tfe.gresource.xml tfe.ui
14 glib-compile-resources tfe.gresource.xml --target=resources.c --generate-source
15
16 .Phony: clean
17
18 clean:
19 rm -f tfe tfe.o tfetextview.o resources.o resources.c
你输入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`
我只使用了非常基本的规则来编写这个Makefile。还有许多更方便的方法可以使它更紧凑。但要解释清楚需要很长时间。我想以make结束今天的内容,然后进入下一个话题。
你可以从GNU网站下载“Gnu Make Manual”。
5 Rake
Rake也是一个类似make的程序。它是用Ruby语言编写的。如果你不使用Ruby,则不需要阅读这一小节。然而,Ruby确实是非常复杂和值得推荐的脚本语言。
- Rakefile控制rake的行为。
- 你可以在Rakefile中编写任何Ruby代码。
Rake有任务和文件任务,与make中的目标、前提和recipe类似。
1 require 'rake/clean'
2
3 targetfile = "tfe"
4 srcfiles = FileList["tfe.c", "tfetextview.c", "resources.c"]
5 uifile = "tfe.ui"
6 rscfile = srcfiles[2]
7 objfiles = srcfiles.ext(".o")
8 gresource_xml = "tfe.gresource.xml"
9
10 CLEAN.include(targetfile, objfiles, rscfile)
11
12 task default: targetfile
13
14 file targetfile => objfiles do |t|
15 sh "gcc -o #{t.name} #{t.prerequisites.join(' ')} `pkg-config --libs gtk4`"
16 end
17
18 objfiles.each do |obj|
19 src = obj.ext(".c")
20 file obj => src do |t|
21 sh "gcc -c -o #{t.name} `pkg-config --cflags gtk4` #{t.source}"
22 end
23 end
24
25 file rscfile => uifile do |t|
26 sh "glib-compile-resources #{gresource_xml} --target=#{t.name} --generate-source"
27 end
Rakefile的内容几乎与前一小节中的Makefile相同。
- 3-8:定义目标文件、源文件等。
- 1、10需要rake/clean库。clean文件被添加到CLEAN。当在命令行中输入rake CLEAN时,CLEAN包含的文件将被删除。
- 12:默认目标依赖于targetfile。任务默认值是任务的最终目标。
- 14-16: targetfile依赖于objfiles。变量t是一个任务对象。
- t.name是目标名称
- t.prerequisites是先决条件的数组。
- t.source是先决条件的第一个元素。
- sh是一个方法,它将下面的字符串作为参数传递给shell并执行shell。
- 18-23:数组objfiles的each迭代器。每个对象都依赖于相应的源文件。
- 25-27:资源文件依赖于ui文件。
Rakefile对于初学者来说似乎很难。但是,您可以在Rakefile中使用任何Ruby语法,因此它非常灵活。如果你使用Ruby和Rakefile,它将是一个高效的工具。