C语言 使用宏注释函数



我正在研究 Flutters 嵌入式 API,因为我可能会在即将到来的项目中通过 Rust 调用它并发现:

FLUTTER_EXPORT
FlutterResult FlutterEngineShutdown(FlutterEngine engine);

FLUTTER_EXPORT部分是(据我所知)一个宏,定义为:

#ifndef FLUTTER_EXPORT
#define FLUTTER_EXPORT
#endif // FLUTTER_EXPORT

我绝不是C大师,但我已经完成了相当多的C编程,但从未有机会使用这样的东西。这是什么?我试图谷歌它,但除了"注释"之外,真的不知道该怎么称呼它,这并不真正适合。

Flutter embedder.h

同样如前所述 - 这些宏不能扩展到有意义的东西 - 例如,如果你写这个宏几次,它不会影响任何事情。

另一个不错的用途是 - (在代码测试中很有用)从这里开始

您可以在使用gcc时传递-DFLUTTER_EXPORT=SOMETHING。这对于在不接触代码库的情况下运行代码以进行测试非常有用。此外,这可用于根据编译时间传递的参数提供不同类型的扩展 - 可以以不同的方式使用。

我的答案中还有很大一部分吹嘘使用空宏的可见性,gcc 还提供了一种方法来实现与此处描述的相同的内容,使用__attribute__ ((visibility ("default")))(如 IharobAlAsimi/nemequ 提到)(-fvisibility=hidden)等为宏FLUTTER_EXPORT。该名称还让我们知道可能需要添加属性__declspec(dllimport)(这意味着它将从 dll 导入)。关于 gcc 在那里使用可见性支持的示例将很有帮助。


它可以在关联这样的某种调试操作时很有用(我的意思是这些空宏也可以像这样使用 - 尽管名称表明这不是预期用途)

#ifdef FLUTTER_EXPORT
#define ...
#else
#define ...
#endif

在这里#define这里将指定一些打印或日志记录宏。如果未定义,则它将被替换为空白语句。do{}while(0)

这会有点漫无边际,但除了您最初的问题之外,我还想谈谈评论中出现的一些内容。

在 Windows 上,在某些情况下,需要显式告知编译器某些符号将由 DLL 公开。在Microsoft的编译器(以前称为 MSVC)中,这是通过向函数添加__declspec(dllexport)注释来完成的,因此您最终会得到类似

__declspec(dllexport)
FlutterResult FlutterEngineShutdown(FlutterEngine engine);

唉,declspec 语法是非标准的。虽然GCC确实支持它(IIRC在非Windows平台上被忽略),但其他符合标准的编译器可能不支持,因此它应该只为支持它的编译器发出。Flutter 开发人员采取的路径是一种简单的方法;如果FLUTTER_EXPORT没有在其他地方定义,他们只是将其定义为无,因此在不需要__declspec(dllexport)的编译器上,您发布的原型将成为

FlutterResult FlutterEngineShutdown(FlutterEngine engine);

但是在 MSVC 上,你会得到 declspec。

设置默认值(无)后,您可以开始考虑如何定义特殊情况。有几种方法可以做到这一点,但最流行的解决方案是包含一个特定于平台的标头,该标头将宏定义为该平台的正确值,或者使用构建系统将定义传递给编译器(例如-DFLUTTER_EXPORT="__declspec(dllexport)")。我更愿意尽可能将逻辑保留在代码而不是构建系统中,以便更轻松地在不同的构建系统中重用代码,所以我假设这是其余答案的方法,但如果您选择构建系统路线,您应该能够看到相似之处。

可以想象,存在默认定义(在本例中为空)这一事实使维护更容易;而不必在每个特定于平台的标头中定义每个宏,您只需在需要非默认值的标头中定义它。此外,如果您添加新的宏,则无需立即将其添加到每个标头中。

我认为这几乎是你最初问题的答案的结束。

现在,如果我们不是在构建 Flutter,而是使用链接到 Flutter DLL 的库中的标头,__declspec(dllexport)是不对的。我们不是在代码中导出FlutterEngineShutdown函数,而是从 DLL导入它。因此,如果我们想使用相同的标头(我们这样做,否则我们引入了标头不同步的可能性),我们实际上希望将FLUTTER_EXPORT映射到__declspec(dllimport).AFAIK 即使在 Windows 上通常也不需要这样做,但在某些情况下确实如此。

这里的解决方案是在构建 Flutter 时定义一个宏,但永远不要在公共标头中定义它。同样,我喜欢使用单独的标题,例如

#define FLUTTER_COMPILING
#include "public-header.h"

我还会放一些包含守卫,并进行检查以确保公共 API 不会首先意外包含,但我懒得在这里输入它。

然后你可以用类似的东西来定义FLUTTER_EXPORT

