QQ登录

只需一步,快速开始

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 1621|回复: 1

控制符号的可见性

[复制链接]
发表于 2006-3-17 18:31:52 | 显示全部楼层 |阅读模式
http://developer.apple.com.cn/Documentation/DeveloperTools/Conceptual/CppRuntimeEnv/Articles/SymbolVisibility.html

控制符号的可见性
在普通的C语言中,如果您希望将函数或者变量限制在当前文件中,需要对其使用static关键字。然而,在一个包含很多文件的共享库中,如果您希望某个符号可以被共享库内部的几个文件访问,而又不提供给外部,则对符号进行隐藏处理就会比较困难。大多数的连接器都提供一些便利的方法来隐藏或者显示模块中所有的符号,但是如果您希望更加具有选择性,则需要更多的处理。

在Mac OS X v10.4之前,有两种机制可以控制符号的可见性。第一种技术是通过__private_extern__关键字,将符号声明为共享库私有,而又可以从当前文件输出。这个关键字可以使用到static或extern关键字可以使用的任何地方;第二种技术是使用输出列表。

输出列表是一个文件,含有您希望隐藏或者显示的符号名称。C语言的符号名称比较容易确定(在名称前面加一个下划线字符就可以),C++的符号名称的确定则要复杂得多。由于有类和名字空间,编译器必须包含更多的信息才能唯一标识每一个符号,编译器也因此为每个符号创建一个所谓的重整名称(mangled name)。这种重整名称的生成规则通常和编译器有关,难于进行逻辑的推断,也很难在共享库定义的大列表中找到。

幸运的是,GCC 4.0提供了一些新的、用于改变符号可见性的方法。下面部分将对这些新方法进行描述,并讨论为什么这些技术对您来说相当重要。

本部分包括如下内容:
使用GCC 4.0来标识符号的可见性
限制符号可见性的原因
嵌入函数的可见性
符号的可见性和Objective-C



使用GCC 4.0来标识符号的可见性
从Mac OS X v10.4开始,要隐藏C++符号名称就简单很多了。GCC 4.0编译器支持一些用于隐藏和显示符号名称的新选项,同时还支持一个新的pragma和一些编译器属性,用于改变代码符号的可见性。

请注意:下面的特性只在GCC 4.0和更新的版本提供。更多如何在Xcode中使用这些特性的信息,请参见Xcode 2.1用户指南。


编译器选项
GCC 4.0支持一个新的选项,用于设置源文件中符号的缺省可见性。这个选项是-fvisibility=vis ,您可以用它来设置当前编译的符号的可见性。这个选项的值可以是default(缺省)或者hidden(隐藏),设置为default时,没有显式标识为hidden的符号都处理为可见;设置为hidden时,没有显式标识为可见的符号都处理为隐藏。如果您在编译中没有指定-fvisibility选项,编译器会自行处理为缺省的可见性。

请注意:default设置不是指编译器缺省的处理方式。和hidden设置一样,default来自ELF格式定义的可见性名称。具有default可见性的符号和所有不使用特殊机制的符号具有相同的可见性类型&8212;也就是说,它将作为公共接口的一部分输出。


编译器还支持-fvisibility-inlines-hidden选项,用于强制隐藏所有的嵌入函数。当您希望对大多数项目使用缺省的可见性,但又希望隐藏所有的嵌入函数时,您可能会用到这个选项。有关为何有必要对嵌入函数采取这种处理的更多信息,请参见“嵌入函数的可见性”部分。

可见性属性
如果您正在用GCC 4.0编译代码,可以用可见性属性将个别的符号标识为default或hidden:

__attribute__((visibility("default"))) void MyFunction1() {}


__attribute__((visibility("hidden"))) void MyFunction2() {}





可见性属性会覆盖编译时通过-fvisibility选项指定的值。因此,增加default可见性属性会使符号在所有情况下都被输出,反过来,增加hidden可见性属性会隐藏相应的符号。

可见性属性可以应用到函数,变量,模板,以及C++类。如果一个类被标识为hidden,则该类的所有成员函数,静态成员变量,以及编译器生成的元数据,比如虚函数表和RTTI信息也都会被隐藏。

请注意:虽然模板声明可以用可见性属性来标识,但是模板实例则不能。这是个已知的限制,在GCC的未来版本中可能被修复。


