创建DLL时导出所有符号

87

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

7个回答

62

简短回答

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

目前它在dev分支中。 稍后,该功能将添加到cmake-3.4的发布版本中。

CMake dev链接:

cmake_dev

描述技术的文章链接:

使用新的CMake导出所有功能,在Windows上创建dlls而不使用declspec()

示例项目链接:

cmake_windows_export_all_symbols


长篇回答

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

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

在Windows中,您必须明确从dll中导出符号。

有关此更多信息,请参见以下链接:

从DLL导出

如何:从DLL导出C++类

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

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

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


1.1. 在你想使用的类或方法中添加“__declspec(dllexport) / __declspec(dllimport)”宏。因此,如果您想要导出所有类,则应将此宏添加到所有类中

有关此的更多信息,请参见链接:

使用 __declspec(dllexport) 从 DLL 导出

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

// 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++编译器进行编译,则可以在函数前添加extern "C"以消除名称修饰

有关C++名称修饰的更多信息,请参见链接:

名称修饰

使用示例:

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

有关此信息的更多内容,请参见链接:

将C++函数导出以供在C语言可执行文件中使用


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

有关此问题的更多信息,请参阅以下链接:

使用DEF文件从DLL导出

接下来,我将描述三种创建.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(整个程序优化)!

  • 基于"CMakeLists.txt"文件创建CMake项目。
  • 将以下行添加到"CMakeLists.txt"文件中:set(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)

主.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" 符号字段(第三列), "External/Static" 符号字段(第五列),以及 "??", "?" 信息来解析.obj文件。

我不知道CMake如何解析.obj文件。 然而,CMake是开源的,如果你感兴趣,可以找到相关信息。

CMake项目链接:

CMake_github

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

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

步骤4)-5),即在链接和使用.def文件之前解析.obj文件并创建.def文件,CMake通过“Pre-Link Event”帮助完成。当“Pre-Link Event”触发时,您可以调用任何想要的程序。因此,在“CMake使用”“Pre-Link Event”中,使用以下信息调用CMake:.def文件的放置位置,"objects.txt"文件的位置以及参数“-E __create_def”。您可以通过创建带有“set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)”的CMake Visual Studio项目来检查此信息,然后检查dll的“.vcxproj”项目文件。
如果尝试编译未使用“set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)”或使用“set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF)”的项目,则会出现链接错误,因为符号未从dll导出。
有关此更多信息,请参见链接: 理解自定义构建步骤和构建事件

2.3.2. 不使用CMake

您可以自己创建一个小程序来解析.obj文件,而不使用CMake。然而,我必须承认,CMake是非常有用的程序,特别是对于跨平台开发。


1
这是真正的交易!非常感谢。 - fotinsky
这是很棒的信息。我只想补充一点,选项1特别是OP不想做的。选项2回答了他的问题。具体来说,2.3是新的信息,超过了已接受答案和@user72260的信息。 - Andrew Stein
在Windows上,自Visual Studio 2015 Update 2起,链接器具有/WHOLEARCHIVE选项。请参见https://learn.microsoft.com/en-us/cpp/build/reference/wholearchive-include-all-library-object-files - bleater
刚试了一下Pre-Link事件,似乎很好用。谢谢!以下是我使用liblmdb进行实验的结果。`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 - Sergey
对于mingw用户来说,变量CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS的名称不太明确,它只适用于msvc。 - javs

47

可以做到这一点...

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

设置起来需要花费很多工作,但它允许我们在Windows上编译没有dllexport声明的Unix代码。


11
通常最好添加您的导出宏,它们会在Windows上扩展为__declspec(dllexport),在gcc上为__attribute__((dllexport)),在其他编译器上为空。然后在gcc上传递-fvisibility=hidden。这样可以获得更小、更干净的符号表,并且当在Linux上测试时,可以捕捉到可能会破坏Windows构建的错误。 - Craig Ringer
21
OP 不想到处写“__declspec(dllexport)”这个东西。随意添加其他的导出宏也同样困难。 - Andrew Stein

13
我想创建一个DLL并自动导出所有符号,而不需要在每个地方添加__declspec(dllexport),也不需要手动创建.def文件。有没有办法做到这一点?
这是一个晚来的答案,但它提供了Maks在第(2)节中的答案的详细信息。它还避免了脚本,并使用了一个名为dump2def的C++程序。下面是dump2def的源代码。
最后,以下步骤假定您正在从Visual Studio Developer Prompt中工作,这是一个Windows终端,其中已运行vcvarsall.bat。您需要确保构建工具,如cl.exelib.exelink.exenmake.exe在路径上。
关于此更多信息,请参考以下链接: 使用DEF文件从DLL导出 以下指令使用:
  • static.lib - 静态库归档文件(*.a文件在Linux上)
  • dynamic.dll - 动态库(*.so文件在Linux上)
  • 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的拼接结果。它是真实的Nmake文件的一部分。
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;
}

10

我写了一个小程序来解析 "dumpbin /linkermember" 命令生成的 .lib 文件的输出。在一个 DLL 中,我要导出超过 8,000 个函数引用。

处理 DLL 的问题在于,您必须先链接不带导出定义的 DLL 以创建 .lib 文件,然后生成 .def 文件,这意味着您现在必须使用 .def 文件重新链接 DLL 以实际导出引用。

使用静态库更容易。编译所有源代码成为静态库,运行 dumbrin,使用您的小程序生成 .def 文件,然后将库链接到一起形成 DLL,此时导出名称可用。

不幸的是,我的公司不允许我向您展示源代码。涉及的工作包括识别“dump”输出中不需要在 def 文件中的“公共符号”。您必须丢弃许多这些引用项,如 NULL_IMPORT_DESCRIPTOR、NULL_THUNK_DATA、__imp*等。


你如何处理在cpp文件中有成员的模板? - rxantos
使用显式实例化来强制预先实例化。或者,确保它是一个仅包含头文件的实现(听起来你没有这个)。 - jww
@user72260 - 使用相同的对象构建静态库而不是 DLL。在静态库上运行 dumpbin.exe。您将不会看到 NULL_IMPORT_DESCRIPTORNULL_THUNK_DATA__imp* 等内容。然后,使用相同的对象和新的 DEF 文件创建 DLL。 - jww

5

感谢@Maks提供的详细答案

以下是我在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)并进行了mdb_*函数的grep。然后解析输出以保留名称,考虑到缩进的空格数量(在将其拆分为标记后和echo中另一个)。虽然我不知道这是否很重要。
现实世界的脚本可能会更加复杂。

3
也许有人会发现我的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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接