75 votes

Exporter tous les symboles lors de la création d'une DLL

Avec VS2005, je veux créer une DLL et exporter automatiquement tous les symboles sans ajouter __declspec(dllexport) partout et sans créer manuellement des fichiers .def. Existe-t-il un moyen de faire cela ?

45voto

Andrew Stein Points 6344

C'est possible...

La façon dont nous le faisons ici est d'utiliser l'option /DEF de l'éditeur de liens pour passer un fichier "fichier de définition de module" contenant une liste de nos exportations. Je vois dans votre question que vous connaissez ces fichiers. Cependant, nous ne le faisons pas à la main. La liste des exportations elle-même est créée par le dumpbin /LINKERMEMBER, et de manipuler la sortie via un simple script au format d'un fichier de définition de module.

C'est beaucoup de travail à mettre en place, mais cela nous permet de compiler du code créé sans déclarations dllexport pour Unix sur Windows.

11 votes

Il est généralement préférable d'ajouter vos macros d'exportation, qui se développent en __declspec(dllexport) sur Windows, __attribute__ ((dllexport)) sur gcc, et vide sur les autres compilateurs. Ensuite, passez -fvisibility=hidden sur gcc. Vous obtiendrez une table de symboles plus petite et plus propre et vous attraperez des erreurs qui auraient cassé la version Windows lors des tests sous Linux.

17 votes

Le PO ne voulait pas écrire __declspec(dllexport) partout. Ajouter une autre macro d'exportation partout est tout aussi difficile.

44voto

Maks Points 1072

Réponse courte

Vous pouvez le faire à l'aide de la nouvelle version de CMake (toute version cmake-3.3.20150721-g9cd2f-win32-x86.exe ou supérieure).

Actuellement, elle se trouve dans la branche dev. Plus tard, la fonctionnalité sera ajoutée dans la version release de cmake-3.4.

Lien vers le dev. cmake :

cmake_dev

Lien vers un article qui décrit la technique :

Créer des dlls sous Windows sans declspec() en utilisant la nouvelle fonctionnalité CMake export all

Lien vers un exemple de projet :

cmake_windows_export_all_symbols


Réponse longue

Attention : Toutes les informations ci-dessous concernent le compilateur MSVC ou Visual Studio.

Si vous utilisez d'autres compilateurs comme gcc sous Linux ou le compilateur MinGW gcc sous Windows, vous n'aurez pas d'erreurs de liaison dues à des symboles non exportés, car le compilateur gcc exporte tous les symboles dans une bibliothèque dynamique (dll) par défaut à la place des compilateurs MSVC ou Intel Windows.

Sous Windows, vous devez exporter explicitement le symbole d'une dll.

De plus amples informations à ce sujet sont fournies par des liens :

Exportation à partir d'une DLL

Comment faire : Exporter les classes C++ d'une DLL

Ainsi, si vous souhaitez exporter tous les symboles d'une dll avec MSVC (compilateur Visual Studio), vous avez deux possibilités :

  • Utilisez le mot-clé __declspec(dllexport) dans la définition de la classe/fonction.
  • Créez un fichier de définition de module (.def) et utilisez le fichier .def lors de la construction de la DLL.

1. Utilisez le mot clé __declspec(dllexport) dans la définition de la classe/fonction.


1.1. Ajoutez les macros "__declspec(dllexport) / __declspec(dllimport)" à une classe ou une méthode que vous voulez utiliser. Ainsi, si vous voulez exporter toutes les classes, vous devez ajouter les macros suivantes à chacune d'entre elles

Plus d'informations à ce sujet sont fournies par le lien :

Exportation à partir d'une DLL en utilisant __declspec(dllexport)

Exemple d'utilisation (remplacer "Projet" par le nom réel du projet) :

// 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

Ensuite, ajoutez "PROJECTAPI" à toutes les classes. Définir "USEPROJECTLIBRARY" seulement si vous voulez exporter/importer des symboles de la dll. Définir "PROJECTLIBRARY_EXPORTS" pour la dll.

Exemple d'exportation de classe :

#include "ProjectExport.h"

namespace hello {
    class PROJECTAPI Hello {}   
}

Exemple d'exportation de fonctions :

#include "ProjectExport.h"

PROJECTAPI void HelloWorld();

Attention : n'oubliez pas d'inclure le fichier "ProjectExport.h".


1.2. Exportation en tant que fonctions C. Si vous utilisez le compilateur C++ pour compiler du code écrit en C, vous pouvez ajouter extern "C" devant une fonction pour éliminer la confusion des noms.

Plus d'informations sur la manipulation des noms C++ sont fournies par le lien :

Décoration du nom

Exemple d'utilisation :

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

Plus d'informations à ce sujet sont fournies par le lien :

Exportation de fonctions C++ pour utilisation dans des exécutables en langage C


2. Créez un fichier de définition de module (.def) et utilisez le fichier .def lors de la construction de la DLL.

