Build nhiều file¶
Bài giới thiệu
Bài này tóm tắt cách tổ chức một project C nhiều file và cách để build tool chỉ build lại đúng phần cần thiết. Cú pháp Makefile và CMake được trình bày riêng ở Makefile và CMake.
Ở bài 1 ta thấy mỗi file .c là một translation unit: được preprocess và compile độc lập thành một .o, rồi linker ghép lại. Hai file chỉ "biết nhau" qua khai báo trong header, còn việc nối thực sự diễn ra ở bước link (bài 3).
Header và source: ai làm gì¶
Header (.h) |
Source (.c) |
|
|---|---|---|
| Chứa | khai báo: prototype, extern, typedef, struct, macro |
định nghĩa: thân hàm, biến |
| Ai include | mọi file cần dùng | hiếm khi bị include |
Sinh ra gì trong .o |
thường không gì (trừ static inline) |
code và dữ liệu |
Nguyên tắc: header là giao diện (API) của một module, source là cài đặt. Định nghĩa biến/hàm trong header (không phải static/inline) sẽ bị chép vào mọi file include nó, dẫn tới lỗi multiple definition khi link (bài 3).
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int square(int x);
#endif /* MATH_UTILS_H */
Include guard (#ifndef/#define/#endif) ngăn header bị chép hai lần vào cùng một translation unit (ví dụ a.h include b.h, main.c include cả hai), vì nếu không, struct/typedef sẽ bị định nghĩa lại. #pragma once ngắn hơn và được mọi compiler chính hỗ trợ nhưng không thuộc chuẩn C.
Linkage: ai nhìn thấy ai¶
| Khai báo | Linkage | Symbol trong .o |
|---|---|---|
int x = 1; (file scope) |
external | GLOBAL |
static int x = 1; |
internal | LOCAL |
extern int x; |
external (chỉ khai báo) | UND nếu có dùng |
static bên trong hàm |
không | LOCAL, lưu trữ tĩnh |
Quy ước thực tế: hàm và biến chỉ dùng trong một file thì khai báo static. Việc này vừa tránh xung đột tên giữa các file, vừa cho compiler thêm cơ hội tối ưu (inline, loại bỏ code không dùng).
Bố cục project¶
project/
├── include/ # header public
│ └── math_utils.h
├── src/
│ ├── main.c
│ └── math_utils.c
├── Makefile
└── build/ # sinh ra, không commit
Source include header bằng #include "math_utils.h", kèm -Iinclude ở command line, để không phải viết đường dẫn tương đối trong code.
Build incremental với Makefile¶
Một Makefile tối thiểu, đã thử chạy trên ví dụ trên:
CFLAGS := -O2 -g -Wall -Wextra -Iinclude
BUILD := build
SRCS := $(wildcard src/*.c)
OBJS := $(SRCS:src/%.c=$(BUILD)/%.o)
DEPS := $(OBJS:.o=.d)
app: $(OBJS)
$(CC) $^ -o $@
$(BUILD)/%.o: src/%.c | $(BUILD)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
$(BUILD):
mkdir -p $@
clean:
rm -rf $(BUILD) app
-include $(DEPS)
.PHONY: clean
Điểm chính là cặp -MMD -MP kèm dòng -include $(DEPS).
Vì sao cần -MMD¶
Rule %.o: %.c chỉ nói .o phụ thuộc vào .c, không biết .c include header nào. Nếu sửa math_utils.h mà make không rebuild, ta có một bản build cũ lệch với header mới. Đây là loại lỗi rất khó tìm (ví dụ sai layout struct giữa hai file).
-MMD bảo gcc ghi thêm một file .d cùng lúc compile, liệt kê header đã include (trừ header hệ thống):
-include $(DEPS) nạp các file đó vào Makefile. Dòng thứ hai (include/math_utils.h:) do -MP sinh ra: một rule rỗng cho từng header, để khi xoá hoặc đổi tên header, make không báo lỗi No rule to make target.
Kết quả:
$ make
cc -O2 -g -Wall -Wextra -Iinclude -MMD -MP -c src/main.c -o build/main.o
cc -O2 -g -Wall -Wextra -Iinclude -MMD -MP -c src/math_utils.c -o build/math_utils.o
cc build/main.o build/math_utils.o -o app
$ make
make: 'app' is up to date.
$ touch include/math_utils.h && make # header đổi => file include nó được build lại
Build song song¶
Vì mỗi .o độc lập với nhau, make -j$(nproc) build song song các file .c. Bước link phải đợi tất cả .o, nên là điểm tuần tự duy nhất.
Cross compile chỉ là đổi biến¶
Vì Makefile dùng $(CC), đổi toolchain chỉ cần:
$ make clean && make CC=arm-linux-gnueabihf-gcc
$ file app
app: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), ...
Trong build system lớn (Linux kernel, U-Boot, Buildroot), biến này thường là tiền tố CROSS_COMPILE=arm-linux-gnueabihf-, xem bài 6.
Từ Makefile sang CMake¶
Khi project lớn lên, viết tay danh sách phụ thuộc, quản lý nhiều toolchain, cấu hình -D theo target trở nên khó. CMake mô tả project ở mức cao hơn (target, include directory, link library) rồi sinh ra Makefile hoặc Ninja. Cùng project trên:
cmake_minimum_required(VERSION 3.16)
project(app C)
add_executable(app src/main.c src/math_utils.c)
target_include_directories(app PRIVATE include)
target_compile_options(app PRIVATE -Wall -Wextra)
CMake tự sinh dependency header (tương đương -MMD) và hỗ trợ cross compile qua toolchain file, xem CMake.
Tóm tắt¶
- Mỗi
.clà một translation unit; header chỉ chứa khai báo, định nghĩa ở.c. - Include guard chống include trùng;
staticcho symbol chỉ dùng nội bộ file. - Rule
%.o: %.cthiếu phụ thuộc header: dùng-MMD -MP+-include $(DEPS). - Build incremental và song song (
make -j) là lợi ích chính của việc tách compile và link.
Tiếp theo: Cross compile.