【问题标题】:Export all symbols when creating a DLL创建 DLL 时导出所有符号
【发布时间】:2010-09-18 12:48:31
【问题描述】:

使用 VS2005,我想创建一个 DLL 并自动导出所有符号,而无需在任何地方添加 __declspec(dllexport),也无需手动创建 .def 文件。有没有办法做到这一点?

【问题讨论】:

    标签: c++ windows visual-studio visual-studio-2005


    【解决方案1】:

    简答

    您可以借助新版本的 CMake(任何版本的 cmake-3.3.20150721-g9cd2f-win32-x86.exe 或更高版本)来完成。

    目前它在 dev 分支中。 后续会在cmake-3.4的release版本中加入该功能。

    链接到 cmake 开发:

    cmake_dev

    链接到描述该技术的文章:

    Create dlls on Windows without declspec() using new CMake export all feature

    链接到示例项目:

    cmake_windows_export_all_symbols


    长答案

    注意: 以下所有信息都与 MSVC 编译器或 Visual Studio 有关。

    如果您在 Linux 上使用 gcc 或在 Windows 上使用 MinGW gcc 编译器等其他编译器,则不会因未导出符号而出现链接错误,因为 gcc 编译器默认导出动态库 (dll) 中的所有符号,而不是 MSVC 或英特尔 windows 编译器。

    在 Windows 中,您必须从 dll 中显式导出符号。

    更多信息由链接提供:

    Exporting from a DLL

    HowTo: Export C++ classes from a DLL

    因此,如果您想使用 MSVC(Visual Studio 编译器)从 dll 中导出所有符号,您有两种选择:

    • 在类/函数的定义中使用关键字 __declspec(dllexport)。
    • 创建模块定义 (.def) 文件并在构建 DLL 时使用 .def 文件。

    1.在类/函数的定义中使用关键字 __declspec(dllexport)


    1.1.将“__declspec(dllexport) / __declspec(dllimport)”宏添加到要使用的类或方法中。所以如果你想导出所有类,你应该把这个宏添加到所有类中

    链接提供了更多信息:

    Exporting from a DLL Using __declspec(dllexport)

    使用示例(将“项目”替换为真实项目名称):

    // ProjectExport.h
    
    #ifndef __PROJECT_EXPORT_H
    #define __PROJECT_EXPORT_H
    
    #ifdef USEPROJECTLIBRARY
    #ifdef  PROJECTLIBRARY_EXPORTS 
    #define PROJECTAPI __declspec(dllexport)
    #else
    #define PROJECTAPI __declspec(dllimport)
    #endif
    #else
    #define PROJECTAPI
    #endif
    
    #endif
    

    然后将“PROJECTAPI”添加到所有类。 仅当您希望从 dll 导出/导入符号时才定义“USEPROJECTLIBRARY”。 为 dll 定义“PROJECTLIBRARY_EXPORTS”。

    类导出示例:

    #include "ProjectExport.h"
    
    namespace hello {
        class PROJECTAPI Hello {}   
    }
    

    函数导出示例:

    #include "ProjectExport.h"
    
    PROJECTAPI void HelloWorld();
    

    注意:不要忘记包含“ProjectExport.h”文件。


    1.2.导出为 C 函数。 如果你使用 C++ 编译器编译代码是用 C 编写的,你可以在函数前面添加 extern "C" 以消除名称混淆

    有关 C++ 名称修饰的更多信息由链接提供:

    Name Decoration

    使用示例:

    extern "C" __declspec(dllexport) void HelloWorld();
    

    链接提供了更多信息:

    Exporting C++ Functions for Use in C-Language Executables


    2.创建模块定义 (.def) 文件并在构建 DLL 时使用 .def 文件

    链接提供了更多信息:

    Exporting from a DLL Using DEF Files

    我进一步描述了关于如何创建 .def 文件的三种方法。


    2.1.导出 C 函数

    在这种情况下,您可以简单地手动在 .def 文件中添加函数声明。

    使用示例:

    extern "C" void HelloWorld();
    

    .def 文件示例(__cdecl 命名约定):

    EXPORTS 
    _HelloWorld
    

    2.2.从静态库中导出符号

    我尝试了“user72260”建议的方法。

    他说:

    • 首先,您可以创建静态库。
    • 然后使用“dumpbin /LINKERMEMBER”从静态库中导出所有符号。
    • 解析输出。
    • 将所有结果放入 .def 文件中。
    • 使用 .def 文件创建 dll。

    我使用了这种方法,但总是创建两个构建(一个作为静态库,另一个作为动态库)不太方便。但是,我不得不承认,这种方法确实有效。


    2.3.从 .obj 文件或借助 CMake 导出符号


    2.3.1.使用 CMake

    重要提示:您不需要任何导出宏到类或函数!

    重要提示:使用此方法时不能使用 /GL (Whole Program Optimization)!

    • 根据“CMakeLists.txt”文件创建CMake项目。
    • 将以下行添加到“CMakeLists.txt”文件中: 设置(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
    • 然后在“CMake (cmake-gui)”的帮助下创建 Visual Studio 项目。
    • 编译项目。

    使用示例:

    根文件夹

    CMakeLists.txt(根文件夹)

    cmake_minimum_required(VERSION 2.6)
    project(cmake_export_all)
    
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
    
    set(dir ${CMAKE_CURRENT_SOURCE_DIR})
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${dir}/bin")
    
    set(SOURCE_EXE main.cpp)
    
    include_directories(foo)
    
    add_executable(main ${SOURCE_EXE})
    
    add_subdirectory(foo)
    
    target_link_libraries(main foo)
    

    main.cpp(根文件夹)

    #include "foo.h"
    
    int main() {
        HelloWorld();
    
        return 0;
    }
    

    Foo 文件夹(根文件夹/Foo 文件夹)

    CMakeLists.txt(Foo 文件夹)

    project(foo)
    
    set(SOURCE_LIB foo.cpp)
    
    add_library(foo SHARED ${SOURCE_LIB})
    

    foo.h(Foo 文件夹)

    void HelloWorld();
    

    foo.cpp(Foo 文件夹)

    #include <iostream>
    
    void HelloWorld() {
        std::cout << "Hello World!" << std::endl;
    }
    

    再次链接到示例项目:

    cmake_windows_export_all_symbols

    CMake 使用不同于“2.2. 从静态库中导出符号”的方法。

    它执行以下操作:

    1) 在构建目录中创建“objects.txt”文件,其中包含在dll中使用的.obj文件的信息。

    2) 编译dll,即创建.obj文件。

    3) 根据“objects.txt”文件信息从.obj文件中提取所有符号。

    使用示例:

    DUMPBIN /SYMBOLS example.obj > log.txt
    

    链接提供了更多信息:

    /SYMBOLS

    4) 从 .obj 文件信息中提取解析。

    我认为我会使用调用对流,例如“__cdecl/__fastcall”、“SECTx/UNDEF”符号字段(第三列)、“外部/静态”符号字段(第五列)、“??” ,“?”解析 .obj 文件的信息。

    我不知道 CMake 究竟是如何解析 .obj 文件的。 但是,CMake 是开源的,因此您可以了解它是否对您感兴趣。

    链接到 CMake 项目:

    CMake_github

    5) 将所有导出的符号放入 .def 文件中。

    6) 使用 .def 创建的文件链接 dll。

    步骤 4)-5),即在链接和使用 .def 文件之前解析 .obj 文件并创建一个 .def 文件 CMake 在“预链接事件”的帮助下执行。 当“预链接事件”触发时,您可以调用任何您想要的程序。 因此,在“CMake 使用”“预链接事件”的情况下,使用以下信息调用 CMake,其中包含有关将 .def 文件放在何处以及“objects.txt”文件在何处以及参数“-E __create_def”的信息。 您可以通过使用“set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)”创建 CMake Visusal Studio 项目来检查此信息,然后检查 dll 的“.vcxproj”项目文件。

    如果您尝试在没有“set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)”或“set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF)”的情况下编译项目,由于符号不是从 dll 导出的事实,您将收到链接错误。

    链接提供了更多信息:

    Understanding Custom Build Steps and Build Events


    2.3.2.不使用 CMake

    您可以自己创建一个小程序来解析 .obj 文件,而无需使用 CMake。但是,我不得不承认 CMake 是非常有用的程序,尤其是对于跨平台开发。

    【讨论】:

    • 这是真正的交易!非常感谢。
    • 这是很棒的信息。我只想补充一点,选项 1 是 OP not 想要做的。选项 2 回答了他的问题。具体来说,2.3 是接受的 anwser 和 @user72260 的新信息。
    • 在 Windows 上,从 Visual Studio 2015 Update 2 开始,链接器具有 /WHOLEARCHIVE 选项。见docs.microsoft.com/en-us/cpp/build/reference/…
    • 刚刚尝试玩 Pre-Link 事件。似乎工作得很好。谢谢!以下是我对 liblmdb 的实验结果。 `dumpbin /SYMBOLS $(平台)\$(配置)\mdb.obj | findstr /R "().*External.*mdb_.*" > $(Platform)\$(Configuration)\mdb_symbols & (echo EXPORTS & for /F "usebackq tokens=2 delims==|" %%E in ( type $(Platform)\$(Configuration)\mdb_symbols) 做@echo %%E) > $(Platform)\$(Configuration)\lmdb.def
    • 对于mingw用户,变量CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS名字含糊,只适用于msvc。
    【解决方案2】:

    可以做到……

    我们在这里做的方式是使用链接器的 /DEF 选项来传递一个包含我们导出列表的"module definition file"。我从您的问题中看到您了解这些文件。但是,我们不会手动操作。导出列表本身由dumpbin /LINKERMEMBER 命令创建,并通过简单脚本将输出操作为模块定义文件的格式。

    设置工作量很大,但它允许我们在 Windows 上为 Unix 编译没有 dllexport 声明的代码。

    【讨论】:

    • 通常最好添加导出宏,在 Windows 上扩展为 __declspec(dllexport),在 gcc 上扩展为 __attribute__ ((dllexport)),在其他编译器上为空。然后在 gcc 上传递-fvisibility=hidden。您将获得一个更小、更清晰的符号表,并且在 Linux 上进行测试时,您会发现会破坏 Windows 构建的错误。
    • OP不想到处写__declspec(dllexport)。在任何地方添加一些其他导出宏同样困难。
    【解决方案3】:

    我想创建一个 DLL 并自动导出所有符号,而无需在任何地方添加 __declspec(dllexport) 并且无需手动创建 .def 文件。有没有办法做到这一点?

    这是一个较晚的答案,但它提供了 Maks 在第 (2) 节中的答案的详细信息。它还避免使用脚本并使用名为dump2def 的C++ 程序。 dump2def的源代码如下。

    最后,以下步骤假设您使用 Visual Studio Developer Prompt 工作,这是一个运行 vcvarsall.bat 的 Windows 终端。您需要确保 cl.exelib.exelink.exenmake.exe 等构建工具在路径上。

    链接提供了更多信息:

    Exporting from a DLL Using DEF Files
    ...

    以下指令使用:

    • static.lib - 静态库存档(Linux 上的 *.a 文件)
    • dynamic.dll - 动态库(Linux 上的 *.so 文件)
    • import.lib - 动态库(Windows 上的导入库)

    还请注意,尽管您从 DLL 导出所有内容,但客户端仍然必须在他们使用的所有符号(类、函数和数据)上使用 declspec(dllimport)。另请参阅 MSDN。

    首先,获取您的对象并创建一个静态存档:

    AR = lib.exe
    ARFLAGS = /nologo
    
    CXX_SRCS = a.cpp b.cpp c.cpp ...
    LIB_OBJS = a.obj b.obj c.obj ...
    
    static.lib: $(LIB_OBJS)
        $(AR) $(ARFLAGS) $(LIB_OBJS) /out:$@
    

    其次,在存档上运行dumpbin.exe /LINKERMEMEBER 以创建*.dump 文件:

    dynamic.dump:
        dumpbin /LINKERMEMBER static.lib > dynamic.dump
    

    第三,在*.dump 文件上运行dump2def.exe 以生成*.def 文件。 dump2def.exe的源代码如下。

    dynamic.def: static.lib dynamic.dump
        dump2def.exe dynamic.dump dynamic.def
    

    四、构建DLL:

    LD = link.exe
    LDFLAGS = /OPT:REF /MACHINE:X64
    LDLIBS = kernel32.lib
    
    dynamic.dll: $(LIB_OBJS) dynamic.def
        $(LD) $(LDFLAGS) /DLL /DEF:dynamic.def /IGNORE:4102 $(LIB_OBJS) $(LDLIBS) /out:$@
    

    /IGNORE:4102 用于避免此警告。在这种情况下预计:

    dynamic.def : warning LNK4102: export of deleting destructor 'public: virtual v
    oid * __ptr64 __cdecl std::exception::`scalar deleting destructor'(unsigned int)
     __ptr64'; image may not run correctly
    

    当调用dynamic.dll 配方时,它也会创建一个dynamic.lib 导入文件和dynamic.exp 文件:

    > cls && nmake /f test.nmake dynamic.dll
    ...
    Creating library dynamic.lib and object dynamic.exp
    

    还有:

     C:\Users\Test\testdll>dir *.lib *.dll *.def *.exp
     Volume in drive C is Windows
     Volume Serial Number is CC36-23BE
    
     Directory of C:\Users\Test\testdll
    
    01/06/2019  08:33 PM        71,501,578 static.lib
    01/06/2019  08:33 PM        11,532,052 dynamic.lib
    
     Directory of C:\Users\Test\testdll
    
    01/06/2019  08:35 PM         5,143,552 dynamic.dll
    
     Directory of C:\Users\Test\testdll
    
    01/06/2019  08:33 PM         1,923,070 dynamic.def
    
     Directory of C:\Users\Test\testdll
    
    01/06/2019  08:35 PM         6,937,789 dynamic.exp
                   5 File(s)     97,038,041 bytes
                   0 Dir(s)  139,871,186,944 bytes free
    

    在这里将它们粘合在一起就是 Nmake makefile 的样子。它是real Nmake file 的一部分:

    all: test.exe
    
    test.exe: pch.pch static.lib $(TEST_OBJS)
        $(LD) $(LDFLAGS) $(TEST_OBJS) static.lib $(LDLIBS) /out:$@
    
    static.lib: $(LIB_OBJS)
        $(AR) $(ARFLAGS) $(LIB_OBJS) /out:$@
    
    dynamic.map:
        $(LD) $(LDFLAGS) /DLL /MAP /MAPINFO:EXPORTS $(LIB_OBJS) $(LDLIBS) /out:dynamic.dll
    
    dynamic.dump:
        dumpbin.exe /LINKERMEMBER static.lib /OUT:dynamic.dump
    
    dynamic.def: static.lib dynamic.dump
        dump2def.exe dynamic.dump
    
    dynamic.dll: $(LIB_OBJS) dynamic.def
        $(LD) $(LDFLAGS) /DLL /DEF:dynamic.def /IGNORE:4102 $(LIB_OBJS) $(LDLIBS) /out:$@
    
    clean:
        $(RM) /F /Q pch.pch $(LIB_OBJS) pch.obj static.lib $(TEST_OBJS) test.exe *.pdb
    

    这里是dump2def.exe的源代码:

    #include <iostream>
    #include <fstream>
    #include <sstream>
    #include <string>
    #include <vector>
    #include <set>
    
    typedef std::set<std::string> SymbolMap;
    
    void PrintHelpAndExit(int code)
    {
        std::cout << "dump2def - create a module definitions file from a dumpbin file" << std::endl;
        std::cout << "           Written and placed in public domain by Jeffrey Walton" << std::endl;
        std::cout << std::endl;
    
        std::cout << "Usage: " << std::endl;
    
        std::cout << "  dump2def <infile>" << std::endl;
        std::cout << "    - Create a def file from <infile> and write it to a file with" << std::endl;
        std::cout << "      the same name as <infile> but using the .def extension" << std::endl;
    
        std::cout << "  dump2def <infile> <outfile>" << std::endl;
        std::cout << "    - Create a def file from <infile> and write it to <outfile>" << std::endl;
    
        std::exit(code);
    }
    
    int main(int argc, char* argv[])
    {
        // ******************** Handle Options ******************** //
    
        // Convenience item
        std::vector<std::string> opts;
        for (size_t i=0; i<argc; ++i)
            opts.push_back(argv[i]);
    
        // Look for help
        std::string opt = opts.size() < 3 ? "" : opts[1].substr(0,2);
        if (opt == "/h" || opt == "-h" || opt == "/?" || opt == "-?")
            PrintHelpAndExit(0);
    
        // Add <outfile> as needed
        if (opts.size() == 2)
        {
            std::string outfile = opts[1];
            std::string::size_type pos = outfile.length() < 5 ? std::string::npos : outfile.length() - 5;
            if (pos == std::string::npos || outfile.substr(pos) != ".dump")
                PrintHelpAndExit(1);
    
            outfile.replace(pos, 5, ".def");
            opts.push_back(outfile);
        }
    
        // Check or exit
        if (opts.size() != 3)
            PrintHelpAndExit(1);
    
        // ******************** Read MAP file ******************** //
    
        SymbolMap symbols;
    
        try
        {
            std::ifstream infile(opts[1].c_str());
            std::string::size_type pos;
            std::string line;
    
            // Find start of the symbol table
            while (std::getline(infile, line))
            {
                pos = line.find("public symbols");
                if (pos == std::string::npos) { continue; }        
    
                // Eat the whitespace after the table heading
                infile >> std::ws;
                break;
            }
    
            while (std::getline(infile, line))
            {
                // End of table
                if (line.empty()) { break; }
    
                std::istringstream iss(line);
                std::string address, symbol;
                iss >> address >> symbol;
    
                symbols.insert(symbol);
            }
        }
        catch (const std::exception& ex)
        {
            std::cerr << "Unexpected exception:" << std::endl;
            std::cerr << ex.what() << std::endl;
            std::cerr << std::endl;
    
            PrintHelpAndExit(1);
        }
    
        // ******************** Write DEF file ******************** //
    
        try
        {
            std::ofstream outfile(opts[2].c_str());
    
            // Library name, cryptopp.dll
            std::string name = opts[2];
            std::string::size_type pos = name.find_last_of(".");
    
            if (pos != std::string::npos)
                name.erase(pos);
    
            outfile << "LIBRARY " << name << std::endl;
            outfile << "DESCRIPTION \"Crypto++ Library\"" << std::endl;        
            outfile << "EXPORTS" << std::endl;
            outfile << std::endl;
    
            outfile << "\t;; " << symbols.size() << " symbols" << std::endl;
    
            // Symbols from our object files
            SymbolMap::const_iterator it = symbols.begin();
            for ( ; it != symbols.end(); ++it)
                outfile << "\t" << *it << std::endl;
        }
        catch (const std::exception& ex)
        {
            std::cerr << "Unexpected exception:" << std::endl;
            std::cerr << ex.what() << std::endl;
            std::cerr << std::endl;
    
            PrintHelpAndExit(1);
        }   
    
        return 0;
    }
    

    【讨论】:

      【解决方案4】:

      我编写了一个小程序来解析 .lib 文件中“dumpbin /linkermember”的输出。我有超过 8,000 个函数引用要从一个 DLL 导出。

      在 DLL 上执行此操作的问题是,您必须在没有导出定义的情况下链接 DLL 一次以创建 .lib 文件,然后生成 .def,这意味着您现在必须再次使用 .def 重新链接 DLL文件以实际导出引用。

      使用静态库更容易。将所有源代码编译到静态库中,运行 dumbin,使用您的小程序生成 .def,然后在导出名称可用时将这些库链接到 DLL 中。

      很遗憾,我的公司不允许我向您展示来源。所涉及的工作是识别转储输出中的哪些“公共符号”在您的 def 文件中不需要。你必须扔掉很多引用,NULL_IMPORT_DESCRIPTOR、NULL_THUNK_DATA、__imp* 等。

      【讨论】:

      • 如何处理在 cpp 文件中有成员的模板?
      • @rxantos - 使用 Explicit Instantiations 强制实例化。或者,确保它是一个仅标题的实现(听起来你没有)。
      • @user72260 - 使用相同的对象构建静态库而不是 DLL。在静态库上运行 dumpbin.exe。您不会有NULL_IMPORT_DESCRIPTORNULL_THUNK_DATA__imp* 等。然后,使用相同的对象和新的 DEF 文件创建 DLL。
      【解决方案5】:

      感谢@Maks 提供detailed answer

      以下是我在 Pre-Link 事件中用于从 obj 生成 def 文件的示例。我希望它对某人有所帮助。

      dumpbin /SYMBOLS $(Platform)\$(Configuration)\mdb.obj | findstr /R "().*External.*mdb_.*" > $(Platform)\$(Configuration)\mdb_symbols
      (echo EXPORTS & for /F "usebackq tokens=2 delims==|" %%E in (`type $(Platform)\$(Configuration)\mdb_symbols`) do @echo  %%E) > $(Platform)\$(Configuration)\lmdb.def
      

      基本上我只取了一个对象 (mdb.obj) 并 grepped mdb_* 函数。然后解析输出以保留名称,同时考虑缩进的空格数量(一个在拆分为标记后,另一个在回显中。我不知道这是否重要)。

      不过,现实世界的脚本可能会更复杂。

      【讨论】:

        【解决方案6】:

        也许有人发现我的 Python 脚本对于将 .dump 转换为 .def 很有用。

        import sys, os
        functions = []
        startPoint = False
        # Exclude standard API like sprintf to avoid multiple definition link error
        excluded_functions = [ 'sprintf', 'snprintf', 'sscanf', 'fprintf' ]
        
        if len(sys.argv) < 2:
            print('Usage: %s <Input .dump file> <Output .def file>.' % sys.argv[0])
            print('Example: %s myStaticLib.dump exports.def' % sys.argv[0])
            sys.exit(1)
        print('%s: Processing %s to %s' % (sys.argv[0], sys.argv[1], sys.argv[2]))
        
        fin = open(sys.argv[1], 'r')
        lines = fin.readlines()
        fin.close()
        
        # Reading
        for l in lines:
            l_str = l.strip()
            if (startPoint == True) and (l_str == 'Summary'): # end point
                break
            if (startPoint == False) and ("public symbols" in l_str):
                startPoint = True
                continue
            if (startPoint == True) and l_str is not '':
                funcName = l_str.split(' ')[-1]
                if funcName not in excluded_functions:
                    functions.append("    " + funcName)
        # Writing
        fout = open(sys.argv[2], 'w')
        fout.write('EXPORTS\n')
        for f in functions:
            fout.write('%s\n' % f)
        fout.close()
        

        使用此脚本,您可以分两步获取 .lib 的 .def 文件:

        dumpbin /LINKERMEMBER:1 myStaticLib.lib > myExports.dump
        python dump2def.py myExports.dump myExports.def
        

        【讨论】:

          【解决方案7】:

          不,您需要一个宏,当它包含在实现导出函数的 .cpp 文件中时解析为 __declspec(dllexport),否则解析为 __declspec(dllimport)

          【讨论】:

            猜你喜欢
            • 2011-03-02
            • 2011-05-15
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-04-14
            • 2017-05-19
            相关资源
            最近更新 更多