为了演示这些属性如何在编译时进行工作,请看一下下面的声明:

int a(int n) {return n;}





__attribute__((visibility("hidden"))) int b(int n) {return n;}





__attribute__((visibility("default"))) int c(int n) {return n;}





class X


{


    public:


        virtual ~X();


};





class __attribute__((visibility("hidden"))) Y


{


    public:


        virtual ~Y();


};





class __attribute__((visibility("default"))) Z


{


    public:


        virtual ~Z();


};





X::~X() { }


Y::~Y() { }


Z::~Z() { }





用-fvisibility=default选项编译这段代码会使函数a和c ,还有类X和Z的符号输出到库的外部。而如果用-fvisibility=hidden选项进行编译,则会输出函数c和类Z的符号。

从实践上看,用可见性属性标识符号的可见或者隐藏比起用__private_extern__关键字隐藏个别的符号要好。__private_extern__关键字采用的方法是缺省暴露所有的符号,然后选择性地隐藏所有的符号。在大型的共享库中,相反的方法通常更好一些。因此,隐藏所有的符号,然后有选择地暴露希望客户使用的符号通常会好一些。

为了简化将符号标识为输出的任务,您可能希望将default的可见性属性集合定义为宏,如下面的例子所示:

#define EXPORT __attribute__((visibility("default")))





// Always export the following function.


EXPORT int MyFunction1();





使用宏的优点是当您的代码也需要在其它平台编译的时候,可以将宏改为适用于其它平台的关键字。

Pragmas
将符号标识为default或者hidden的另外一种方法是使用GCC 4.0新引入的pragma指令。GCC可见性pragma的优点是可以快速地标识一整块函数,而不需要将可见性属性应用到每个函数中。这个pragma的用法如下:

void f() { }





#pragma GCC visibility push(default)


void g() { }


void h() { }


#pragma GCC visibility pop





在这个例子中,函数g和h被标识为default,因此无论-fvisibility选项如何设置,都会输出;而函数f则遵循-fvisibility选项设置的任何值。push和pop两个关键字标识这个pragma可以被嵌套。

限制符号可见性的原因
从动态共享库中尽可能少地输出符号是一个好的实践经验。输出一个受限制的符号会提高程序的模块性,并隐藏实现的细节。在库中减少符号的数目还可以减少库的内存印迹,减少动态连接器的工作量。动态连接器装载和识别的符号越少,程序启动和运行的速度就越快。

嵌入函数的可见性
您可能会认为嵌入函数的可见性不是个问题,但它的确是个问题。嵌入函数通常在调用现场被展开,因此在目标文件中完全不表示为符号。然而在很多情况下,由于一些很好的理由,编译器可能会表示出嵌入函数的函数体,并因此为其生成符号。最为常见的情况是如果所有的优化都被禁止了,编译器可能决定不进行嵌入优化;较为少见的情况是函数太大,不能进行嵌入,或者函数的地址在其它地方被使用,因此需要有一个符号。

虽然在C++中,您可以将可见性属性(参见“可见性属性”部分)应用到嵌入函数中,就如同其它符号,但是隐藏所有的嵌入函数通常要更好一些。从动态共享库中输出嵌入函数会带来一些复杂的问题。因为有几个因素跟编译器的选择(是将函数表示出来,还是进行嵌入处理)有关系,和共享库的不同连编版本一起连编客户程序的时候,可能会碰到错误。

C和C++的嵌入函数语法有一些细微的区别,记住这一点也是很重要的。在C程序中,只有一个源代码文件可以为一个嵌入函数提供out-of-line(译者注:与inline相对应,指不进行嵌入展开)的定义。这意味着C程序员对out-of-line的拷贝驻留的位置有精确的控制。因此对于基于C的动态共享库来说,只输出嵌入函数的一个拷贝是可能的。对于C++,嵌入函数的定义必须包含在每个使用该函数的翻译单元中。因此如果编译器表示了一个out-of-line的拷贝,则可能会有几个拷贝驻留在不同的翻译单元中。

最后,如果您希望隐藏所有的嵌入函数(但不希望隐藏所有的其它代码),可以用在代码编译的时候使用-fvisibility-inlines-hidden选项。如果您已经向编译器传递了-fvisibility=hidden选项,则没有必要使用-fvisibility-inlines-hidden选项。