Plus d'informations à ce sujet sont fournies par le lien :

Exportation à partir d'une DLL en utilisant les fichiers DEF

Je décris ensuite trois approches pour créer un fichier .def.


2.1. Fonctions d'exportation C

Dans ce cas, vous pouvez simplement ajouter des déclarations de fonctions dans le fichier .def à la main.

Exemple d'utilisation :

extern "C" void HelloWorld();

Exemple de fichier .def (convention de dénomination __cdecl) :

EXPORTS 
_HelloWorld

2.2. Exportation des symboles de la bibliothèque statique

J'ai essayé l'approche suggérée par "user72260".

Il a dit :

  • Tout d'abord, vous pouvez créer une bibliothèque statique.
  • Utilisez ensuite "dumpbin /LINKERMEMBER" pour exporter tous les symboles de la bibliothèque statique.
  • Analyse la sortie.
  • Mettez tous les résultats dans un fichier .def.
  • Créer une dll avec le fichier .def.

J'ai utilisé cette approche, mais ce n'est pas très pratique de toujours créer deux builds (un en tant que bibliothèque statique et l'autre en tant que bibliothèque dynamique). Cependant, je dois admettre que cette approche fonctionne vraiment.


2.3. Exporter les symboles à partir de fichiers .obj ou avec l'aide de CMake.


2.3.1. Avec l'utilisation de CMake

Avis important : Vous n'avez pas besoin d'exporter des macros vers des classes ou des fonctions !

Avis important : Vous ne pouvez pas utiliser /GL ( Optimisation de l'ensemble du programme ) lorsqu'on utilise cette approche !

  • Créer un projet CMake basé sur le fichier "CMakeLists.txt".
  • Ajoutez la ligne suivante au fichier "CMakeLists.txt" : set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
  • Ensuite, créez un projet Visual Studio avec l'aide de "CMake (cmake-gui)".
  • Compilez le projet.

Exemple d'utilisation :

Dossier racine

CMakeLists.txt (Dossier racine)

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 (Dossier racine)

#include "foo.h"

int main() {
    HelloWorld();

    return 0;
}

Dossier Foo (Dossier racine / Dossier Foo)

CMakeLists.txt (dossier Foo)

project(foo)

set(SOURCE_LIB foo.cpp)

add_library(foo SHARED ${SOURCE_LIB})

foo.h (dossier Foo)

void HelloWorld();

foo.cpp (dossier Foo)

#include <iostream>

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

Reliez à nouveau le projet d'exemple :

cmake_windows_export_all_symbols

CMake utilise une approche différente de celle de "2.2. Exporter les symboles de la bibliothèque statique".

Il fait ce qui suit :

1) Créer un fichier "objects.txt" dans le répertoire de construction avec des informations sur les fichiers .obj utilisés dans une dll.

2) Compiler la dll, c'est-à-dire créer des fichiers .obj.

3) Sur la base des informations du fichier "objects.txt", extraire tous les symboles du fichier .obj.

Exemple d'utilisation :

DUMPBIN /SYMBOLS example.obj > log.txt

Plus d'informations à ce sujet sont fournies par le lien :

/SYMBOLS

4) Analyse des informations extraites du fichier .obj.

A mon avis, j'utiliserais la convection d'appel, par exemple "__cdecl/__fastcall", le champ de symbole "SECTx/UNDEF" (la troisième colonne), le champ de symbole "External/Static" (la cinquième colonne), les informations " ??", " ?" pour le parsing d'un fichier .obj.

Je ne sais pas exactement comment CMake analyse un fichier .obj. Cependant, CMake est open source, donc vous pourriez trouver si cela vous intéresse.

Lien vers le projet CMake :

CMake_github

5) Mettez tous les symboles exportés dans un fichier .def.

6) Lier une dll avec l'utilisation d'un fichier .def créé.

Les étapes 4)-5), c'est-à-dire analyser les fichiers .obj et créer un fichier .def avant de lier et d'utiliser le fichier .def, CMake le fait avec l'aide de "Pre-Link event". Pendant que "Pre-Link event" se déclenche, vous pouvez appeler n'importe quel programme que vous voulez. Ainsi, dans le cas de l'utilisation de CMake, l'événement "Pre-Link" appelle CMake avec les informations suivantes : où placer le fichier .def et où placer le fichier "objects.txt" et avec l'argument "-E __create_def". Vous pouvez vérifier cette information en créant un projet CMake Visusal Studio avec "set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)" et ensuite vérifier le fichier ".vcxproj" pour la dll.

Si vous essayez de compiler un projet sans "set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)" ou avec "set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF)", vous obtiendrez des erreurs de liaison, dues au fait que les symboles ne sont pas exportés depuis une dll.

Plus d'informations à ce sujet sont fournies par le lien :

Comprendre les étapes de construction personnalisées et les événements de construction


