英文:
Single-header C library is causing linking errors. How can I restructure the project to fix this?
问题
我正在使用 small3dlib 与 SDL 来尝试创建一个简单的 3D 游戏。
我将库包含到我的 C 项目中,只需将头文件放入我的源代码目录中,然后创建了两个文件 gfx.c 和 gfx.h,作为整个项目的图形控制。
我的项目结构如下:
tsa-3d/
| bin/
| build/
| src/
|-- gfx.c
|-- main.c
|-- include/
|---- gfx.h
|---- logger.h
|---- loggerconf.h
|---- models.h
|---- small3dlib.h
|---- tgc.h
|-- lib/
|---- logger.c
|---- loggerconf.c
|---- tgc.c
|-- models/
|---- carModel.c
|---- cityModel.c
|---- cityTexture.c
其中,唯一包含 small3dlib 的文件(间接)是 main.c、gfx.c/.h、models.h 以及所有的模型。
该库要求在包含之前定义一些宏,因此每次我必须将我的图形接口头文件命名为 gfx.h,而不是 small3dlib.h。
#ifndef GFX_H
#define GFX_H
#define S3L_FLAT 0
#define S3L_NEAR_CROSS_STRATEGY 3
#define S3L_PERSPECTIVE_CORRECTION 2
#define S3L_SORT 0
#define S3L_STENCIL_BUFFER 0
#define S3L_Z_BUFFER 2
#define S3L_PIXEL_FUNCTION draw_pixel
#define S3L_RESOLUTION_X 640
#define S3L_RESOLUTION_Y 480
#define TEXTURE_W 256
#define TEXTURE_H 256
#define WINDOW_TITLE "hello, world"
#include <SDL2/SDL.h>
#include <stdint.h>
#include "small3dlib.h"
#include "models.h"
...
由于这个布局,我在每个在 small3dlib.h 中定义的函数都有以下链接错误:
/usr/bin/ld: src/main.o:(.bss+0x0): `S3L_zBuffer' 多次定义;src/gfx.o:(.bss+0x0): 首次定义在此处
/usr/bin/ld: src/main.o: 在函数 `S3L_zBufferRead' 中:
main.c:(.text+0x7d): 多次定义;src/gfx.o:gfx.c:(.text+0x7d): 首次定义在此处
/usr/bin/ld: src/main.o: 在函数 `S3L_zBufferWrite' 中:
main.c:(.text+0xb9): 多次定义;src/gfx.o:gfx.c:(.text+0xb9): 首次定义在此处
/usr/bin/ld: src/main.o: 在函数 `S3L_mat4Copy' 中:
...
所有的模型也出现了相同的问题(最初都是从项目中引用的,头文件中包含了变量和函数定义,直到我将它们拆分为单独的 .c 文件和一个共享的头文件)。
我已经尽力查找解决方法,但不确定该如何解决。我知道变量和函数定义不应该放在头文件中,但这个库的整体设计是将所有定义放在一个头文件中。该库的所有示例程序只包括一个 .c 文件,其中包括 small3dlib.h,那么在这种情况下,当被多个文件包含时,该库的使用方式是什么?
到目前为止项目的完整代码可以在 这里 找到。这是从库的存储库中的 city 示例尝试的一种适应。
我尝试将库拆分为一个头文件和一个源文件,以便所有函数仅在头文件中进行声明。但这个选项失败了,因为头文件本身非常庞大,将每个函数定义转换为声明并了解哪些函数需要转换哪些不需要,在 2800 行 C 代码中是繁琐和乏味的。大约尝试了半个小时后,我认为我很可能只是在尝试链接库时做错了什么,而库本身很可能不是这个问题的原因。
编辑:这是这个问题的最小可重现示例:
以下是发生的情况:
文件结构:
project/
| Makefile
| src/
|-- main.c
|-- foo.c
|-- include/
|---- something.h
|---- lib.h
Makefile
# 全局变量
SRC := $(wildcard src/*.c) \
$(wildcard src/**/*.c) \
$(wildcard src/**/**/*.c) \
$(wildcard src/**/**/**/*.c)
OUT := program
# 默认构建
EXE := ./build/${OUT}
CFLAGS := -I./src/include
LIBFLAGS := -lSDL2 -ldl
OBJ := ${SRC:.c=.o}
build: clean-obj dir ${OBJ}
gcc ${OBJ} -o ${EXE} ${CFLAGS} ${LIBFLAGS}
${call clean-obj}
%.o: %.c
gcc -c $< -o $@ ${CFLAGS}
run: build
./bin/${OUT}
# 实用程序
dir:
mkdir -p ./bin ./build/
clean-obj:
rm -f ${OBJ} ./bin/*
clean: clean-obj
rm -f ${EXE}
lib.h -> small3dlib.h 的表示
#ifndef LIB_H
#define LIB_H
#include <stdint.h>
#include <stdio.h>
#ifndef SOME_MACRO
#error SOME_MACRO NOT DEFINED!
#endif
#define ARRAY_LENGTH 3
const uint8_t array[ARRAY_LENGTH] = { 1, 2, 3 };
void hello(uint8_t x) {
for (uint8_t i = 0; i < x; i++)
printf("%d", i);
}
#endif
something.h
#ifndef SOMETHING_H
#define SOMETHING_H
#define SOME_MACRO 3
#include "lib.h"
void bar();
#endif
main.c
#include "include/something.h"
int main(int argc, char* argv[]) {
hello(array[2]);
bar();
}
英文:
I'm currently using small3dlib with SDL to try and make a simple 3D game.
I included the library to my C project by simply putting the headerfile into my source directory, and I made two files, gfx.c and gfx.h, to serve as the graphics control for the entire project.
My current project structure looks like this:
tsa-3d/
| bin/
| build/
| src/
|-- gfx.c
|-- main.c
|-- include/
|---- gfx.h
|---- logger.h
|---- loggerconf.h
|---- models.h
|---- small3dlib.h
|---- tgc.h
|-- lib/
|---- logger.c
|---- loggerconf.c
|---- tgc.c
|-- models/
|---- carModel.c
|---- cityModel.c
|---- cityTexture.c
Of these, the only files that include small3dlib (indirectly) are main.c, gfx.c/.h, models.h, and all of the models.
The library requires that a few macros are defined before being included, so I am required to call my graphics interface header gfx.h rather than small3dlib.h each time.
#ifndef GFX_H
#define GFX_H
#define S3L_FLAT 0
#define S3L_NEAR_CROSS_STRATEGY 3
#define S3L_PERSPECTIVE_CORRECTION 2
#define S3L_SORT 0
#define S3L_STENCIL_BUFFER 0
#define S3L_Z_BUFFER 2
#define S3L_PIXEL_FUNCTION draw_pixel
#define S3L_RESOLUTION_X 640
#define S3L_RESOLUTION_Y 480
#define TEXTURE_W 256
#define TEXTURE_H 256
#define WINDOW_TITLE "hello, world"
#include <SDL2/SDL.h>
#include <stdint.h>
#include "small3dlib.h"
#include "models.h"
...
As a result of this layout, I have the following linking error for every function defined within small3dlib.h:
/usr/bin/ld: src/main.o:(.bss+0x0): multiple definition of `S3L_zBuffer'; src/gfx.o:(.bss+0x0): first defined here
/usr/bin/ld: src/main.o: in function `S3L_zBufferRead':
main.c:(.text+0x7d): multiple definition of `S3L_zBufferRead'; src/gfx.o:gfx.c:(.text+0x7d): first defined here
/usr/bin/ld: src/main.o: in function `S3L_zBufferWrite':
main.c:(.text+0xb9): multiple definition of `S3L_zBufferWrite'; src/gfx.o:gfx.c:(.text+0xb9): first defined here
/usr/bin/ld: src/main.o: in function `S3L_mat4Copy':
...
I had the same problem for all of the models as well (all were sourced from the project and had variable/function definitions in the headers originally until I split them into just .c files and a single shared header.
I've looked around as much as I can and I'm not sure what I need to do to fix this. I'm aware that variable and function definitions are not supposed to go into header files, but this entire library is built around having all definitions in a singular header. All of the example programs for this library have small3dlib.h included by only a singular .c file, so in that case, how is this library intended to be used when included by multiple files?
The full code for the project thus far can be found here. It is an attempted adaptation from the city example on the library's repository
I have tried splitting up the library into a headerfile and a source file so that all of the functions would now simply be prototyped in the header. This option failed because the header itself is incredibly large, and converting each function definition to a declaration and knowing which functions need to be converted and which don't is tiresome and tedious for 2800 lines of C. After attempting it for about half an hour, I assumed that I am most likely just doing something wrong in attempting to link the library, and that the library is most likely not at fault for this issue.
Edit: This is a minimal reproducible example of this:
Here is an example of what is going on here:
file structure:
project/
| Makefile
| src/
|-- main.c
|-- foo.c
|-- include/
|---- something.h
|---- lib.h
Makefile
# Global
SRC := $(wildcard src/*.c) \
$(wildcard src/**/*.c) \
$(wildcard src/**/**/*.c) \
$(wildcard src/**/**/**/*.c)
OUT := program
# Default Build
EXE := ./build/${OUT}
CFLAGS := -I./src/include
LIBFLAGS := -lSDL2 -ldl
OBJ := ${SRC:.c=.o}
build: clean-obj dir ${OBJ}
gcc ${OBJ} -o ${EXE} ${CFLAGS} ${LIBFLAGS}
${call clean-obj}
%.o: %.c
gcc -c $< -o $@ ${CFLAGS}
run: build
./bin/${OUT}
# Utility
dir:
mkdir -p ./bin ./build/
clean-obj:
rm -f ${OBJ} ./bin/*
clean: clean-obj
rm -f ${EXE}
lib.h -> representation of small3dlib.h
#ifndef LIB_H
#define LIB_H
#include <stdint.h>
#include <stdio.h>
#ifndef SOME_MACRO
#error SOME_MACRO NOT DEFINED!
#endif
#define ARRAY_LENGTH 3
const uint8_t array[ARRAY_LENGTH] = { 1, 2, 3 };
void hello(uint8_t x) {
for (uint8_t i = 0; i < x; i++)
printf("%d", i);
}
#endif
something.h
#ifndef SOMETHING_H
#define SOMETHING_H
#define SOME_MACRO 3
#include "lib.h"
void bar();
#endif
main.c
#include "include/something.h"
int main(int argc, char* argv[]) {
hello(array[2]);
bar();
}
foo.c
#include "include/something.h"
void bar() {
hello(8);
}
This results in:
...
/usr/bin/ld: src/main.o:(.rodata+0x0): multiple definition of `array'; src/foo.o:(.rodata+0x0): first defined here
/usr/bin/ld: src/main.o: in function `hello':
main.c:(.text+0x0): multiple definition of `hello'; src/foo.o:foo.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
make: *** [Makefile:18: build] Error 1
答案1
得分: 0
头文件库不容易处理。您不能将它们包含在多个翻译单元(通常称为源代码)中。否则,您将有与包含它的翻译单元一样多的定义,结果会导致构建错误。而且,在它们的控制#define
中可能存在不匹配。
因此,就您的例子而言,您需要编写一个提供单一翻译单元的实现。我将其命名为"lib_impl.c"。为了集中必要的定义,我编写了一个单独的头文件,命名为"lib_defs.h":
#ifndef LIB_DEFS_H
#define LIB_DEFS_H
#define SOME_MACRO 3
#endif
不幸的是,您需要编写一个单独的头文件,其中包含您的应用程序中使用的所有对象的声明。您不需要声明库的所有可能对象。我将其命名为"lib_decl.h":
#ifndef LIB_DECL_H
#define LIB_DECL_H
#include <stdint.h>
extern const uint8_t array[];
void hello(uint8_t x);
#endif
唯一的实现文件如下:
// lib_impl.c
#include "lib_defs.h"
#include "lib_decl.h"
// 实际上,这是一个源文件,而不是一个头文件!
#include "lib.h"
如果您在实现文件中包含它,您可以让编译器检查您是否正确编写了该声明头文件。
您现在可以在需要访问库对象的地方包含声明头文件。
在您的例子中,您似乎使用了某种“全局”包含文件,即"something.h"。显然,它收集了多个模块的声明:
#ifndef SOMETHING_H
#define SOMETHING_H
#include "lib_decl.h"
void bar();
#endif
所有其他文件保持不变,应用程序可以顺利构建。
_注意1:_由于您向编译器提供了包含文件的路径,您不需要在#include
指令中使用"include"。
_注意2:_当删除所有对象文件,通常是中间文件时,您就丧失了“make”中的一个主要功能:只运行构建依赖目标文件所需的命令。
_最后说明:_我理解为什么作者喜欢头文件库。它只是一个文件需要维护。然而,这带来了几个不利之处:
- 用户需要
#define
她想要使用的所有功能。 - 如果库变得更大,它将成为一个在文件级别上没有组织的庞大源代码块。
- 用户被迫只在一个翻译单元中包含它。
实际上,这将一次性的努力从库的作者那里移除,并转移给了_每个_用户,将工作量乘以使用次数。:-( 请您自行判断。
英文:
Header-file-only libraries are ... erm ... not easy to handle. You cannot include them in more than one translation unit (vulgo source). Else you have as many definitions as translation units that included it, and in consequence build errors. Plus, there is a potential mismatch in their controlling #define
s.
Therefore, as for your example, you need to write an implementation that provides the single translation unit. I named it "lib_impl.c". To centralize the necessary definitions, I wrote a separate header file, named "lib_defs.h":
#ifndef LIB_DEFS_H
#define LIB_DEFS_H
#define SOME_MACRO 3
#endif
Unfortunately you need to write a separate header file, which contains all the declarations of the objects you use in your application. You don't need to declare all possible objects of the library. I named it "lib_decl.h":
#ifndef LIB_DECL_H
#define LIB_DECL_H
#include <stdint.h>
extern const uint8_t array[];
void hello(uint8_t x);
#endif
The single implementation file is this:
// lib_impl.c
#include "lib_defs.h"
#include "lib_decl.h"
// Actually, this is a source file, not a header file!
#include "lib.h"
You can let the compiler check that you wrote this declaration header file correctly, if you include it in the implementation file.
You can now include the declaration header file where you need to access the library's objects.
In your example, you seem to use some kind of "global" include file, "something.h". Apparently it collects the declarations of multiple modules:
#ifndef SOMETHING_H
#define SOMETHING_H
#include "lib_decl.h"
void bar();
#endif
All other files are unchanged, and the application builds without problems.
Note 1: Since you provide the path to the include files to the compiler, you don't need to use "include" in the #include
directives.
Note 2: When removing all object files, in general intermediate files, you drop a main feature of "make": Run only the necessary commands to build dependent target files.
Final note: I understand why authors like header-file-only libraries. It is just one file to maintain. However, this comes with several disadvantages:
- A user needs to
#define
all the features that she wants to use. - If the library gets bigger, it becomes a bloated piece of source without any organization on the file level.
- Users are urged to include it only in one translation unit.
This actually removes one-time effort from the library's author and gives it to each user, multiplying the effort by the number of usages. Judge for yourself.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论