Einfach zu verwendender Wrapper über LoadLibrary () und GetProcAddress ()

Präambel


Wie Sie wissen, gibt es zwei Möglichkeiten, dynamisch verknüpfte Bibliotheken (DLLs) zu verwenden: Ladezeit- und Laufzeitverknüpfung . In letzterem Fall müssen Sie die vom Betriebssystem bereitgestellte API verwenden, um das gewünschte Modul (Bibliothek) zu laden und nach der Adresse der erforderlichen Prozedur zu suchen. Es gibt viele Wrapper, aber leider sind alle, auf die ich gestoßen bin, sehr kompliziert und mit unnötigem Code überladen. Die vorgeschlagene Lösung sollte ursprünglich Funktionen aufrufen, die in DLLs von ausführbaren Modulen (EXE) gespeichert sind. Sie zeichnet sich durch eine relativ einfache Implementierung und (viel wichtiger) eine einfache Verwendung im Client-Code aus.

Die Lösung mit der reinen Win32-API sieht ungefähr so ​​aus (praktisch ist dies eine Wiederholung eines Fragments aus MSDN):

typedef int (__cdecl *some_proc_t)(LPWSTR);
HINSTANCE hlib = LoadLibrary(_T("some.dll"));
myproc_t proc_addr = NULL;
int result = -1;
if (hlib) {
    proc_addr = (some_proc_t) GetProcAddress(hlib, "SomeProcName");
    if (proc_addr) {
        result = proc_addr(L"send some string to DLL function");
        printf("Successfully called DLL procedure with result %d", result);
    }
    FreeLibrary("some.dll");
}

Dieser Artikel enthält einen benutzerfreundlichen Wrapper für diese Systemaufrufe. Anwendungsbeispiel:

ntprocedure some_proc_("SomeProcName", _T("some.dll"));
try {
    int result = some_proc_(L"send some string to DLL function");
    printf("Successfully called DLL procedure with result %d", result);
} catch (...) {
    printf("Failed to call DLL procedure");
}

Wie Sie der Auflistung entnehmen können, müssen Sie lediglich ein ntprocedure- Objekt mit Vorlagenparametern erstellen , die dem Typ der aufgerufenen Funktion entsprechen, und den Namen und den Bibliotheksnamen im Konstruktor übergeben.

Implementierung


Bevor ich die Implementierung des Wrappers beschreibe, werde ich eine kleine Header-Datei mit trivialen Deklarationen erstellen, die für viele nutzlos erscheinen und die leicht zu entfernen sind, von mir aber im Code verwendet werden.

Common.h-Datei
#pragma once
#include "tchar.h"
#include 
#define NS_BEGIN_(A) namespace A {
#define NS_BEGIN_A_ namespace {
#define NS_END_ }
#define NO_EXCEPT_ throw()
#define THROW_(E) throw(E)
#define PROHIBITED_ = delete
//=============================================================================
typedef std::basic_string<
  TCHAR, std::char_traits, std::allocator > tstring;


Wir werden darüber nachdenken, wie sichergestellt werden kann, dass sich die zu entwickelnde Vorlagenklasse wie eine Funktion am Aufrufpunkt verhält und eine beliebige Anzahl und Art von Argumenten unterstützt. Das erste, was mir einfällt, ist die Verwendung eines verallgemeinerten Funktors. Autoren bekannter Implementierungen solcher Wrapper tun genau das. In diesem Fall wird abhängig von der Anzahl der Argumente entweder eine teilweise Spezialisierung der Funktionsschablone verwendet oder der Funktionsaufrufoperator mehrfach überladen. Ohne die Hilfe von Makros ist die Sache in der Regel nicht vollständig. Glücklicherweise sind in C ++ 11 Vorlagen mit einer variablen Anzahl von Argumenten erschienen, die das Leben erheblich vereinfachen:

R operator () (Args ... args)

Tatsächlich können Sie in unserem Fall viel einfacher vorgehen, nämlich den Umsetzungsoperator anstelle des Funktionsaufrufoperators verwenden. Wenn T ein Typ eines Funktionszeigers ist und address eine Variable ist, in der seine Adresse gespeichert ist, kann die folgende Anweisung definiert werden:

operator T()
{
    return reinterpret_cast(address);
}

Im Folgenden finden Sie den vollständigen Code für die Headerdatei ntprocedure.h.