2.3.2. Sans utilisation de CMake

Vous pourriez simplement créer un petit programme pour analyser les fichiers .obj par vous-même sans utiliser CMake. Cependant, je dois admettre que CMake est un programme très utile, surtout pour le développement multiplateforme.

10voto

jww Points 9514

Je veux créer une DLL et exporter automatiquement tous les symboles sans ajouter __declspec(dllexport) partout et sans créer manuellement des fichiers .def. Existe-t-il un moyen de le faire ?

Cette réponse est tardive, mais elle fournit les détails de la réponse de Maks dans la section (2). Elle évite également les scripts et utilise un programme C++ appelé dump2def . Le code source de dump2def est ci-dessous.

Enfin, les étapes ci-dessous supposent que vous travaillez à partir d'un fichier Visual Studio Invitation au développeur qui est un terminal Windows où vcvarsall.bat a été exécuté. Vous devez vous assurer que les outils de construction tels que cl.exe , lib.exe , link.exe et nmake.exe sont sur le chemin.

Plus d'informations à ce sujet sont fournies par le lien :

Exporter à partir d'une DLL en utilisant DEF Fichiers
...

L'instruction ci-dessous utilise :

  • static.lib - archive de bibliothèque statique (fichier *.a sur Linux)
  • dynamic.dll - bibliothèque dynamique (fichier *.so sous Linux)
  • import.lib - bibliothèque dynamique (bibliothèque d'importation sous Windows)

Notez également que même si vous exportez tout à partir de la DLL, les clients doivent toujours utiliser la fonction declspec(dllimport) sur tous les symboles (classes, fonctions et données) qu'ils utilisent. Voir aussi sur MSDN.

Tout d'abord, prenez vos objets et créez une archive statique :

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:$@

Deuxièmement, courir dumpbin.exe /LINKERMEMEBER sur l'archive pour créer un *.dump fichier :

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

Troisièmement, courir dump2def.exe sur le *.dump pour produire le fichier *.def fichier. Le code source de dump2def.exe est ci-dessous.

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

Quatrièmement, construisez la 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 est utilisé pour éviter cet avertissement. Il est attendu dans ce cas :

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

Lorsque le dynamic.dll est invoquée, elle crée un dynamic.lib le fichier d'importation et dynamic.exp également :

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

Et :

 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

Voici à quoi ressemble le makefile de Nmake. Il fait partie d'un véritable fichier 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

Et voici le code source de 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;
}

9voto

user72260 Points 111

J'ai écrit un petit programme pour analyser la sortie de "dumpbin /linkermember" sur le fichier .lib. J'ai plus de 8000 références de fonctions à exporter d'une DLL.

Le problème de le faire sur une DLL est que vous devez lier la DLL sans les définitions exportées une fois pour créer le fichier .lib, puis générer le .def, ce qui signifie que vous devez à nouveau lier la DLL avec le fichier .def pour que les références soient réellement exportées.

Travailler avec des bibliothèques statiques est plus facile. Compilez toutes vos sources en librairies statiques, exécutez dumbin, générez un .def avec votre petit programme, puis liez les librairies ensemble dans une DLL maintenant que les noms d'exportation sont disponibles.

Malheureusement, ma société ne me permet pas de vous montrer la source. Le travail à faire est de reconnaître quels "symboles publics" dans la sortie du dump ne sont pas nécessaires dans votre fichier def. Vous devez jeter un grand nombre de ces références, NULL_IMPORT_DESCRIPTOR, NULL_THUNK_DATA, __imp*, etc.

0 votes

Comment faire avec les modèles qui ont des membres sur des fichiers cpp ?

0 votes

@rxantos - Utilisation Instanciations explicites pour forcer l'instanciation à l'avance. Ou bien, assurez-vous qu'il s'agit d'une implémentation d'en-tête seulement (ce qui ne semble pas être le cas).

0 votes

@user72260 - Construire une librairie statique au lieu d'une DLL en utilisant les mêmes objets. Exécuter dumpbin.exe sur la librairie statique. Vous n'aurez pas NULL_IMPORT_DESCRIPTOR , NULL_THUNK_DATA , __imp* etc. Ensuite, créez la DLL avec les mêmes objets et le nouveau fichier DEF.

4voto

Sergey Points 163

Merci @Maks pour le réponse détaillée .

Vous trouverez ci-dessous un exemple de ce que j'ai utilisé dans l'événement Pre-Link pour générer un fichier def à partir d'un obj. J'espère qu'il sera utile à quelqu'un.

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

En fait, j'ai juste pris un des objets (mdb.obj) et j'ai recherché les fonctions mdb_*. Puis j'ai analysé la sortie pour ne garder que les noms en tenant compte du nombre d'espaces pour l'indentation (un après la division en tokens et un autre en echo. Je ne sais pas si c'est important).

Le monde réel script sera probablement un peu plus complexe cependant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X