符号可见性和Objective-C
Objective-C是C的限制超集, Objective-C++则是C++的限制超集。这意味着这里关于C和C++符号可见性的所有讨论都同样可以应用到Objective-C和Objective-C++上。您可以用编译器选项,可见性属性,以及可见性pragma来隐藏Objective-C代码文件中的C和C++代码。然而,这些可见性控制只能应用到代码中的C或者C++子集,不能应用到Objective-C的类和方法上。

Objective-C类和消息名称由Objective-C运行环境来限制,而不是通过连接器,因此可见性的说明对它们是不起作用的。我们无法在共享库的客户程序中隐藏共享库定义的Objective-C类。
 楼主| 发表于 2006-3-17 18:33:23 | 显示全部楼层
http://gcc.gnu.org/wiki/Visibility

Visibility
Note: the text on this page was almost integrally written by Niall Douglas, the original author of the patch, and placed in his website. This is basically a local mirror (especially useful because the website now appears to be down).

Why is the new C++ visibility support so useful?
Put simply, it hides most of the ELF symbols which would have previously (and unnecessarily) been public. This means:

It very substantially improves load times of your DSO (Dynamic Shared Object).
For example, a huge C++ template-based library which was tested (the TnFOX Boost.Python bindings library) now loads in eight seconds rather than over six minutes!

It lets the optimiser produce better code.
PLT indirections (when a function call or variable access must be looked up via the Global Offset Table such as in PIC code) can be completely avoided, thus substantially avoiding pipeline stalls on modern processors and thus much faster code. Furthermore when most of the symbols are bound locally, they can be safely elided (removed) completely through the entire DSO. This gives greater latitude especially to the inliner which no longer needs to keep an entry point around "just in case".

It reduces the size of your DSO by 5-20%.
ELF's exported symbol table format is quite a space hog, giving the complete mangled symbol name which with heavy template usage can average around 1000 bytes. C++ templates spew out a huge amount of symbols and a typical C++ library can easily surpass 30,000 symbols which is around 5-6Mb! Therefore if you cut out the 60-80% of unnecessary symbols, your DSO can be megabytes smaller!

Much lower chance of symbol collision.
The old woe of two libraries internally using the same symbol for different things is finally behind us with this patch. Hallelujah!

Although the library quoted above is an extreme case, the new visibility support reduced the exported symbol table from > 200,000 symbols to less than 18,000. Some 21Mb was knocked off the binary size as well!

Some people may suggest that GNU linker version scripts can do just as well. Perhaps for C programs this is true, but for C++ it cannot be true - unless you labouriously specify each and every symbol to make public (and the complex mangled name of it), you must use wildcards which tend to let a lot of spurious symbols through. And you have to update the linker script if you decide to change names to the classes or the functions. In the case of the library above, the author couldn't get the symbol table below 40,000 symbols using version scripts. Furthermore, using linker version scripts doesn't permit GCC to better optimise the code.

Windows compatibility
For anyone who has worked on any sizeable portable application on both Windows and POSIX, you'll know the sense of frustration that non-Windows builds of GCC don't offer an equivalent to __declspec(dllexport), i.e. the ability to mark your C/C++ interface as being that of the shared library. Frustration because good DSO interface design is just as important for healthy coding as good class design, or correctly opaquing internal data structures.

While the semantics can't be the same with Windows DLL's and ELF DSO's, almost all Windows-based code uses a macro to compile-time select whether dllimport or dllexport is being used. This mechanism can be easily reused with this patch so adding support to anything already able to be compiled as a Windows DLL is literally a five minute operation.

Note: The semantics are not the same between Windows and this GCC feature - for example, __declspec(dllexport) void (*foo)(void) and void (__declspec(dllexport) *foo)(void) mean quite different things whereas this generates a warning about not being able to apply attributes to non-types on GCC.

Still not convinced?
A further reading on the subject of good DSO design is this article by Ulrich Drepper (lead maintainer of GNU glibc).

