📜 ⬆️ ⬇️

Easy to use wrapper over LoadLibrary () and GetProcAddress ()

Preamble


The use of dynamically linked libraries (DLLs) is known to imply one of two ways to connect: load-time linking and run-time linking . In the latter case, you need to use the API provided by the operating system to load the required module (library) and search for the address of the required procedure in it. There are many wrappers, but, unfortunately, all I have encountered are very complicated and overloaded with excess code. The proposed solution was originally intended to call functions stored in a DLL from executable modules (EXE), it is characterized by relative ease of implementation, and (more importantly) ease of use in client code.

The solution using the pure Win32 API looks like this (pratic, this is a repetition of a fragment from 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"); } 

This article offers an easy-to-use wrapper over these system calls. Usage example:

 ntprocedure<int(LPWSTR)> 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"); } 

As you can see from the listing, all that needs to be done is to create an ntprocedure object with template parameters that correspond to the type of the function being called, passing in the constructor its name and the library name.
')

Implementation


Before proceeding to the description of the implementation of the wrapper, I’ll give a small header file with trivial declarations that many will find useless, which is easy to get rid of, but are used by me in the code.

File common.h
 #pragma once #include "tchar.h" #include <string> #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<TCHAR>, std::allocator<TCHAR> > tstring; 


We will think about how to ensure that the template class being developed behaves as a function at the calling point and can support an arbitrary number and type of arguments. The first thing that comes to mind is to use a generalized functor. The authors of the implementations of such wrappers known to me do just that. It uses either a partial specialization of the functor pattern depending on the number of arguments, or multiple overloading of the function call operator. The case is usually not complete without the help of macros. Fortunately, in C ++ 11, templates with a variable number of arguments appeared that greatly simplify life:

 R operator () (Args ... args) 

In fact, in our case, you can do much easier, namely, use the cast operator instead of the function call operator. If T is a type of function pointer, and address is a variable that stores its address, you can define the following statement:

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

Below is the complete code for the header file "ntprocedure.h".

Ntprocedure.h file
 #pragma once #include "common.h" #include <memory> #include <string> #include <type_traits> 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<ntmodule> m_module; }; NS_END_ template<typename T> class ntprocedure : public detail::ntprocedure_base { public: typedef typename std::remove_pointer<T>::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<callable_ptr_t>(address()); } return m_function; } // Return stored function to invoke. private: callable_ptr_t m_function; }; NS_END_ 


A couple of points that the attentive reader noticed - the address of the procedure is stored in the m_function variable and is calculated once, and the second point is that a shared pointer to an object of the ntmodule class is stored in the base class. It is not difficult to guess that it stores information about the loaded module. Using shared_ptr allows you to automatically unload a module after destroying all the procedure objects that use it.

Ntmodule.h file
 #pragma once #include "common.h" #include "resource_ptr.h" #include <list> #include <memory> NS_BEGIN_(ntutils) NS_BEGIN_(detail) class ntmodule : public std::enable_shared_from_this<ntmodule> { ntmodule(const ntmodule&) PROHIBITED_; void operator=(const ntmodule&) PROHIBITED_; public: typedef std::list<ntmodule*> 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<ntmodule> 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_ 


Consider the definition of the class ntmodule:

Ntmodule.cpp file
 #include "stdafx.h" #include "ntmodule.h" #include "ntprocedure.h" #include <cassert> #include <exception> 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> 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; } 


As you can see, pointers to all used modules are stored in a static list. This provides caching. The constructor of the ntmodule class places a pointer to its object in the list, and the destructor deletes it. Completely clarify the picture definition class ntprocedure.

Ntprocedure.cpp file
 #include "stdafx.h" #include "ntmodule.h" #include "ntprocedure.h" #include <cassert> #include <exception> 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<ntmodule>(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; } 


In the ntprocedure_base constructor, the module is searched for in the static list by its name. If such a module is found, then calling module-> share () creates a shared pointer based on the pointer in the list, but if there is no such module yet, a new object is created.

Please note that for each module we use for the first time, we call LoadLibrary () , not relying on the GetModuleHandle () function and only then we control the created objects using shared_ptr . This makes it safe to use the created wrapper together in the same project with code that uses direct calls to LoadLibrary () and FreeLibrary () .

That's all. Oh yes, the resouce_ptr type appears in the code. This is nothing more than a RAII wrapper over such types as HANDLE, HMODULE, and so on. For those to whom intereno, here is the implementation:

Resource_ptr.h file
 #pragma once #include "common.h" #include "windows.h" #include <cassert> #include <memory> NS_BEGIN_(ntutils) template<typename HTag_> 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<handle_tag> { void operator()(handle_tag::resource_t a_handle) const NO_EXCEPT_ { bool status = !!::CloseHandle(a_handle); assert(status); } }; template<> struct resource_close<hmodule_tag> { 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<RTag_> > 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_tag, INVALID_HANDLE_VALUE> handle_ptr; typedef resource_ptr<hmodule_tag, NULL> hmodule_ptr; NS_END_ 


This is exactly all. Thank you for your attention, I will be glad to hear your comments!

Source: https://habr.com/ru/post/266809/


All Articles