我正在研究Flutters embedder API,因为我可能会在即将推出的项目中通过Rust调用它,并找到它:
FLUTTER_EXPORT
FlutterResult FlutterEngineShutdown(FlutterEngine engine);
FLUTTER_EXPORT
部分(从我可以看到)一个宏定义为:
#ifndef FLUTTER_EXPORT
#define FLUTTER_EXPORT
#endif // FLUTTER_EXPORT
我绝不是C大师,但我已经完成了C编程的公平份额,但从来没有机会使用这样的东西。它是什么?我已经尝试过Google了,但除了"注释"之外,我真的不知道该怎么称呼它。这并不是一个完美的契合。
答案 0 :(得分:1)
同样正如所指出的那样 - 这些宏不能扩展到有意义的东西 - 例如,如果你写这个宏几次,它将不会影响任何东西。
另一个不错的用途是 - here
可以用来测试代码您可以在使用-DFLUTTER_EXPORT=SOMETHING
时传递gcc
。这对于在不触及代码库的情况下运行代码进行测试非常有用。这也可以用于根据编译时传递参数提供不同类型的expnasion - 可以以不同的方式使用。
我的回答的另一个重要部分是使用空宏的可见性,gcc还提供了一种使用__attribute__ ((visibility ("default")))
实现与here描述相同的方法(如IharobAlAsimi / nemequ)提到)(-fvisibility=hidden
)等宏FLUTTER_EXPORT
。该名称还让我们知道可能需要添加属性__declspec(dllimport)
(这意味着它将从dll导入)。关于gcc在那里使用可见性支持的示例将会有所帮助。
它可以用于关联某种类型的调试操作(我的意思是这些空宏也可以像这样使用 - 虽然名称暗示这不是预期用途)
#ifdef FLUTTER_EXPORT
#define ...
#else
#define ...
#endif
这里#define
这里将指定一些打印或记录宏。如果没有定义,那么它将被替换为空白语句。 do{}while(0)
等。
答案 1 :(得分: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)
。即使在Windows上,AFAIK通常也是必需的,但有时会出现这种情况。
这里的解决方案是在我们构建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_API
或FLUTTER_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所提到的)基本上是一个无操作语句:
#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的项目。它是一个单一的标题,您可以放入源代码树,这样可以让很多更容易利用这种类型的功能,而不会让人们看到你的眼睛流血头。