How to use the new C++ visibility support
In your header files, wherever you want an interface or API made public outside the current DSO, place __attribute__ ((visibility("default"))) in struct, class and function declarations you wish to make public (it's easier if you define a macro as this). You don't need to specify it in the definition. Then, alter your make system to pass -fvisibility=hidden to each call of GCC compiling a source file. If you are throwing exceptions across shared object boundaries see the section "Caveats" below. Use nm -C -D on the outputted DSO to compare before and after to see the difference it makes.

Some examples of the syntax:

#ifdef _MSC_VER
  #ifdef BUILDING_DLL
    #define DLLEXPORT __declspec(dllexport)
  #else
    #define DLLEXPORT __declspec(dllimport)
  #endif
  #define DLLLOCAL
#else
  #ifdef HAVE_GCCVISIBILITYPATCH
    #define DLLEXPORT __attribute__ ((visibility("default")))
    #define DLLLOCAL __attribute__ ((visibility("hidden")))
  #else
    #define DLLEXPORT
    #define DLLLOCAL
  #endif
#endif

extern "C" DLLEXPORT void function(int a);
class DLLEXPORT SomeClass
{
   int c;
   DLLLOCAL void privateMethod();  // Only for use within this DSO
public:
   Person(int _c) : c(_c) { }
   static void foo(int a);
};
This also helps producing more optimised code: when you declare something defined outside the current compiland, GCC cannot know if that symbol resides inside or outside the DSO in which the current compiland will eventually end up; so, GCC must assume the worst and route everything through the GOT (Global Offset Table) which carries overhead both in code space and extra (costly) relocations for the dynamic linker to perform. To tell GCC a class, struct, function or variable is defined within the current DSO you must specify hidden visibility manually within its header file declaration (using the example above, you declare such things with DLLLOCAL). This causes GCC to generate optimal code.

But this is of course cumbersome: this is why -fvisibility was added. With -fvisibility=hidden, you are telling GCC that every declaration not explicitally marked with a visbility attribute has a hidden visibility. And like in the example above, even for classes marked as visibile (exported from the DSO), you may still want to mark e.g. private members as hidden, so that optimal code will be produced when calling them (from within the DSO).

To aid you converting old code to use the new system, GCC now supports also a #pragma GCC visibility command:

extern void foo(int);
#pragma GCC visibility push(hidden)
extern void someprivatefunct(int);
#pragma GCC visibility pop
You should really only use this for legacy code. All new code should specify each declaration as exported or local individually.

Lastly, there's one other new command line switch: -fvisibility-inlines-hidden. This causes all inlined class member functions to have hidden visibility, causing significant export symbol table size & binary size reductions though not as much as using -fvisibilty=hidden. However, -fvisibility-inlines-hidden can be used with no source alterations - simply apply and win!

Problems with C++ exceptions (please read!)
Exception catching of a user defined type in a binary other than the one which threw the exception requires a typeinfo lookup. Go back and read that last statement again. When exceptions start mysteriously malfunctioning, the cause is exactly this one!

The obvious first step is to mark all types throwable across shared object boundaries always as default visibility. We suggest a parameterised macro eg; EXCEPTIONAPI(spec) which becomes (in the example above) DLLEXPORT on Win32 but always __attribute__ ((visibility("default"))) on GCC. You must do this because even if (e.g.) the exception type's implementation code lives in DLL A, when DLL B throws an instance of that type, the catch handler in DLL C will look for the typeinfo in DLL B.

However, this isn't the full story - it gets harder. The GNU linker treats typeinfo symbols a bit specially - it keeps a table of them which it marks off against each object file it processes when forming the output binary. Symbol visibility is "default" by default but if it encounters just one definition with it hidden - just one - that typeinfo symbol becomes permanently hidden (remember the C++ standard's ODR - one definition rule). Remember that typeinfo symbols are defined on demand within each object file compiled at the point of first use and are defined weakly so the definitions get merged at link time into one copy.

The upshot of this is that if you forget your preprocessor defines in just one object file, or if at any time a throwable type is not declared explicitly public, the -fvisibility=hidden will cause it to be marked hidden in that object file which causes the typeinfo to vanish in the outputted binary (which then causes any throws of that type to cause terminate() to be called in the catching binary). This behaviour only applies to the typeinfo - the vtable and everything else appears fine, so your binaries will link perfectly and appear to work correctly, even though they don't.

While it would be lovely to have a warning for this, there are plenty of legitimate reasons to keep throwable types out of public view. And until whole program optimisation or the export keyword is added to GCC, the compiler can't know which throws are caught locally.