Ntprocedure.h-Datei
#pragma once
#include "common.h"
#include 
#include 
#include 
NS_BEGIN_(ntutils)
NS_BEGIN_(detail)
class ntmodule;
class ntprocedure_base {
  ntprocedure_base(const ntprocedure_base&) PROHIBITED_;
  void operator=(const ntprocedure_base&) PROHIBITED_;
public:
  ntprocedure_base(const std::string& a_proc_name, const tstring& a_lib_name);
  // Constructor.
  virtual ~ntprocedure_base() = 0;
  // Destructor.
  FARPROC WINAPI address();
  // Get the procedure address.
  const std::string& name() const;
  // Get the procedure name.
private:
  std::string m_name;
  std::shared_ptr m_module;
};
NS_END_
template class ntprocedure : public detail::ntprocedure_base {
public:
  typedef typename std::remove_pointer::type callable_t;
  typedef callable_t *callable_ptr_t;
  ntprocedure(const std::string& a_proc_name, const tstring& a_lib_name)
  : ntprocedure_base(a_proc_name, a_lib_name),
    m_function(nullptr)
  {    
  }
  // Constructor.
  virtual ~ntprocedure()
  {
  }
  // Destructor.
  operator callable_ptr_t()
  {
    if (!m_function) {
      m_function = reinterpret_cast(address());
    }
    return m_function;
  }
  // Return stored function to invoke.
private:
  callable_ptr_t m_function;    
};
NS_END_


Einige Punkte, die ein aufmerksamer Leser bemerkt hat: Die Adresse der Prozedur wird in der Variablen m_function gespeichert und einmal berechnet, und im zweiten Moment wird ein gemeinsamer Zeiger auf ein Objekt der Klasse ntmodule in der Basisklasse gespeichert . Es ist leicht zu erraten, dass es Informationen über das geladene Modul speichert. Mit shared_ptr können Sie ein Modul automatisch entladen, nachdem Sie alle Prozedurobjekte zerstört haben, die es verwenden.

Ntmodule.h-Datei
#pragma once
#include "common.h"
#include "resource_ptr.h"
#include 
#include 
NS_BEGIN_(ntutils)
NS_BEGIN_(detail)
class ntmodule : public std::enable_shared_from_this {
  ntmodule(const ntmodule&) PROHIBITED_;
  void operator=(const ntmodule&) PROHIBITED_;
public:
  typedef std::list container_t;
  ntmodule(const tstring& a_name);
  // Constructor.
  ~ntmodule();
  // Destructor.
  const tstring& name() const;
  // Get the module name.      
  FARPROC WINAPI address(const std::string& a_name);
  // Get the procedure address.
  std::shared_ptr share();
  // Share this object.
  static container_t& cached();
  // Return the reference to the cache.
private:
  tstring m_name;
  hmodule_ptr m_handle;
};
NS_END_
NS_END_


Betrachten Sie die ntmodule-Klassendefinition:

Ntmodule.cpp-Datei
#include "stdafx.h"
#include "ntmodule.h"
#include "ntprocedure.h"
#include 
#include 
ntutils::detail::ntmodule::ntmodule(const tstring& a_name)
: m_name(a_name)
{
  assert(!a_name.empty());
  cached().push_back(this);
}
ntutils::detail::ntmodule::~ntmodule()
{
  cached().remove(this);
}
const tstring& ntutils::detail::ntmodule::name() const
{
  return m_name;
}
FARPROC WINAPI ntutils::detail::ntmodule::address(
  const std::string& a_name
)
{
  assert(!a_name.empty());
  if (!m_handle) {
    m_handle.reset(::LoadLibrary(m_name.c_str()));    
  }
  if (!m_handle) {
    std::string err("LoadLibrary failed");
    throw std::runtime_error(err);
  }
  return m_handle ? ::GetProcAddress(m_handle, a_name.c_str()) : 0;
}
std::shared_ptr
ntutils::detail::ntmodule::share()
{
  return shared_from_this();
}
ntutils::detail::ntmodule::container_t&
ntutils::detail::ntmodule::cached()
{
  static container_t* modules = new container_t;
  return *modules;
}


Wie Sie sehen, werden Zeiger auf alle verwendeten Module in einer statischen Liste gespeichert. Dies sorgt für Caching. Der Konstruktor der Klasse ntmodule platziert einen Zeiger auf sein Objekt in der Liste, und der Destruktor löscht ihn. Die Definition der Klasse ntprocedure verdeutlicht das Bild vollständig.

