stb 作者教你如何编写单文件 (Header-only) 的 C/C++ 库

说明

所谓单文件(Header-only)库,就是整个库只有一个头文件,用起来就像引入标准库一样简单。最近需要写这种形式的工具库,于是参考了 stb 库 作者提供的指南,感觉真不绰。下面是我对这篇文档的翻译版本,由于个人水平所限,难免有所纰漏,因此给出了英文原文作为对照。

英文版文档包含于 stb 库的 docs 文件夹,也可以 在这里查看

正文

关于如何创建单文件库,我所学到的

—— Sean Barrett 于 2013 年 9 月

当创建形式类似于 stb 库的单文件库时,你应该做的,以及为什么要这样做:

Lessons learned about how to make a header-file library

September 2013 Sean Barrett

Things to do in an stb-style header-file library, and rationales:

1 - #define 库名英文大写字母_IMPLEMENTATION

使用类似上述形式的符号,以(允许用户)控制何时在其代码中引入你库中的实现(在我最初的一些单文件库中,所用的宏名并没有上面给出的这么清晰;随着我创建多个这样的库,我开始明白这样做是错误的)。

在你的库中用保护性代码来包围所有库函数的 声明 段落,而对于这些函数的 定义 则用上述宏保护起来,而不是用 头文件名_H。这样一来,如果用户在自己的头文件 X 中包含了你的库,他仍可以正常地将 X 包含在他们的源文件中;当你用上述宏保护了库中的实现部分时,在文件 X 中,库函数的声明部分就能够被正常联入,而实现部分被忽略。

译者注:这段的大意是,库中的代码应该分两部分,分别用两种宏来包围。第一部分是所有库函数的声明,就像写普通的头文件一样:

#ifndef 头文件名_H
#define 头文件名_H
/* 所有要暴露出来的库函数声明 */
#endif // 头文件名_H

第二部分则要用条件包围,也就是仅判断该宏是否被定义,而不真正定义这个宏(这个宏是要留给用户去定义的):

#ifdef 库名_IMPLEMENTATION
/* 所有实现代码 */
#endif // 库名_IMPLEMENTATION

这个宏的存在是实现单文件效果的关键。当该宏没被定义时,库就和一个普通头文件没有任何区别;当用户在某 1 个源文件中定义了该宏时,库中的实现部分才被联入那个源文件中,从而避免了函数的重复定义。

1 - #define LIBRARYNAME_IMPLEMENTATION

Use a symbol like the above to control creating the implementation. (I used a far-less-clear name in my first header-file library; it became clear that was a mistake once I had multiple libraries.)

