Skip to content

Commit

Permalink
Merge pull request #43 from ESSS/fb-SIMBR-1735-hookcaller-c++
Browse files Browse the repository at this point in the history
Implement hook loading in C++
  • Loading branch information
nicoddemus authored Jan 18, 2019
2 parents 9ed990c + 4cf3eed commit b0a47ec
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 41 deletions.
3 changes: 3 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ History
0.2.0 (unreleased)
------------------

- Moved load hook function code to ``HookCaller.load_impls_from_library`` function implemented in C++. This
enables using hook functionality in projects which don't use Python as their entry point.

- ``HookSpecs`` now accepts an ``extra_includes`` argument, which can be used to add custom ``#include`` directives
to the generated ``HookCaller.hpp`` file.

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Hookman
.. image:: https://img.shields.io/travis/ESSS/hookman.svg
:target: https://travis-ci.org/ESSS/hookman

.. image:: https://ci.appveyor.com/api/projects/status/github/ESSS/hookman?branch=master
.. image:: https://ci.appveyor.com/api/projects/status/w472d1cm0w37ka3l/branch/master?svg=true
:target: https://ci.appveyor.com/project/ESSS/hookman/?branch=master&svg=true

.. image:: https://codecov.io/gh/ESSS/hookman/branch/master/graph/badge.svg
Expand Down
185 changes: 164 additions & 21 deletions hookman/hookman_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
from hookman.hooks import HookSpecs
from hookman.plugin_config import PluginInfo

INDENTATION = " "
NEW_LINE = "\n"


class Hook(NamedTuple):
"""
Expand Down Expand Up @@ -329,7 +326,17 @@ def _hook_caller_hpp_content(self) -> str:
"#define _H_HOOKMAN_HOOK_CALLER",
"",
"#include <functional>",
"#include <memory>",
"#include <stdexcept>",
"#include <string>",
"#include <vector>",
"#include <iostream>",
"",
"#ifdef _WIN32",
f" #include <windows.h>",
"#else",
f" #include <dlfcn.h>",
"#endif",
"",
]
content_lines += (f'#include <{x}>' for x in self.extra_includes)
Expand All @@ -338,32 +345,34 @@ def _hook_caller_hpp_content(self) -> str:
"namespace hookman {",
"",
"template <typename F_TYPE> std::function<F_TYPE> from_c_pointer(uintptr_t p) {",
f"{INDENTATION}return std::function<F_TYPE>(reinterpret_cast<F_TYPE *>(p));",
f" return std::function<F_TYPE>(reinterpret_cast<F_TYPE *>(p));",
"}",
"",
"",
"class HookCaller {",
"public:",
]

for hook in self.hooks:
list_with_hook_calls += [
f'{INDENTATION}std::vector<std::function<{hook.r_type}({hook.args_type})>> {hook.name}_impls() {{',
f'{INDENTATION*2}return this->_{hook.name}_impls;',
f'{INDENTATION}}}',
f' std::vector<std::function<{hook.r_type}({hook.args_type})>> {hook.name}_impls() {{',
f' return this->_{hook.name}_impls;',
f' }}',
]
list_with_private_members += [
f'{INDENTATION}std::vector<std::function<{hook.r_type}({hook.args_type})>> _{hook.name}_impls;'
f' std::vector<std::function<{hook.r_type}({hook.args_type})>> _{hook.name}_impls;'
]

list_with_set_functions += [
f'{1*INDENTATION}void append_{hook.name}_impl(uintptr_t pointer) {{',
f'{2*INDENTATION}this->_{hook.name}_impls.push_back(from_c_pointer<{hook.r_type}({hook.args_type})>(pointer));',
f'{1*INDENTATION}}}',
f' void append_{hook.name}_impl(uintptr_t pointer) {{',
f' this->_{hook.name}_impls.push_back(from_c_pointer<{hook.r_type}({hook.args_type})>(pointer));',
f' }}',
"",
]
content_lines += list_with_hook_calls
content_lines.append("")
content_lines += list_with_set_functions
content_lines.append("")
content_lines += _generate_load_function(self.hooks)
content_lines.append("private:")
content_lines += list_with_private_members
content_lines.append("};")
Expand All @@ -372,7 +381,7 @@ def _hook_caller_hpp_content(self) -> str:
content_lines.append("#endif // _H_HOOKMAN_HOOK_CALLER")
content_lines.append('')

return NEW_LINE.join(content_lines)
return "\n".join(content_lines)

def _hook_caller_python_content(self) -> str:
"""
Expand All @@ -397,22 +406,23 @@ def _hook_caller_python_content(self) -> str:
for index, (r_type, args_type) in enumerate(sorted(signatures)):
name = f'vector_hook_impl_type_{index}'
vector_type = f"std::vector<std::function<{r_type}({args_type})>>"
content_lines.append(f'{INDENTATION}py::bind_vector<{vector_type}>(m, "{name}");')
content_lines.append(f' py::bind_vector<{vector_type}>(m, "{name}");')
content_lines.append("")