Step-by-step guide
The following instructions are how to add full support to your library, yielding the highest quality code with the greatest reductions in binary size, load times and link times. All new code should have this support from the beginning! And it's worth your while especially in speed critical libraries to spend the few days required to implement it fully - it's a once off investment of time with nothing but good resulting forever more. You can however add basic support to your library in far less time though it is not recommended that you do so.

Place something along the lines of the following code in your master header file (or a specific header that you will include everywhere). This code is taken from the aforementioned TnFOX library:
// Shared library support
#ifdef WIN32
  #define FXIMPORT __declspec(dllimport)
  #define FXEXPORT __declspec(dllexport)
  #define FXDLLLOCAL
  #define FXDLLPUBLIC
#else
  #define FXIMPORT
  #ifdef GCC_HASCLASSVISIBILITY
    #define FXEXPORT __attribute__ ((visibility("default")))
    #define FXDLLLOCAL __attribute__ ((visibility("hidden")))
    #define FXDLLPUBLIC __attribute__ ((visibility("default")))
  #else
    #define FXEXPORT
    #define FXDLLLOCAL
    #define FXDLLPUBLIC
  #endif
#endif

// Define FXAPI for DLL builds
#ifdef FOXDLL
  #ifdef FOXDLL_EXPORTS
    #define FXAPI FXEXPORT
  #else
    #define FXAPI  FXIMPORT
  #endif // FOXDLL_EXPORTS
#else
  #define FXAPI
#endif // FOXDLL

// Throwable classes must always be visible on GCC in all binaries
#ifdef WIN32
  #define FXEXCEPTIONAPI(api) api
#elif defined(GCC_HASCLASSVISIBILITY)
  #define FXEXCEPTIONAPI(api) FXEXPORT
#else
  #define FXEXCEPTIONAPI(api)
#endif
Obviously, you may wish to replace the =FX? with a prefix suiting your library and for projects also supporting Win32, you'll find a lot of the above familiar (you can reuse most of your Win32 macro machinery to also support GCC). To explain:

If WIN32 is defined (as is usual when building for Windows):

If FOXDLL_EXPORTS is defined, we are building our library and symbols should be exported. Something ending with _EXPORTS is defined by MSVC by default in all projects.
If FOXDLL_EXPORTS is not defined, we are importing our library and symbols should be imported.
If WIN32 is not defined (as is usual when building for Unix with GCC):

If GCC_HASCLASSVISIBILITY is defined, then GCC supports the new features. You should define this in your make system if GCC's version is 4.0 or later. Or you may make it configurable.
For every non-templated non-static function definition in your library (both headers and source files), decide if it is publicly used or internally used:

If it is publicly used, mark with FXAPI like this: extern FXAPI PublicFunc()
If it is only internally used, mark with FXDLLLOCAL like this: extern FXDLLLOCAL PublicFunc()
Remember, static functions need no demarcation, nor does anything which is templated.

For every non-templated class definition in your library (both headers and source files), decide if it is publicly used or internally used:

If it is publicly used, mark with FXAPI like this: class FXAPI PublicClass
If it is only internally used, mark with FXDLLLOCAL like this: class FXDLLLOCAL PublicClass
An exception is types which can be thrown as an exception across a shared object boundary - these require special demarcation: class FXEXCEPTIONAPI(FXAPI) PublicThrowableClass. You need not do this for types which are never thrown across a shared object boundary.

Individual member functions of an exported class, in particular ones which are private, non-virtual, and are not used by friendly code, should be marked individually with FXDLLLOCAL.

In your build system (Makefile etc), you will need to define the GCC_HASCLASSVISIBILITY if GCC is v4.0 or later and the user has configured in support. You will probably also wish to add the -fvisibility=hidden and -fvisibility-inlines-hidden options to the command line arguments of every GCC invocation. Remember to test your library thoroughly afterwards, including that all exceptions correctly traverse shared object boundaries.
If you want to see before and after results, use the command nm -C -D <library>.so which lists in demangled form all exported symbols.
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

GMT+8, 2024-11-2 20:24 , Processed in 0.035987 second(s), 16 queries .

© 2021 Powered by Discuz! X3.5.

快速回复 返回顶部 返回列表