#if defined(FLUTTER_COMPILING)
#define FLUTTER_EXPORT __declspec(dllexport)
#else
#define FLUTTER_EXPORT __declspec(dllimport)
#endif

您可能还想添加第三种情况,其中两者都未定义,用于将 Flutter SDK 构建到可执行文件中的情况,而不是将 Flutter 构建为共享库,然后从可执行文件链接到它。我不确定 Flutter 是否支持这一点,但现在让我们只关注 Flutter SDK 作为一个共享库。

接下来,让我们看一个相关问题:可见性。大多数不是 MSVC 的编译器伪装成 GCC;它们将__GNUC____GNUC_MINOR____GNUC_PATCHLEVEL__(以及其他宏)定义为适当的值,更重要的是,如果它们假装是 GCC ≥ 4.2,它们支持可见性属性。

可见性与 dllexport/dllimport 并不完全相同。相反,它更像是告诉编译器该符号是内部的("隐藏")还是公开可见的("默认")。这有点像static关键字,但是虽然static限制符号对当前编译单元(源文件)的可见性,但隐藏符号可以在其他编译单元中使用,但它们不会由链接器公开。

隐藏不需要公开的符号可能是一个巨大的性能胜利,特别是对于C++(这往往会暴露比人们想象的更多的符号)。显然,拥有较小的符号表会使链接速度更快,但也许更重要的是编译器可以执行许多优化,而这些优化不能或不能用于公共符号。有时,这就像内联一个原本不会的函数一样简单,但是编译器能够假设来自调用方的数据是对齐的,这反过来又允许矢量化而无需不必要的洗牌。另一个可能允许编译器假定指针没有别名,或者可能永远不会调用函数并且可以修剪。基本上,编译器只有在知道它可以看到函数的所有调用时才能进行许多优化,因此,如果您关心运行时效率,则永远不应该公开超过必要的内容。

这也是一个很好的机会,可以注意到FLUTTER_EXPORT不是一个很好的名字。像FLUTTER_APIFLUTTER_PUBLIC这样的东西会更好,所以让我们从现在开始使用它。

默认情况下,符号是公开可见的,但您可以通过将符号传递给编译器来更改-fvisibility=hidden。你是否这样做将决定你是否需要用__attribute__((visibility("default")))注释公共函数或用__attribute__((visibility("hidden")))注释私有函数,但我建议传递参数,这样如果你忘记注释某些东西,当你尝试从不同的模块使用它而不是默默地公开它时,你最终会得到一个错误。

评论中还提到了另外两件事:调试宏和函数注释。由于使用FLUTTER_EXPORT的位置,我们知道它不是一个调试宏,但无论如何我们都可以讨论它们。这个想法是,您可以根据是否定义了宏,在编译的软件中插入额外的代码。这个想法是这样的:

#if defined(DISABLE_LOGGING)
#  define my_log_func(msg)
#else
#  define my_log_func(msg) my_log_func_ex(expr)
#endif

这实际上是一个非常糟糕的主意;考虑这样的代码:

if (foo)
my_log_func("Foo is true!");
bar();

使用上述定义,如果使用定义DISABLE_LOGGING进行编译,则最终将得到

if (foo)
bar();

这可能不是你想要的(除非你正在参加一个混淆的C竞赛,或者试图插入一个后门)。

相反,你通常想要的(正如coderredoc所提到的)基本上是一个no-op语句:

#if defined(DISABLE_LOGGING)
#  define my_log_func(msg) do{}while(0)
#else
#  define my_log_func(msg) my_log_func_ex(expr)
#endif

在某些奇怪的情况下,您可能会遇到编译器错误,但是编译时错误比难以找到的错误(例如第一个版本)要好得多。

静态分析的注释是评论中提到的另一种情况,这是我的忠实粉丝。例如,假设我们的公共 API 中有一个函数,它采用 printf 样式的格式字符串。我们可以添加一个注释:

__attribute__((format(2,3)))
void print_warning(Context* ctx, const char* fmt, ...);

现在,如果您尝试将 int 传递给 %f,或者忘记参数,编译器可以在编译时发出诊断,就像printf本身一样。我不打算逐一介绍,但利用它们是让编译器在错误进入生产代码之前捕获错误的好方法。

现在进行一些自我推销。几乎所有这些东西都是高度依赖平台的;某项功能是否可用以及如何正确使用它取决于编译器、编译器版本、操作系统等。如果你想让你的代码保持可移植性,你最终会得到很多预处理器的麻烦来让它一切正常。为了解决这个问题,我前段时间整理了一个名为Hedley的项目。它是一个单一的标题,你可以放入你的源代码树中,这应该可以更容易地利用这种类型的功能,而不会让人们在看到你的标题时流血。

最新更新