content_lines += [
f'{INDENTATION}py::class_<hookman::HookCaller>(m, "HookCaller")',
f"{INDENTATION * 2}.def(py::init<>())",
f' py::class_<hookman::HookCaller>(m, "HookCaller")',
f" .def(py::init<>())",
f' .def("load_impls_from_library", &hookman::HookCaller::load_impls_from_library)',
]
for hook in self.hooks:
content_lines += [
f'{2*INDENTATION}.def("{hook.name}_impls", &hookman::HookCaller::{hook.name}_impls)',
f'{2*INDENTATION}.def("append_{hook.name}_impl", &hookman::HookCaller::append_{hook.name}_impl)',
f' .def("{hook.name}_impls", &hookman::HookCaller::{hook.name}_impls)',
f' .def("append_{hook.name}_impl", &hookman::HookCaller::append_{hook.name}_impl)',
]
content_lines.append(f'{INDENTATION};')
content_lines.append(f' ;')
content_lines.append('}')
content_lines.append('')
return NEW_LINE.join(content_lines)
return "\n".join(content_lines)

def _generate_cmake_files(self, dst_path: Path):
hook_caller_hpp = Path(dst_path / 'cpp' / 'CMakeLists.txt')
Expand Down Expand Up @@ -484,7 +494,7 @@ def _plugin_source_content(self) -> str:
Create a C header file with the content informed on the hook_specs
"""
file_content = []
plugin_hooks_macro = [f'// HOOK_{hook.macro_name}({hook.args}){{}}{NEW_LINE}' for hook in self.hooks]
plugin_hooks_macro = [f'// HOOK_{hook.macro_name}({hook.args}){{}}\n' for hook in self.hooks]

file_content += dedent(f"""\
#include "hook_specs.h"
Expand Down Expand Up @@ -571,3 +581,136 @@ def _compile_shared_lib_python_script_content(self, shared_lib_name):
shutil.copy2(src=shared_lib, dst=package_dir)
''')
return file_content


def _generate_load_function(hooks):
result = [
"#if defined(_WIN32)",
"",
]
result += _generate_windows_body(hooks)
result += [
"",
"#elif defined(__linux__)",
"",
]
result += _generate_linux_body(hooks)
result += [
"",
"#else",
f' #error "unknown platform"',
"#endif",
"",
]
return result


def _generate_windows_body(hooks):
"""Generate Windows specific functions.
At the moment it implements load_impls_from_library, class destructor, and an utility function
to convert from utf8 to wide-strings so we can use the wide family of windows
functions that accept unicode.
"""
# generate destructor to free the library handles opened by load_from_library()
result = [
"public:",
f" ~HookCaller() {{",
f" for (auto handle : this->handles) {{",
f" FreeLibrary(handle);",
f" }}",
f" }}",
]

# generate load_impls_from_library()
result += [
f" void load_impls_from_library(const std::string utf8_filename) {{",
f' std::wstring w_filename = utf8_to_wstring(utf8_filename);',
f' auto handle = LoadLibraryW(w_filename.c_str());',
f' if (handle == NULL) {{',
f' throw std::runtime_error("Error loading library " + utf8_filename + ": " + std::to_string(GetLastError()));',
f' }}',
f' this->handles.push_back(handle);',
"",
]

for index, hook in enumerate(hooks):
result += [
f' auto p{index} = GetProcAddress(handle, "{hook.function_name}");',
f' if (p{index} != nullptr) {{',
f' this->append_{hook.name}_impl((uintptr_t)(p{index}));',
f' }}',
"",
]
result.append(" }")

result += [
"",
"",
"private:",
f" std::wstring utf8_to_wstring(const std::string &s) {{",
f" int flags = 0;",
f" int required_size = MultiByteToWideChar(CP_UTF8, flags, s.c_str(), -1, nullptr, 0);",
f" auto buffer = std::make_unique<WCHAR[]>(required_size);",
f" int err = MultiByteToWideChar(CP_UTF8, flags, s.c_str(), -1, buffer.get(), required_size);",
f" if (err == 0) {{",
f" // error handling: https://docs.microsoft.com/en-us/windows/desktop/api/stringapiset/nf-stringapiset-multibytetowidechar#return-value",
f" switch (GetLastError()) {{",
f" case ERROR_INSUFFICIENT_BUFFER: throw std::runtime_error(\"utf8_to_wstring: ERROR_INSUFFICIENT_BUFFER\");",
f" case ERROR_INVALID_FLAGS: throw std::runtime_error(\"utf8_to_wstring: ERROR_INVALID_FLAGS\");",
f" case ERROR_INVALID_PARAMETER: throw std::runtime_error(\"utf8_to_wstring: ERROR_INVALID_PARAMETER\");",
f" case ERROR_NO_UNICODE_TRANSLATION: throw std::runtime_error(\"utf8_to_wstring: ERROR_NO_UNICODE_TRANSLATION\");",
f" default: throw std::runtime_error(\"Undefined error: \" + std::to_string(GetLastError()));",
f" }}",
f" }}",
f" return std::wstring(buffer.get(), required_size);",
f" }}",
f"",
f"",
f"private:",
f" std::vector<HMODULE> handles;",
]
return result


def _generate_linux_body(hooks):
"""
Generate linux specific functions.
At the moment it implements load_impls_from_library and the class destructor
to cleanup handles.
"""
# generate destructor to free the library handles opened by load_from_library()
result = [
f"private:",
f" std::vector<void*> handles;",
"",
"public:",
f" ~HookCaller() {{",
f" for (auto handle : this->handles) {{",
f" dlclose(handle);",
f" }}",
f" }}",
]

# generate load_impls_from_library()
result += [
f" void load_impls_from_library(const std::string utf8_filename) {{",
f' auto handle = dlopen(utf8_filename.c_str(), RTLD_LAZY);',
f' if (handle == nullptr) {{',
f' throw std::runtime_error("Error loading library " + utf8_filename + ": dlopen failed");',
f' }}',
f' this->handles.push_back(handle);',
"",
]

for index, hook in enumerate(hooks):
result += [
f' auto p{index} = dlsym(handle, "{hook.function_name}");',
f' if (p{index} != nullptr) {{',
f' this->append_{hook.name}_impl((uintptr_t)(p{index}));',
f' }}',
"",
]
result.append(" }")
return result
21 changes: 2 additions & 19 deletions hookman/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,30 +128,13 @@ def get_plugins_available(self, ignored_plugins: Sequence[str]=()) -> Optional[L
plugins_available = [PluginInfo(plugin_file, self.hooks_available) for plugin_file in plugin_config_files]
return [plugin_info for plugin_info in plugins_available if plugin_info.name not in ignored_plugins]

def get_hook_caller(self, ignored_plugins: Sequence[str]=()):
def get_hook_caller(self, ignored_plugins: Sequence[str] = ()):
"""
Return a HookCaller class that holds all references for the functions implemented
on the plugins.
"""
_hookman = __import__(self.specs.pyd_name)
hook_caller = _hookman.HookCaller()
for plugin in self.get_plugins_available(ignored_plugins):
self._bind_libs_functions_on_hook_caller(plugin.shared_lib_path, hook_caller)
hook_caller.load_impls_from_library(str(plugin.shared_lib_path))
return hook_caller

def _bind_libs_functions_on_hook_caller(self, shared_lib_path: Path, hook_caller):
"""
Load the shared_lib_path from the plugin and bind methods that are implemented on the
hook_caller.
"""
plugin_dll = ctypes.cdll.LoadLibrary(str(shared_lib_path))

hooks_to_bind = {}
for hook_name, full_hook_name in self.hooks_available.items():
if PluginInfo.is_implemented_on_plugin(plugin_dll, full_hook_name):
func_address = PluginInfo.get_function_address(plugin_dll, full_hook_name)
hooks_to_bind[f'append_{hook_name}_impl'] = func_address

for hook in hooks_to_bind:
cpp_func = getattr(hook_caller, hook)
cpp_func(hooks_to_bind[hook])
Loading

0 comments on commit b0a47ec

Please sign in to comment.