Include a “header-file” section with header-file guards and declarations for all the functions, but only guard the implementation with LIBRARYNAME_IMPLEMENTATION, not the header-file guard. That way, if client’s header file X includes your header file for declarations, they can still include header file X in the source file that creates the implementation; if you guard the implementation too, then the first include (before the #define) creates the declarations, and the second one (after the #define) does nothing.

2 - 避免依赖

不要依赖除标准库之外的任何库。

如果你创建的库就是用于包裹或引入其他库的,那么你当然可以依赖那个库;但如果你要创建的库是开放许可证的,那么最好是把要依赖的库也直接嵌入到你的库中,以减少用户需要依赖的库数目。这样做时,一旦被依赖的库有了更新,你就应该同步更新。

如果你要依赖标准库,就应该考虑把代码中所有对标准库的调用定义成宏,再将所依赖的标准库用宏条件包围起来,这样可以允许用户用自己写的函数来替换对标准库的调用。

如果你库中某些函数具有附加效应,例如分配了内存,就应当考虑允许用户传入一个自行定义的上下文,并在“不使用标准库”版本的宏中间引入该上下文;否则,用户就可能被迫使用全局变量(或线程内的局部变量)来实现这一点。

译者注:这段的大意是:

  1. 尽量避免依赖非标准库,如果是开源项目且许可证兼容,最好在代码中直接嵌入要依赖的第三方库,不过这样做够麻烦的;
  2. 有些用户(如嵌入式开发者)可能不想用标准库,你的库最好能允许用户通过定义宏来拒绝这些依赖,比如用 #ifndef 不_要_用_STDLIB_的_宏 来包围 #include <stdlib.h>,再由用户根据需要自行定义这些宏;这样一来,标准库的调用就需要再定义一个宏来包裹一下,比如:
    #ifndef NO_STDLIB
    #include <stdlib.h>
    #endif // NO_STDLIB
    ...
    #ifndef NO_STDLIB
    #define ALLOCATE malloc
    #endif // NO_STDLIB
    
    这样一来,当用户定义了 NO_STDLIB 时,就可以向 ALLOCATE 这个宏注入他自己实现的 malloc,从而实现了可定制性;

2 - AVOID DEPENDENCIES

Don’t rely on anything other than the C standard libraries.

(If you’re creating a library specifically to leverage/wrap some other library, then obviously you can rely on that library. But if that library is public domain, you might be better off directly embedding the source, to reduce dependencies for your clients. But of course now you have to update whenever that library updates.)

If you use stdlib, consider wrapping all stdlib calls in macros, and then conditionally define those macros to the stdlib function, allowing the user to replace them.

For functions with side effects, like memory allocations, consider letting the user pass in a context and pass that in to the macros. (The stdlib versions will ignore the parameter.) Otherwise, users may have to use global or thread-local variables to achieve the same effect.

3 - 避免 malloc

有时候你是迫不得已,但如果你有得选,那就不要用,嵌入式开发者会很感激的。然而我自己基本上不会规避 malloc,因为麻烦死了(而且有时这样做会降低实用性,详见这里)—— 但我的懒惰显然是阻止某些潜在的用户使用 stb 库的因素之一。

译者注:我感觉这段基本上是无效信息 _(:з」∠)_

3 - AVOID MALLOC

You can’t always do this, but when you can, embedded developers will appreciate it. I almost never bother avoiding, as it’s too much work (and in some cases is pretty infeasible; see http://nothings.org/gamedev/font_rendering_malloc.txt ). But it’s definitely something one of the things I’ve gotten the most pushback on from potential users.

4 - 允许静态实现

#define 以使函数声明和定义变为静态。这样做可以让实现代码(在逻辑上)对用户的源文件不可见,从而允许用户在其工程中多次使用你的库而不发生冲突(只有你的库中包含设置性的宏或全局状态时,或当你的库有多个不能向下兼容的版本时,才有必要这样做。这两种情况我自己都遇到过)。

译者注:我们知道,对于通常的库而言,用 static 修饰的函数是对其他文件不可见的。但由于单文件库的特殊性,其实现部分联入用户的源文件时,在该源文件内,static 原本的作用就被破坏了。因此,当库中需要使用函数(或全局状态),但不想暴露这些函数给用户时,就不能用简单的 static 做弱封装,而是要将函数定义成宏来使用,并在该函数的所有调用结束后 undef 这个宏。读 stb 的源码可以发现作者经常使用这个技巧。

—— 纯属个人理解,如有错误,欢迎指正。

4 - ALLOW STATIC IMPLEMENTATION

Have a #define which makes function declarations and function definitions static. This makes the implementation private to the source file that creates it. This allows people to use your library multiple times in their project without collision. (This is only necessary if your library has configuration macros or global state, or if your library has multiple versions that are not backwards compatible. I’ve run into both of those cases.)

5 - 保持对 C 的兼容性

兼容 C 而不是 C++(例如,直接以 C 编写库,或使用 extern "C"),可以让用户在 C 和其他兼容 C 注入的语言中使用你的库(我在谷歌 stb_image 时得到的最早的结果之一,就是为其写的一个 Haskell wrapper);否则,别人要包裹你的库,就得写一大堆函数调用。我们写库的初衷就是为了方便他人,对不?

(以下省略作者对自己编程偏好的陈述)

5 - MAKE ACCESSIBLE FROM C

Making your code accessible from C instead of C++ (i.e. either coding in C, or using extern “C”) makes it more straightforward to be used in C and in other languages, which often only have support for C bindings, not C++. (One of the earliest results I found in googling for stb_image was a Haskell wrapper.) Otherwise, people have to wrap it in another set of function calls, and the whole point here is to make it convenient for people to use, isn’t it? (See below.)

I prefer to code entirely in C, so the source file that instantiates the implementation can be C itself, for those crazy people out there who are programming in C. But it’s probably not a big hardship for a C programmer to create a single C++ source file to instantiate your library.

6 - 将函数限定在命名空间中

尽力避免在你的代码中使用可能与用户代码冲突的名称。为此,你可以使用 C++ 命名空间,而在 C 中你可以为所有函数添加库名作为前缀。

一般而言,我使用统一的前缀来命名 API 函数和内部使用的符号,如“stbtt_”意为“stb_truetype”,以此来最小化与用户的屎山里的某个破名冲突的概率(译者放飞自我ing)。尤其恶心的情况是,可能用户在使用你库上一个版本时,自己“成功”使用了“stbtt_foo”这个名字,然而升级到你的下一个版本时,发现你的代码里居然有也个“stbtt_foo”或“stbtt__foo”。

需要注意:双下划线是编译器的保留符号,但是 ① 没有什么为“中间件”保留的符号,库想要避免与用户发生命名冲突,却又别无他法;② 实际上并没有哪个编译器会用到单词中间出现的双下划线,用的都是在符号首尾出现的,所以我们凭啥不用呢?(不幸的是,至少有 1 个游戏主机专用的傻 * 编译器会默认对双下划线报警)

译者注:这段的大意是,在库中尽量使用不容易引起命名冲突的名称,可以使用双下划线来提高独特性,双下划线尽量只出现在名称中间。

6 - NAMESPACE PRIVATE FUNCTIONS

Try to avoid having names in your source code that will cause conflicts with identical names in client code. You can do this either by namespacing in C++, or prefixing with your library name in C.

In C, generally, I use the same prefix for API functions and private symbols, such as “stbtt_” for stb_truetype; but private functions (and static globals) use a second underscore as in “stbtt__” to further minimize the chance of additional collisions in the unlikely but not impossible event that users write wrapper functions that have names of the form “stbtt_”. (Consider the user that has used “stbtt_foo” successfully, and then upgrades to a new version of your library which has a new private function named either “stbtt_foo” or “stbtt__foo”.)

Note that the double-underscore is reserved for use by the compiler, but (1) there is nothing reserved for “middleware”, i.e. libraries desiring to avoid conflicts with user symbols have no other good options, and (2) in practice no compilers use double-underscore in the middle rather than the beginning/end. (Unfortunately, there is at least one videogame-console compiler that will warn about double-underscores by default.)

7 - 挑一个容易用的许可

(略)

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值