Ntprocedure.cpp-Datei
#include "stdafx.h"
#include "ntmodule.h"
#include "ntprocedure.h"
#include 
#include 
ntutils::detail::ntprocedure_base::ntprocedure_base(
  const std::string& a_proc_name, const tstring& a_lib_name
)
: m_name(a_proc_name),
  m_module(nullptr)
{
  assert(!a_proc_name.empty());
  assert(!a_lib_name.empty());
  for (auto module : ntmodule::cached()) {
    // Perform case insensitive comparison:
    if (!lstrcmpi(module->name().c_str(), a_lib_name.c_str())) {
      m_module = module->share();
      break;
    }
  }
  if (!m_module) {
    m_module = std::make_shared(a_lib_name);
  }
}
ntutils::detail::ntprocedure_base::~ntprocedure_base()
{
}
FARPROC WINAPI ntutils::detail::ntprocedure_base::address()
{
  FARPROC addr = m_module->address(m_name);
  if (!addr) {
    std::string err("GetProcAddress failed");
    throw std::runtime_error(err);
  }
  return addr;
}
const std::string& ntutils::detail::ntprocedure_base::name() const
{
  return m_name;
}


В конструкторе ntprocedure_base происходит поиск нужного модуля в статическом списке по его имени. Если такой модуль найден, то вызов module->share() создает разделяемый указатель на основе имеющегося в списке указателя, если же такого модуля еще нет, создается новый объект.

Обратите внимание, что для каждого впервые используемого нами модуля мы вызываем LoadLibrary(), не полагаясь на функцию GetModuleHandle() и уже потом контролируем созданные объекты посредством shared_ptr. Это делает безопасным использование созданной обертки совместно в одном проекте с кодом, использующим непосредственные вызовы LoadLibrary() и FreeLibrary().

Das ist alles Oh ja, der Typ resouce_ptr erscheint im Code . Dies ist nichts weiter als ein RAII-Wrapper für Typen wie HANDLE, HMODULE usw. Für diejenigen, die intereno sind, bringe ich die Umsetzung:

Resource_ptr.h-Datei
#pragma once
#include "common.h"
#include "windows.h"
#include 
#include 
NS_BEGIN_(ntutils)
template
struct resource_close {
  void operator()(typename HTag_::handle_t) const NO_EXCEPT_;
};
struct handle_tag {
  typedef HANDLE resource_t;
};
struct hmodule_tag {
  typedef HMODULE resource_t;
};
template<> struct resource_close {
  void operator()(handle_tag::resource_t a_handle) const NO_EXCEPT_
  {
    bool status = !!::CloseHandle(a_handle);
    assert(status);
  }
};
template<> struct resource_close {
  void operator()(hmodule_tag::resource_t a_handle) const NO_EXCEPT_
  {
    bool status = !!::FreeLibrary(a_handle);
    assert(status);
  }
};
template<
  typename RTag_,
  typename RTag_::resource_t RInvalid_,
  typename RFree_ = resource_close
>
class resource_ptr {
  typedef typename RTag_::resource_t resource_t;
  typedef RFree_ deletor_t;
  resource_ptr(const resource_ptr&) PROHIBITED_;
  void operator=(const resource_ptr&) PROHIBITED_;
public:
  resource_ptr() NO_EXCEPT_
  : m_resource(RInvalid_)
  {
  }
  resource_ptr(resource_t a_resource) NO_EXCEPT_
  : m_resource(a_resource)
  {  
  }
  // Constructor.
  explicit operator bool() const NO_EXCEPT_
  {
    return m_resource && m_resource != RInvalid_;
  }
  // Operator bool().
  operator const resource_t&() const NO_EXCEPT_
  {
    return m_resource;
  }
  // Get the stored handle value.
  void reset(resource_t a_resource = resource_t()) NO_EXCEPT_
  {
    resource_t old = m_resource;
    m_resource = a_resource;
    if (old != resource_t() && old != RInvalid_) {
      m_deletor(old);
    }
  }
  ~resource_ptr() NO_EXCEPT_
  {
    if (m_resource != resource_t() && m_resource != RInvalid_) {
      m_deletor(m_resource);
    }
  }
  // Destructor.
private:
  resource_t m_resource;
  deletor_t m_deletor;
};
typedef resource_ptr handle_ptr;
typedef resource_ptr hmodule_ptr;
NS_END_


Das ist alles sicher. Vielen Dank für Ihre Aufmerksamkeit, ich werde mich freuen, Ihre Kommentare zu hören!

Jetzt auch beliebt: