mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Added Reclass.NET plugin compatibility layer
This commit is contained in:
@@ -317,4 +317,5 @@ endif()
|
|||||||
add_subdirectory(plugins/ProcessMemory)
|
add_subdirectory(plugins/ProcessMemory)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
add_subdirectory(plugins/WinDbgMemory)
|
add_subdirectory(plugins/WinDbgMemory)
|
||||||
|
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal file
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(RcNetCompatPlugin LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
|
||||||
|
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
set(CMAKE_AUTORCC ON)
|
||||||
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
|
||||||
|
# Plugin sources
|
||||||
|
set(PLUGIN_SOURCES
|
||||||
|
RcNetCompatPlugin.h
|
||||||
|
RcNetCompatPlugin.cpp
|
||||||
|
RcNetCompatProvider.h
|
||||||
|
RcNetCompatProvider.cpp
|
||||||
|
ReClassNET_Plugin.hpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Optional .NET bridge -------------------------------------------------
|
||||||
|
# When the .NET SDK is available, build the C# bridge assembly and enable
|
||||||
|
# CLR hosting support in the C++ plugin.
|
||||||
|
|
||||||
|
find_program(DOTNET_EXE dotnet)
|
||||||
|
if(DOTNET_EXE)
|
||||||
|
# Check that 'dotnet build' actually works for net472
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${DOTNET_EXE} --list-sdks
|
||||||
|
OUTPUT_VARIABLE _dotnet_sdks
|
||||||
|
ERROR_QUIET
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
if(_dotnet_sdks)
|
||||||
|
set(HAS_CLR_BRIDGE ON)
|
||||||
|
message(STATUS "RcNetCompat: .NET SDK found -- building managed bridge")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(HAS_CLR_BRIDGE)
|
||||||
|
list(APPEND PLUGIN_SOURCES
|
||||||
|
ClrHost.h
|
||||||
|
ClrHost.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build the C# bridge assembly
|
||||||
|
set(_bridge_src "${CMAKE_CURRENT_SOURCE_DIR}/bridge")
|
||||||
|
set(_bridge_out "${CMAKE_BINARY_DIR}/Plugins/RcNetBridge.dll")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${_bridge_out}"
|
||||||
|
COMMAND ${DOTNET_EXE} build
|
||||||
|
"${_bridge_src}/RcNetBridge.csproj"
|
||||||
|
-c Release
|
||||||
|
-o "${CMAKE_BINARY_DIR}/Plugins"
|
||||||
|
--nologo -v quiet
|
||||||
|
DEPENDS
|
||||||
|
"${_bridge_src}/RcNetBridge.cs"
|
||||||
|
"${_bridge_src}/RcNetBridge.csproj"
|
||||||
|
COMMENT "Building RcNetBridge.dll (.NET bridge)..."
|
||||||
|
)
|
||||||
|
add_custom_target(RcNetBridge ALL DEPENDS "${_bridge_out}")
|
||||||
|
else()
|
||||||
|
message(STATUS "RcNetCompat: .NET SDK not found -- managed plugin support disabled")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Create shared library (DLL)
|
||||||
|
add_library(RcNetCompatPlugin SHARED ${PLUGIN_SOURCES})
|
||||||
|
|
||||||
|
if(HAS_CLR_BRIDGE)
|
||||||
|
target_compile_definitions(RcNetCompatPlugin PRIVATE HAS_CLR_BRIDGE=1)
|
||||||
|
add_dependencies(RcNetCompatPlugin RcNetBridge)
|
||||||
|
# CLR hosting uses COM (ole32)
|
||||||
|
target_link_libraries(RcNetCompatPlugin PRIVATE ole32)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Link Qt
|
||||||
|
target_link_libraries(RcNetCompatPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
target_include_directories(RcNetCompatPlugin PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||||
|
)
|
||||||
|
|
||||||
|
# Output to Plugins folder
|
||||||
|
set_target_properties(RcNetCompatPlugin PROPERTIES
|
||||||
|
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||||
|
)
|
||||||
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal file
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#include "ClrHost.h"
|
||||||
|
|
||||||
|
#include <cwchar>
|
||||||
|
|
||||||
|
// -- GUIDs ----------------------------------------------------------------
|
||||||
|
|
||||||
|
using FnCLRCreateInstance = HRESULT(STDAPICALLTYPE*)(REFCLSID, REFIID, LPVOID*);
|
||||||
|
|
||||||
|
// {9280188D-0E8E-4867-B30C-7FA83884E8DE}
|
||||||
|
static const GUID sCLSID_CLRMetaHost =
|
||||||
|
{0x9280188d, 0x0e8e, 0x4867, {0xb3, 0x0c, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde}};
|
||||||
|
|
||||||
|
// {D332DB9E-B9B3-4125-8207-A14884F53216}
|
||||||
|
static const GUID sIID_ICLRMetaHost =
|
||||||
|
{0xD332DB9E, 0xB9B3, 0x4125, {0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16}};
|
||||||
|
|
||||||
|
// {BD39D1D2-BA2F-486A-89B0-B4B0CB466891}
|
||||||
|
static const GUID sIID_ICLRRuntimeInfo =
|
||||||
|
{0xBD39D1D2, 0xBA2F, 0x486a, {0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91}};
|
||||||
|
|
||||||
|
// {90F1A06E-7712-4762-86B5-7A5EBA6BDB02}
|
||||||
|
static const GUID sCLSID_CLRRuntimeHost =
|
||||||
|
{0x90F1A06E, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
|
||||||
|
|
||||||
|
// {90F1A06C-7712-4762-86B5-7A5EBA6BDB02}
|
||||||
|
static const GUID sIID_ICLRRuntimeHost =
|
||||||
|
{0x90F1A06C, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
|
||||||
|
|
||||||
|
// -- ClrHost implementation -----------------------------------------------
|
||||||
|
|
||||||
|
ClrHost::ClrHost()
|
||||||
|
{
|
||||||
|
startClr();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClrHost::~ClrHost()
|
||||||
|
{
|
||||||
|
if (m_runtimeHost) m_runtimeHost->Release();
|
||||||
|
if (m_runtimeInfo) m_runtimeInfo->Release();
|
||||||
|
if (m_metaHost) m_metaHost->Release();
|
||||||
|
if (m_mscoree) FreeLibrary(m_mscoree);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ClrHost::startClr()
|
||||||
|
{
|
||||||
|
m_mscoree = LoadLibraryW(L"mscoree.dll");
|
||||||
|
if (!m_mscoree)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto fnCreate = reinterpret_cast<FnCLRCreateInstance>(
|
||||||
|
GetProcAddress(m_mscoree, "CLRCreateInstance"));
|
||||||
|
if (!fnCreate)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
HRESULT hr = fnCreate(sCLSID_CLRMetaHost, sIID_ICLRMetaHost,
|
||||||
|
reinterpret_cast<LPVOID*>(&m_metaHost));
|
||||||
|
if (FAILED(hr) || !m_metaHost)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
hr = m_metaHost->GetRuntime(L"v4.0.30319", sIID_ICLRRuntimeInfo,
|
||||||
|
reinterpret_cast<LPVOID*>(&m_runtimeInfo));
|
||||||
|
if (FAILED(hr) || !m_runtimeInfo)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
hr = m_runtimeInfo->GetInterface(sCLSID_CLRRuntimeHost, sIID_ICLRRuntimeHost,
|
||||||
|
(LPVOID*)&m_runtimeHost);
|
||||||
|
if (FAILED(hr) || !m_runtimeHost)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
hr = m_runtimeHost->Start();
|
||||||
|
if (FAILED(hr))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
m_clrStarted = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ClrHost::loadManagedPlugin(const QString& bridgeDllPath,
|
||||||
|
const QString& pluginPath,
|
||||||
|
RcNetFunctions* outFunctions,
|
||||||
|
QString* errorMsg)
|
||||||
|
{
|
||||||
|
if (!m_runtimeHost || !m_clrStarted) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
".NET Framework 4.x is not available on this machine.\n"
|
||||||
|
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Zero the function table -- the bridge will fill it
|
||||||
|
memset(outFunctions, 0, sizeof(RcNetFunctions));
|
||||||
|
|
||||||
|
// Build the argument string: "<hex_address_of_function_table>|<plugin_path>"
|
||||||
|
// Use %ls (not %s) for wide strings -- MinGW follows POSIX conventions.
|
||||||
|
wchar_t arg[2048];
|
||||||
|
swprintf(arg, sizeof(arg) / sizeof(wchar_t),
|
||||||
|
L"%llx|%ls",
|
||||||
|
reinterpret_cast<unsigned long long>(outFunctions),
|
||||||
|
reinterpret_cast<const wchar_t*>(pluginPath.utf16()));
|
||||||
|
|
||||||
|
DWORD retVal = 0;
|
||||||
|
HRESULT hr = m_runtimeHost->ExecuteInDefaultAppDomain(
|
||||||
|
reinterpret_cast<LPCWSTR>(bridgeDllPath.utf16()),
|
||||||
|
L"RcNetBridge.Bridge",
|
||||||
|
L"Initialize",
|
||||||
|
arg,
|
||||||
|
&retVal
|
||||||
|
);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"Failed to execute .NET bridge (HRESULT 0x%1).\n"
|
||||||
|
"Bridge: %2\n"
|
||||||
|
"Plugin: %3")
|
||||||
|
.arg(static_cast<uint>(hr), 8, 16, QChar('0'))
|
||||||
|
.arg(bridgeDllPath)
|
||||||
|
.arg(pluginPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retVal != 0) {
|
||||||
|
if (errorMsg) {
|
||||||
|
switch (retVal) {
|
||||||
|
case 1:
|
||||||
|
*errorMsg = QStringLiteral("Bridge: invalid argument format.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"No ICoreProcessFunctions implementation found in the .NET plugin.\n"
|
||||||
|
"The DLL may not be a ReClass.NET plugin.");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"Failed to load the .NET plugin assembly.\n"
|
||||||
|
"Check that all its dependencies are available.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
*errorMsg = QStringLiteral("Bridge returned error code %1.").arg(retVal);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the bridge wrote at least the minimum required function pointers
|
||||||
|
if (!outFunctions->ReadRemoteMemory ||
|
||||||
|
!outFunctions->OpenRemoteProcess ||
|
||||||
|
!outFunctions->EnumerateProcesses ||
|
||||||
|
!outFunctions->CloseRemoteProcess) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"The .NET bridge loaded but did not provide the required functions "
|
||||||
|
"(ReadRemoteMemory, OpenRemoteProcess, CloseRemoteProcess, EnumerateProcesses).");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal file
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#pragma once
|
||||||
|
// In-process CLR hosting for loading .NET ReClass.NET plugins.
|
||||||
|
// Dynamically loads mscoree.dll and uses ICLRMetaHost -> ICLRRuntimeInfo ->
|
||||||
|
// ICLRRuntimeHost::ExecuteInDefaultAppDomain to call into the C# bridge.
|
||||||
|
|
||||||
|
#include "ReClassNET_Plugin.hpp"
|
||||||
|
#include <QString>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <objbase.h>
|
||||||
|
|
||||||
|
// -- Minimal COM interface definitions for CLR hosting --------------------
|
||||||
|
// Defined here to avoid depending on Windows SDK metahost.h / mscoree.h
|
||||||
|
// which may not be present in all MinGW distributions.
|
||||||
|
// Only methods we actually call have real signatures; the rest are stubs
|
||||||
|
// that preserve correct vtable offsets.
|
||||||
|
|
||||||
|
#undef INTERFACE
|
||||||
|
#define INTERFACE ICLRMetaHost
|
||||||
|
DECLARE_INTERFACE_(ICLRMetaHost, IUnknown)
|
||||||
|
{
|
||||||
|
// IUnknown
|
||||||
|
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||||
|
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||||
|
STDMETHOD_(ULONG, Release)() PURE;
|
||||||
|
// ICLRMetaHost
|
||||||
|
STDMETHOD(GetRuntime)(LPCWSTR pwzVersion, REFIID riid, LPVOID* ppRuntime) PURE;
|
||||||
|
STDMETHOD(GetVersionFromFile)(LPCWSTR, LPWSTR, DWORD*) PURE;
|
||||||
|
STDMETHOD(EnumerateInstalledRuntimes)(void**) PURE;
|
||||||
|
STDMETHOD(EnumerateLoadedRuntimes)(HANDLE, void**) PURE;
|
||||||
|
STDMETHOD(RequestRuntimeLoadedNotification)(void*) PURE;
|
||||||
|
STDMETHOD(QueryLegacyV2RuntimeBinding)(REFIID, LPVOID*) PURE;
|
||||||
|
STDMETHOD_(void, ExitProcess)(INT32) PURE;
|
||||||
|
};
|
||||||
|
#undef INTERFACE
|
||||||
|
|
||||||
|
#define INTERFACE ICLRRuntimeInfo
|
||||||
|
DECLARE_INTERFACE_(ICLRRuntimeInfo, IUnknown)
|
||||||
|
{
|
||||||
|
// IUnknown
|
||||||
|
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||||
|
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||||
|
STDMETHOD_(ULONG, Release)() PURE;
|
||||||
|
// ICLRRuntimeInfo
|
||||||
|
STDMETHOD(GetVersionString)(LPWSTR, DWORD*) PURE;
|
||||||
|
STDMETHOD(GetRuntimeDirectory)(LPWSTR, DWORD*) PURE;
|
||||||
|
STDMETHOD(IsLoaded)(HANDLE, BOOL*) PURE;
|
||||||
|
STDMETHOD(LoadErrorString)(UINT, LPWSTR, DWORD*, LONG) PURE;
|
||||||
|
STDMETHOD(LoadLibrary)(LPCWSTR, HMODULE*) PURE;
|
||||||
|
STDMETHOD(GetProcAddress)(LPCSTR, LPVOID*) PURE;
|
||||||
|
STDMETHOD(GetInterface)(REFCLSID rclsid, REFIID riid, LPVOID* ppUnk) PURE;
|
||||||
|
};
|
||||||
|
#undef INTERFACE
|
||||||
|
|
||||||
|
#define INTERFACE ICLRRuntimeHost
|
||||||
|
DECLARE_INTERFACE_(ICLRRuntimeHost, IUnknown)
|
||||||
|
{
|
||||||
|
// IUnknown
|
||||||
|
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||||
|
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||||
|
STDMETHOD_(ULONG, Release)() PURE;
|
||||||
|
// ICLRRuntimeHost
|
||||||
|
STDMETHOD(Start)() PURE;
|
||||||
|
STDMETHOD(Stop)() PURE;
|
||||||
|
STDMETHOD(SetHostControl)(void*) PURE;
|
||||||
|
STDMETHOD(GetCLRControl)(void**) PURE;
|
||||||
|
STDMETHOD(UnloadAppDomain)(DWORD, BOOL) PURE;
|
||||||
|
STDMETHOD(ExecuteInAppDomain)(DWORD, void*, void*) PURE;
|
||||||
|
STDMETHOD(GetCurrentAppDomainId)(DWORD*) PURE;
|
||||||
|
STDMETHOD(ExecuteApplication)(LPCWSTR, DWORD, LPCWSTR*, DWORD, LPCWSTR*, int*) PURE;
|
||||||
|
STDMETHOD(ExecuteInDefaultAppDomain)(LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, DWORD*) PURE;
|
||||||
|
};
|
||||||
|
#undef INTERFACE
|
||||||
|
|
||||||
|
// -- CLR Host wrapper -----------------------------------------------------
|
||||||
|
|
||||||
|
class ClrHost
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ClrHost();
|
||||||
|
~ClrHost();
|
||||||
|
|
||||||
|
// True if the .NET Framework CLR (v4.0) is available on this machine.
|
||||||
|
bool isAvailable() const { return m_runtimeHost != nullptr && m_clrStarted; }
|
||||||
|
|
||||||
|
// Load a managed ReClass.NET plugin via the C# bridge.
|
||||||
|
bool loadManagedPlugin(const QString& bridgeDllPath,
|
||||||
|
const QString& pluginPath,
|
||||||
|
RcNetFunctions* outFunctions,
|
||||||
|
QString* errorMsg = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool startClr();
|
||||||
|
|
||||||
|
HMODULE m_mscoree = nullptr;
|
||||||
|
ICLRMetaHost* m_metaHost = nullptr;
|
||||||
|
ICLRRuntimeInfo* m_runtimeInfo = nullptr;
|
||||||
|
ICLRRuntimeHost* m_runtimeHost = nullptr;
|
||||||
|
bool m_clrStarted = false;
|
||||||
|
};
|
||||||
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal file
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
#include "RcNetCompatPlugin.h"
|
||||||
|
#include "RcNetCompatProvider.h"
|
||||||
|
#include "../../src/processpicker.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QStyle>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
// -- Helpers --------------------------------------------------------------
|
||||||
|
|
||||||
|
QIcon RcNetCompatPlugin::Icon() const
|
||||||
|
{
|
||||||
|
return qApp->style()->standardIcon(QStyle::SP_TrashIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --.NET assembly detection ----------------------------------------------
|
||||||
|
|
||||||
|
static bool isDotNetAssembly(const QString& path)
|
||||||
|
{
|
||||||
|
// A .NET assembly has a non-zero CLR header directory entry in the PE
|
||||||
|
// optional header. We check this by loading the PE without running
|
||||||
|
// DllMain and inspecting the IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR.
|
||||||
|
HMODULE hMod = GetModuleHandleW(reinterpret_cast<LPCWSTR>(path.utf16()));
|
||||||
|
if (!hMod)
|
||||||
|
hMod = LoadLibraryExW(reinterpret_cast<LPCWSTR>(path.utf16()),
|
||||||
|
nullptr, DONT_RESOLVE_DLL_REFERENCES);
|
||||||
|
if (!hMod) return false;
|
||||||
|
|
||||||
|
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(hMod);
|
||||||
|
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
|
||||||
|
|
||||||
|
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
|
||||||
|
reinterpret_cast<const char*>(hMod) + dos->e_lfanew);
|
||||||
|
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
|
||||||
|
|
||||||
|
constexpr DWORD kClrIndex = IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR; // 14
|
||||||
|
DWORD rva = 0, dirSize = 0;
|
||||||
|
|
||||||
|
if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
|
||||||
|
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER64*>(&nt->OptionalHeader);
|
||||||
|
if (opt->NumberOfRvaAndSizes > kClrIndex) {
|
||||||
|
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
|
||||||
|
dirSize = opt->DataDirectory[kClrIndex].Size;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER32*>(&nt->OptionalHeader);
|
||||||
|
if (opt->NumberOfRvaAndSizes > kClrIndex) {
|
||||||
|
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
|
||||||
|
dirSize = opt->DataDirectory[kClrIndex].Size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rva != 0 && dirSize != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --Unified loader (dispatches native vs managed) ------------------------
|
||||||
|
|
||||||
|
bool RcNetCompatPlugin::loadPlugin(const QString& path, QString* errorMsg)
|
||||||
|
{
|
||||||
|
if (m_dllPath == path && (m_lib || m_isManaged))
|
||||||
|
return true; // Already loaded
|
||||||
|
|
||||||
|
if (isDotNetAssembly(path)) {
|
||||||
|
#ifdef HAS_CLR_BRIDGE
|
||||||
|
return loadManagedDll(path, errorMsg);
|
||||||
|
#else
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"This is a .NET assembly.\n\n"
|
||||||
|
"This build does not include .NET bridge support.\n"
|
||||||
|
"Rebuild with the .NET SDK installed to enable managed plugin loading.");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return loadNativeDll(path, errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --Native DLL loading ---------------------------------------------------
|
||||||
|
|
||||||
|
bool RcNetCompatPlugin::loadNativeDll(const QString& path, QString* errorMsg)
|
||||||
|
{
|
||||||
|
unloadNativeDll();
|
||||||
|
|
||||||
|
m_lib = std::make_unique<QLibrary>(path);
|
||||||
|
if (!m_lib->load()) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral("Failed to load DLL: %1").arg(m_lib->errorString());
|
||||||
|
m_lib.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve all function pointers
|
||||||
|
m_fns.EnumerateProcesses =
|
||||||
|
reinterpret_cast<FnEnumerateProcesses>(m_lib->resolve("EnumerateProcesses"));
|
||||||
|
m_fns.OpenRemoteProcess =
|
||||||
|
reinterpret_cast<FnOpenRemoteProcess>(m_lib->resolve("OpenRemoteProcess"));
|
||||||
|
m_fns.IsProcessValid =
|
||||||
|
reinterpret_cast<FnIsProcessValid>(m_lib->resolve("IsProcessValid"));
|
||||||
|
m_fns.CloseRemoteProcess =
|
||||||
|
reinterpret_cast<FnCloseRemoteProcess>(m_lib->resolve("CloseRemoteProcess"));
|
||||||
|
m_fns.ReadRemoteMemory =
|
||||||
|
reinterpret_cast<FnReadRemoteMemory>(m_lib->resolve("ReadRemoteMemory"));
|
||||||
|
m_fns.WriteRemoteMemory =
|
||||||
|
reinterpret_cast<FnWriteRemoteMemory>(m_lib->resolve("WriteRemoteMemory"));
|
||||||
|
m_fns.EnumerateRemoteSectionsAndModules =
|
||||||
|
reinterpret_cast<FnEnumerateRemoteSectionsAndModules>(
|
||||||
|
m_lib->resolve("EnumerateRemoteSectionsAndModules"));
|
||||||
|
m_fns.ControlRemoteProcess =
|
||||||
|
reinterpret_cast<FnControlRemoteProcess>(m_lib->resolve("ControlRemoteProcess"));
|
||||||
|
|
||||||
|
// At minimum we need read + open + close
|
||||||
|
if (!m_fns.ReadRemoteMemory || !m_fns.OpenRemoteProcess || !m_fns.CloseRemoteProcess || !m_fns.EnumerateProcesses) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"DLL is missing required exports (ReadRemoteMemory, OpenRemoteProcess, "
|
||||||
|
"CloseRemoteProcess, EnumerateProcesses). Is this a ReClass.NET native plugin?");
|
||||||
|
m_lib->unload();
|
||||||
|
m_lib.reset();
|
||||||
|
m_fns = {};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dllPath = path;
|
||||||
|
m_isManaged = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RcNetCompatPlugin::unloadNativeDll()
|
||||||
|
{
|
||||||
|
if (m_lib) {
|
||||||
|
m_lib->unload();
|
||||||
|
m_lib.reset();
|
||||||
|
}
|
||||||
|
m_fns = {};
|
||||||
|
m_dllPath.clear();
|
||||||
|
m_isManaged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --Managed (.NET) DLL loading via CLR bridge ----------------------------
|
||||||
|
|
||||||
|
#ifdef HAS_CLR_BRIDGE
|
||||||
|
|
||||||
|
bool RcNetCompatPlugin::loadManagedDll(const QString& path, QString* errorMsg)
|
||||||
|
{
|
||||||
|
unloadNativeDll();
|
||||||
|
|
||||||
|
// Lazily create the CLR host (one per plugin lifetime)
|
||||||
|
if (!m_clrHost)
|
||||||
|
m_clrHost = std::make_unique<ClrHost>();
|
||||||
|
|
||||||
|
if (!m_clrHost->isAvailable()) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
".NET Framework 4.x is not available on this machine.\n"
|
||||||
|
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate RcNetBridge.dll next to our own plugin DLL
|
||||||
|
// Use native separators -- the CLR expects Windows-style backslash paths.
|
||||||
|
QString bridgePath = QDir::toNativeSeparators(
|
||||||
|
QCoreApplication::applicationDirPath()
|
||||||
|
+ QStringLiteral("/Plugins/RcNetBridge.dll"));
|
||||||
|
|
||||||
|
if (!QFileInfo::exists(bridgePath)) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"RcNetBridge.dll not found in the Plugins folder.\n"
|
||||||
|
"Expected at: %1").arg(bridgePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_fns = {};
|
||||||
|
QString nativePath = QDir::toNativeSeparators(path);
|
||||||
|
if (!m_clrHost->loadManagedPlugin(bridgePath, nativePath, &m_fns, errorMsg))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
m_dllPath = path;
|
||||||
|
m_isManaged = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // HAS_CLR_BRIDGE
|
||||||
|
|
||||||
|
// --IProviderPlugin ------------------------------------------------------
|
||||||
|
|
||||||
|
bool RcNetCompatPlugin::canHandle(const QString& target) const
|
||||||
|
{
|
||||||
|
// Target format: "dllpath|pid:name"
|
||||||
|
return target.contains('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<rcx::Provider> RcNetCompatPlugin::createProvider(
|
||||||
|
const QString& target, QString* errorMsg)
|
||||||
|
{
|
||||||
|
// Parse "dllpath|pid:name"
|
||||||
|
int sep = target.indexOf('|');
|
||||||
|
if (sep < 0) {
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid target format");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString dllPath = target.left(sep);
|
||||||
|
QString pidPart = target.mid(sep + 1);
|
||||||
|
|
||||||
|
// Load (or reuse) the plugin DLL
|
||||||
|
if (!loadPlugin(dllPath, errorMsg))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// Parse pid:name
|
||||||
|
QStringList parts = pidPart.split(':');
|
||||||
|
bool ok = false;
|
||||||
|
uint32_t pid = parts[0].toUInt(&ok);
|
||||||
|
if (!ok || pid == 0) {
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID: %1").arg(parts[0]);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
QString procName = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid);
|
||||||
|
|
||||||
|
auto provider = std::make_unique<RcNetCompatProvider>(m_fns, pid, procName);
|
||||||
|
if (!provider->isValid()) {
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral(
|
||||||
|
"Failed to open process %1 (PID: %2) via ReClass.NET plugin.\n"
|
||||||
|
"Ensure the process is running and the plugin supports it.")
|
||||||
|
.arg(procName).arg(pid);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t RcNetCompatPlugin::getInitialBaseAddress(const QString& target) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(target);
|
||||||
|
// The provider sets its own base from module enumeration.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RcNetCompatPlugin::selectTarget(QWidget* parent, QString* target)
|
||||||
|
{
|
||||||
|
// Step 1: Pick a ReClass.NET plugin DLL (native or .NET)
|
||||||
|
QString dllPath = QFileDialog::getOpenFileName(
|
||||||
|
parent,
|
||||||
|
QStringLiteral("Select ReClass.NET Plugin"),
|
||||||
|
QString(),
|
||||||
|
QStringLiteral("DLL Files (*.dll)"));
|
||||||
|
|
||||||
|
if (dllPath.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Step 2: Load and validate the DLL
|
||||||
|
QString loadErr;
|
||||||
|
if (!loadPlugin(dllPath, &loadErr)) {
|
||||||
|
QMessageBox::warning(parent,
|
||||||
|
QStringLiteral("ReClass.NET Compat Layer"),
|
||||||
|
loadErr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Enumerate processes and show picker
|
||||||
|
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||||
|
|
||||||
|
QList<ProcessInfo> processes;
|
||||||
|
for (const auto& p : pluginProcesses) {
|
||||||
|
ProcessInfo info;
|
||||||
|
info.pid = p.pid;
|
||||||
|
info.name = p.name;
|
||||||
|
info.path = p.path;
|
||||||
|
info.icon = p.icon;
|
||||||
|
processes.append(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessPicker picker(processes, parent);
|
||||||
|
if (picker.exec() != QDialog::Accepted)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint32_t pid = picker.selectedProcessId();
|
||||||
|
QString name = picker.selectedProcessName();
|
||||||
|
|
||||||
|
// Step 4: Format target as "dllpath|pid:name"
|
||||||
|
*target = QStringLiteral("%1|%2:%3").arg(dllPath).arg(pid).arg(name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --Process enumeration --------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ProcessCollector {
|
||||||
|
QVector<PluginProcessInfo>* dest = nullptr;
|
||||||
|
};
|
||||||
|
thread_local ProcessCollector g_processCollector;
|
||||||
|
|
||||||
|
void RC_CALLCONV processCallback(EnumerateProcessData* data)
|
||||||
|
{
|
||||||
|
if (!data || !g_processCollector.dest) return;
|
||||||
|
|
||||||
|
PluginProcessInfo info;
|
||||||
|
info.pid = static_cast<uint32_t>(data->Id);
|
||||||
|
info.name = QString::fromUtf16(data->Name);
|
||||||
|
info.path = QString::fromUtf16(data->Path);
|
||||||
|
g_processCollector.dest->append(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
QVector<PluginProcessInfo> RcNetCompatPlugin::enumerateProcesses()
|
||||||
|
{
|
||||||
|
QVector<PluginProcessInfo> result;
|
||||||
|
|
||||||
|
if (!m_fns.EnumerateProcesses)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
g_processCollector.dest = &result;
|
||||||
|
m_fns.EnumerateProcesses(processCallback);
|
||||||
|
g_processCollector.dest = nullptr;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --Plugin factory -------------------------------------------------------
|
||||||
|
|
||||||
|
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||||
|
{
|
||||||
|
return new RcNetCompatPlugin();
|
||||||
|
}
|
||||||
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal file
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../../src/iplugin.h"
|
||||||
|
#include "ReClassNET_Plugin.hpp"
|
||||||
|
|
||||||
|
#include <QLibrary>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#ifdef HAS_CLR_BRIDGE
|
||||||
|
#include "ClrHost.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReclassX plugin that loads ReClass.NET plugin DLLs
|
||||||
|
* and exposes them as ReclassX providers.
|
||||||
|
*
|
||||||
|
* Supports both native DLLs (C exports) and, when built with
|
||||||
|
* HAS_CLR_BRIDGE, managed .NET assemblies via in-process CLR hosting.
|
||||||
|
*
|
||||||
|
* Target string format: "dllpath|pid:processname"
|
||||||
|
*/
|
||||||
|
class RcNetCompatPlugin : public IProviderPlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Plugin metadata
|
||||||
|
std::string Name() const override { return "ReClass.NET Compat Layer"; }
|
||||||
|
std::string Version() const override { return "1.0.0"; }
|
||||||
|
std::string Author() const override { return "Reclass"; }
|
||||||
|
std::string Description() const override {
|
||||||
|
return "Loads ReClass.NET native and .NET plugin DLLs as Reclass data sources";
|
||||||
|
}
|
||||||
|
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||||
|
QIcon Icon() const override;
|
||||||
|
|
||||||
|
// IProviderPlugin interface
|
||||||
|
bool canHandle(const QString& target) const override;
|
||||||
|
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
|
||||||
|
uint64_t getInitialBaseAddress(const QString& target) const override;
|
||||||
|
bool selectTarget(QWidget* parent, QString* target) override;
|
||||||
|
|
||||||
|
// Override process enumeration -- we enumerate via the loaded DLL
|
||||||
|
bool providesProcessList() const override { return true; }
|
||||||
|
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool loadPlugin(const QString& path, QString* errorMsg = nullptr);
|
||||||
|
bool loadNativeDll(const QString& path, QString* errorMsg = nullptr);
|
||||||
|
void unloadNativeDll();
|
||||||
|
|
||||||
|
#ifdef HAS_CLR_BRIDGE
|
||||||
|
bool loadManagedDll(const QString& path, QString* errorMsg = nullptr);
|
||||||
|
std::unique_ptr<ClrHost> m_clrHost;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::unique_ptr<QLibrary> m_lib;
|
||||||
|
RcNetFunctions m_fns;
|
||||||
|
QString m_dllPath;
|
||||||
|
bool m_isManaged = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plugin export
|
||||||
|
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||||
125
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
125
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#include "RcNetCompatProvider.h"
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// -- Construction / destruction -------------------------------------------
|
||||||
|
|
||||||
|
RcNetCompatProvider::RcNetCompatProvider(const RcNetFunctions& fns,
|
||||||
|
uint32_t pid,
|
||||||
|
const QString& processName)
|
||||||
|
: m_fns(fns)
|
||||||
|
, m_pid(pid)
|
||||||
|
, m_processName(processName)
|
||||||
|
{
|
||||||
|
if (m_fns.OpenRemoteProcess)
|
||||||
|
m_handle = m_fns.OpenRemoteProcess(static_cast<RC_Size>(pid),
|
||||||
|
ProcessAccess::Full);
|
||||||
|
|
||||||
|
if (m_handle)
|
||||||
|
cacheModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
RcNetCompatProvider::~RcNetCompatProvider()
|
||||||
|
{
|
||||||
|
if (m_handle && m_fns.CloseRemoteProcess)
|
||||||
|
m_fns.CloseRemoteProcess(m_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Required overrides ---------------------------------------------------
|
||||||
|
|
||||||
|
bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const
|
||||||
|
{
|
||||||
|
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint64_t absAddr = m_base + addr;
|
||||||
|
return m_fns.ReadRemoteMemory(m_handle,
|
||||||
|
reinterpret_cast<RC_Pointer>(absAddr),
|
||||||
|
static_cast<RC_Pointer>(buf),
|
||||||
|
0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int RcNetCompatProvider::size() const
|
||||||
|
{
|
||||||
|
if (!m_handle) return 0;
|
||||||
|
if (m_fns.IsProcessValid && !m_fns.IsProcessValid(m_handle)) return 0;
|
||||||
|
return 0x10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Optional overrides ---------------------------------------------------
|
||||||
|
|
||||||
|
bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len)
|
||||||
|
{
|
||||||
|
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint64_t absAddr = m_base + addr;
|
||||||
|
return m_fns.WriteRemoteMemory(m_handle,
|
||||||
|
reinterpret_cast<RC_Pointer>(absAddr),
|
||||||
|
const_cast<RC_Pointer>(static_cast<const void*>(buf)),
|
||||||
|
0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RcNetCompatProvider::getSymbol(uint64_t addr) const
|
||||||
|
{
|
||||||
|
for (const auto& mod : m_modules)
|
||||||
|
{
|
||||||
|
if (addr >= mod.base && addr < mod.base + mod.size)
|
||||||
|
{
|
||||||
|
uint64_t offset = addr - mod.base;
|
||||||
|
return QStringLiteral("%1+0x%2")
|
||||||
|
.arg(mod.name)
|
||||||
|
.arg(offset, 0, 16, QChar('0'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Module enumeration ---------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Thread-local collector for the module enumeration callback.
|
||||||
|
// ReClass.NET callbacks are synchronous, so this is safe.
|
||||||
|
struct ModuleCollector {
|
||||||
|
QVector<RcNetCompatProvider::ModuleInfo>* dest = nullptr;
|
||||||
|
};
|
||||||
|
thread_local ModuleCollector g_moduleCollector;
|
||||||
|
|
||||||
|
void RC_CALLCONV moduleCallback(EnumerateRemoteModuleData* data)
|
||||||
|
{
|
||||||
|
if (!data || !g_moduleCollector.dest) return;
|
||||||
|
|
||||||
|
QString path = QString::fromUtf16(data->Path);
|
||||||
|
QFileInfo fi(path);
|
||||||
|
|
||||||
|
RcNetCompatProvider::ModuleInfo info;
|
||||||
|
info.name = fi.fileName();
|
||||||
|
info.base = reinterpret_cast<uint64_t>(data->BaseAddress);
|
||||||
|
info.size = static_cast<uint64_t>(data->Size);
|
||||||
|
g_moduleCollector.dest->append(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We still need a section callback even though we don't use it.
|
||||||
|
void RC_CALLCONV sectionCallback(EnumerateRemoteSectionData*)
|
||||||
|
{
|
||||||
|
// Intentionally empty -- we only need module data.
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
void RcNetCompatProvider::cacheModules()
|
||||||
|
{
|
||||||
|
if (!m_fns.EnumerateRemoteSectionsAndModules || !m_handle)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_modules.clear();
|
||||||
|
g_moduleCollector.dest = &m_modules;
|
||||||
|
m_fns.EnumerateRemoteSectionsAndModules(m_handle, sectionCallback, moduleCallback);
|
||||||
|
g_moduleCollector.dest = nullptr;
|
||||||
|
|
||||||
|
// Set base to first module if we got any
|
||||||
|
if (!m_modules.isEmpty() && m_base == 0)
|
||||||
|
m_base = m_modules.first().base;
|
||||||
|
}
|
||||||
48
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
48
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../../src/providers/provider.h"
|
||||||
|
#include "ReClassNET_Plugin.hpp"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider that bridges ReClass.NET native plugin DLL calls
|
||||||
|
* to the ReclassX Provider interface.
|
||||||
|
*/
|
||||||
|
class RcNetCompatProvider : public rcx::Provider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RcNetCompatProvider(const RcNetFunctions& fns, uint32_t pid,
|
||||||
|
const QString& processName);
|
||||||
|
~RcNetCompatProvider() override;
|
||||||
|
|
||||||
|
// Required overrides
|
||||||
|
bool read(uint64_t addr, void* buf, int len) const override;
|
||||||
|
int size() const override;
|
||||||
|
|
||||||
|
// Optional overrides
|
||||||
|
bool write(uint64_t addr, const void* buf, int len) override;
|
||||||
|
bool isWritable() const override { return m_fns.WriteRemoteMemory != nullptr; }
|
||||||
|
QString name() const override { return m_processName; }
|
||||||
|
QString kind() const override { return QStringLiteral("RcNet"); }
|
||||||
|
bool isLive() const override { return true; }
|
||||||
|
uint64_t base() const override { return m_base; }
|
||||||
|
void setBase(uint64_t b) override { m_base = b; }
|
||||||
|
QString getSymbol(uint64_t addr) const override;
|
||||||
|
|
||||||
|
struct ModuleInfo {
|
||||||
|
QString name;
|
||||||
|
uint64_t base;
|
||||||
|
uint64_t size;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
void cacheModules();
|
||||||
|
|
||||||
|
RcNetFunctions m_fns;
|
||||||
|
RC_Pointer m_handle = nullptr;
|
||||||
|
uint32_t m_pid;
|
||||||
|
QString m_processName;
|
||||||
|
uint64_t m_base = 0;
|
||||||
|
QVector<ModuleInfo> m_modules;
|
||||||
|
};
|
||||||
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
#pragma once
|
||||||
|
// Subset of ReClass.NET native plugin types needed for the compatibility layer.
|
||||||
|
// Based on the ReClass.NET NativeCore plugin interface.
|
||||||
|
// Only types required by the 8 supported exports are included (no debug types).
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define RC_CALLCONV __stdcall
|
||||||
|
#else
|
||||||
|
#define RC_CALLCONV
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// -- Basic types ----------------------------------------------------------
|
||||||
|
|
||||||
|
using RC_Pointer = void*;
|
||||||
|
using RC_Size = uint64_t;
|
||||||
|
using RC_UnicodeChar = char16_t;
|
||||||
|
|
||||||
|
// -- Enums ----------------------------------------------------------------
|
||||||
|
|
||||||
|
enum class ProcessAccess
|
||||||
|
{
|
||||||
|
Read = 0,
|
||||||
|
Write = 1,
|
||||||
|
Full = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SectionProtection
|
||||||
|
{
|
||||||
|
NoAccess = 0,
|
||||||
|
Read = 1,
|
||||||
|
Write = 2,
|
||||||
|
Execute = 4,
|
||||||
|
Guard = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SectionType
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Private = 1,
|
||||||
|
Mapped = 2,
|
||||||
|
Image = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SectionCategory
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
CODE = 1,
|
||||||
|
DATA = 2,
|
||||||
|
HEAP = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ControlRemoteProcessAction
|
||||||
|
{
|
||||||
|
Suspend = 0,
|
||||||
|
Resume = 1,
|
||||||
|
Terminate = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- Callback data structures ---------------------------------------------
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
|
||||||
|
struct EnumerateProcessData
|
||||||
|
{
|
||||||
|
RC_Size Id;
|
||||||
|
RC_UnicodeChar Name[260];
|
||||||
|
RC_UnicodeChar Path[260];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnumerateRemoteSectionData
|
||||||
|
{
|
||||||
|
RC_Pointer BaseAddress;
|
||||||
|
RC_Size Size;
|
||||||
|
SectionType Type;
|
||||||
|
SectionCategory Category;
|
||||||
|
SectionProtection Protection;
|
||||||
|
RC_UnicodeChar Name[16];
|
||||||
|
RC_UnicodeChar ModulePath[260];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnumerateRemoteModuleData
|
||||||
|
{
|
||||||
|
RC_Pointer BaseAddress;
|
||||||
|
RC_Size Size;
|
||||||
|
RC_UnicodeChar Path[260];
|
||||||
|
};
|
||||||
|
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
// -- Callback typedefs ----------------------------------------------------
|
||||||
|
|
||||||
|
using EnumerateProcessCallback = void(RC_CALLCONV*)(EnumerateProcessData* data);
|
||||||
|
using EnumerateRemoteSectionsCallback = void(RC_CALLCONV*)(EnumerateRemoteSectionData* data);
|
||||||
|
using EnumerateRemoteModulesCallback = void(RC_CALLCONV*)(EnumerateRemoteModuleData* data);
|
||||||
|
|
||||||
|
// -- Function pointer typedefs for resolved exports -----------------------
|
||||||
|
|
||||||
|
using FnEnumerateProcesses = void(RC_CALLCONV*)(EnumerateProcessCallback callback);
|
||||||
|
|
||||||
|
using FnOpenRemoteProcess = RC_Pointer(RC_CALLCONV*)(RC_Size id, ProcessAccess desiredAccess);
|
||||||
|
|
||||||
|
using FnIsProcessValid = bool(RC_CALLCONV*)(RC_Pointer handle);
|
||||||
|
|
||||||
|
using FnCloseRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle);
|
||||||
|
|
||||||
|
using FnReadRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
|
||||||
|
RC_Pointer address,
|
||||||
|
RC_Pointer buffer,
|
||||||
|
int offset,
|
||||||
|
int size);
|
||||||
|
|
||||||
|
using FnWriteRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
|
||||||
|
RC_Pointer address,
|
||||||
|
RC_Pointer buffer,
|
||||||
|
int offset,
|
||||||
|
int size);
|
||||||
|
|
||||||
|
using FnEnumerateRemoteSectionsAndModules =
|
||||||
|
void(RC_CALLCONV*)(RC_Pointer handle,
|
||||||
|
EnumerateRemoteSectionsCallback sectionCallback,
|
||||||
|
EnumerateRemoteModulesCallback moduleCallback);
|
||||||
|
|
||||||
|
using FnControlRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle,
|
||||||
|
ControlRemoteProcessAction action);
|
||||||
|
|
||||||
|
// -- Resolved function table ----------------------------------------------
|
||||||
|
|
||||||
|
struct RcNetFunctions
|
||||||
|
{
|
||||||
|
FnEnumerateProcesses EnumerateProcesses = nullptr;
|
||||||
|
FnOpenRemoteProcess OpenRemoteProcess = nullptr;
|
||||||
|
FnIsProcessValid IsProcessValid = nullptr;
|
||||||
|
FnCloseRemoteProcess CloseRemoteProcess = nullptr;
|
||||||
|
FnReadRemoteMemory ReadRemoteMemory = nullptr;
|
||||||
|
FnWriteRemoteMemory WriteRemoteMemory = nullptr;
|
||||||
|
FnEnumerateRemoteSectionsAndModules EnumerateRemoteSectionsAndModules = nullptr;
|
||||||
|
FnControlRemoteProcess ControlRemoteProcess = nullptr;
|
||||||
|
};
|
||||||
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal file
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
// RcNetBridge -- in-process C# bridge for loading .NET ReClass.NET plugins.
|
||||||
|
//
|
||||||
|
// Called from C++ via ICLRRuntimeHost::ExecuteInDefaultAppDomain().
|
||||||
|
// The single entry point is Bridge.Initialize(string arg) where arg is:
|
||||||
|
// "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
|
||||||
|
//
|
||||||
|
// The bridge:
|
||||||
|
// 1. Registers an AssemblyResolve handler that provides THIS assembly
|
||||||
|
// when a plugin asks for "ReClassNET", so the stub types below satisfy
|
||||||
|
// the plugin's type references.
|
||||||
|
// 2. Loads the plugin assembly and finds an ICoreProcessFunctions
|
||||||
|
// implementation.
|
||||||
|
// 3. Creates [UnmanagedFunctionPointer] delegates wrapping each method.
|
||||||
|
// 4. Writes the native-callable function pointers into the RcNetFunctions
|
||||||
|
// struct at the address provided by C++.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// ReClass.NET stub types
|
||||||
|
// These mirror the subset of types from the ReClass.NET assembly that
|
||||||
|
// memory-reading plugins reference. When the CLR resolves "ReClassNET"
|
||||||
|
// via our AssemblyResolve handler, it gets THIS assembly, and these types
|
||||||
|
// satisfy the plugin's type references.
|
||||||
|
//
|
||||||
|
// Types are placed in the exact namespaces used by the real ReClass.NET
|
||||||
|
// assembly so that plugins compiled against it resolve correctly.
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ReClassNET.Memory -- section enums (referenced by EnumerateRemoteSectionData)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
namespace ReClassNET.Memory
|
||||||
|
{
|
||||||
|
public enum SectionProtection
|
||||||
|
{
|
||||||
|
NoAccess = 0,
|
||||||
|
Read = 1,
|
||||||
|
Write = 2,
|
||||||
|
Execute = 4,
|
||||||
|
Guard = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SectionType
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Private = 1,
|
||||||
|
Mapped = 2,
|
||||||
|
Image = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SectionCategory
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
CODE = 1,
|
||||||
|
DATA = 2,
|
||||||
|
HEAP = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ReClassNET.Debugger -- debugger types (used by ICoreProcessFunctions)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
namespace ReClassNET.Debugger
|
||||||
|
{
|
||||||
|
public enum DebugContinueStatus
|
||||||
|
{
|
||||||
|
Handled = 0,
|
||||||
|
NotHandled = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HardwareBreakpointRegister
|
||||||
|
{
|
||||||
|
InvalidRegister = 0,
|
||||||
|
Dr0 = 1,
|
||||||
|
Dr1 = 2,
|
||||||
|
Dr2 = 3,
|
||||||
|
Dr3 = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HardwareBreakpointTrigger
|
||||||
|
{
|
||||||
|
Execute = 0,
|
||||||
|
Access = 1,
|
||||||
|
Write = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HardwareBreakpointSize
|
||||||
|
{
|
||||||
|
Size1 = 1,
|
||||||
|
Size2 = 2,
|
||||||
|
Size4 = 4,
|
||||||
|
Size8 = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ExceptionDebugInfo
|
||||||
|
{
|
||||||
|
public IntPtr ExceptionCode;
|
||||||
|
public IntPtr ExceptionFlags;
|
||||||
|
public IntPtr ExceptionAddress;
|
||||||
|
public HardwareBreakpointRegister CausedBy;
|
||||||
|
public RegisterInfo Registers;
|
||||||
|
|
||||||
|
public struct RegisterInfo
|
||||||
|
{
|
||||||
|
public IntPtr Rax, Rbx, Rcx, Rdx;
|
||||||
|
public IntPtr Rdi, Rsi, Rsp, Rbp, Rip;
|
||||||
|
public IntPtr R8, R9, R10, R11, R12, R13, R14, R15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DebugEvent
|
||||||
|
{
|
||||||
|
public DebugContinueStatus ContinueStatus;
|
||||||
|
public IntPtr ProcessId;
|
||||||
|
public IntPtr ThreadId;
|
||||||
|
public ExceptionDebugInfo ExceptionInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ReClassNET.Core -- interface, enums, delegates, and data structs
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
namespace ReClassNET.Core
|
||||||
|
{
|
||||||
|
public enum ProcessAccess
|
||||||
|
{
|
||||||
|
Read = 0,
|
||||||
|
Write = 1,
|
||||||
|
Full = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ControlRemoteProcessAction
|
||||||
|
{
|
||||||
|
Suspend = 0,
|
||||||
|
Resume = 1,
|
||||||
|
Terminate = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EnumerateProcessData
|
||||||
|
{
|
||||||
|
public IntPtr Id;
|
||||||
|
public string Name;
|
||||||
|
public string Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EnumerateRemoteSectionData
|
||||||
|
{
|
||||||
|
public IntPtr BaseAddress;
|
||||||
|
public IntPtr Size;
|
||||||
|
public ReClassNET.Memory.SectionType Type;
|
||||||
|
public ReClassNET.Memory.SectionCategory Category;
|
||||||
|
public ReClassNET.Memory.SectionProtection Protection;
|
||||||
|
public string Name;
|
||||||
|
public string ModulePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EnumerateRemoteModuleData
|
||||||
|
{
|
||||||
|
public IntPtr BaseAddress;
|
||||||
|
public IntPtr Size;
|
||||||
|
public string Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void EnumerateProcessCallback(ref EnumerateProcessData data);
|
||||||
|
public delegate void EnumerateRemoteSectionCallback(ref EnumerateRemoteSectionData data);
|
||||||
|
public delegate void EnumerateRemoteModuleCallback(ref EnumerateRemoteModuleData data);
|
||||||
|
|
||||||
|
public interface ICoreProcessFunctions
|
||||||
|
{
|
||||||
|
void EnumerateProcesses(EnumerateProcessCallback callbackProcess);
|
||||||
|
IntPtr OpenRemoteProcess(IntPtr pid, ProcessAccess desiredAccess);
|
||||||
|
bool IsProcessValid(IntPtr process);
|
||||||
|
void CloseRemoteProcess(IntPtr process);
|
||||||
|
bool ReadRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
|
||||||
|
bool WriteRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
|
||||||
|
void EnumerateRemoteSectionsAndModules(
|
||||||
|
IntPtr process,
|
||||||
|
EnumerateRemoteSectionCallback callbackSection,
|
||||||
|
EnumerateRemoteModuleCallback callbackModule);
|
||||||
|
void ControlRemoteProcess(IntPtr process, ControlRemoteProcessAction action);
|
||||||
|
|
||||||
|
// Debugger methods -- stubs required for interface compatibility
|
||||||
|
bool AttachDebuggerToProcess(IntPtr id);
|
||||||
|
void DetachDebuggerFromProcess(IntPtr id);
|
||||||
|
bool AwaitDebugEvent(ref ReClassNET.Debugger.DebugEvent evt, int timeoutInMilliseconds);
|
||||||
|
void HandleDebugEvent(ref ReClassNET.Debugger.DebugEvent evt);
|
||||||
|
bool SetHardwareBreakpoint(IntPtr id, IntPtr address,
|
||||||
|
ReClassNET.Debugger.HardwareBreakpointRegister register,
|
||||||
|
ReClassNET.Debugger.HardwareBreakpointTrigger trigger,
|
||||||
|
ReClassNET.Debugger.HardwareBreakpointSize size,
|
||||||
|
bool set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ReClassNET.Memory -- RemoteProcess stub
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
namespace ReClassNET.Memory
|
||||||
|
{
|
||||||
|
public class RemoteProcess { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ReClassNET.Logger -- ILogger stub
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
namespace ReClassNET.Logger
|
||||||
|
{
|
||||||
|
public interface ILogger { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Stub types for IPluginHost properties
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
namespace ReClassNET.Forms
|
||||||
|
{
|
||||||
|
public class MainForm { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ReClassNET
|
||||||
|
{
|
||||||
|
public class Settings { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ReClassNET.Plugins
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
namespace ReClassNET.Plugins
|
||||||
|
{
|
||||||
|
public abstract class Plugin : IDisposable
|
||||||
|
{
|
||||||
|
public virtual bool Initialize(IPluginHost host) { return true; }
|
||||||
|
public virtual void Terminate() { }
|
||||||
|
public virtual void Dispose() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPluginHost
|
||||||
|
{
|
||||||
|
ReClassNET.Forms.MainForm MainWindow { get; }
|
||||||
|
System.Resources.ResourceManager Resources { get; }
|
||||||
|
ReClassNET.Memory.RemoteProcess Process { get; }
|
||||||
|
ReClassNET.Logger.ILogger Logger { get; }
|
||||||
|
ReClassNET.Settings Settings { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Bridge
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
namespace RcNetBridge
|
||||||
|
{
|
||||||
|
internal class StubPluginHost : ReClassNET.Plugins.IPluginHost
|
||||||
|
{
|
||||||
|
public ReClassNET.Forms.MainForm MainWindow => null;
|
||||||
|
public System.Resources.ResourceManager Resources => null;
|
||||||
|
public ReClassNET.Memory.RemoteProcess Process => null;
|
||||||
|
public ReClassNET.Logger.ILogger Logger => null;
|
||||||
|
public ReClassNET.Settings Settings => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Bridge
|
||||||
|
{
|
||||||
|
// -- Persistent state (static so it survives after Initialize returns) --
|
||||||
|
|
||||||
|
private static ReClassNET.Core.ICoreProcessFunctions s_functions;
|
||||||
|
private static readonly List<Delegate> s_pinned = new List<Delegate>();
|
||||||
|
|
||||||
|
// -- Entry point called from C++ --------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by ICLRRuntimeHost::ExecuteInDefaultAppDomain.
|
||||||
|
/// arg = "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
|
||||||
|
/// Returns 0 on success, non-zero error code on failure.
|
||||||
|
/// </summary>
|
||||||
|
public static int Initialize(string arg)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int sep = arg.IndexOf('|');
|
||||||
|
if (sep < 0) return 1; // bad arg
|
||||||
|
|
||||||
|
long ptrValue = long.Parse(arg.Substring(0, sep), NumberStyles.HexNumber);
|
||||||
|
IntPtr funcTablePtr = new IntPtr(ptrValue);
|
||||||
|
string pluginPath = arg.Substring(sep + 1);
|
||||||
|
|
||||||
|
// Set up assembly resolution
|
||||||
|
string pluginDir = Path.GetDirectoryName(pluginPath) ?? ".";
|
||||||
|
string parentDir = Path.GetDirectoryName(pluginDir);
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
|
||||||
|
{
|
||||||
|
string asmName = new AssemblyName(resolveArgs.Name).Name;
|
||||||
|
|
||||||
|
// Provide our own assembly as the "ReClass.NET" stub
|
||||||
|
if (string.Equals(asmName, "ReClass.NET", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return typeof(Bridge).Assembly;
|
||||||
|
|
||||||
|
// Search plugin directory and parent for other dependencies
|
||||||
|
string dllName = asmName + ".dll";
|
||||||
|
foreach (string dir in new[] { pluginDir, parentDir })
|
||||||
|
{
|
||||||
|
if (dir == null) continue;
|
||||||
|
string path = Path.Combine(dir, dllName);
|
||||||
|
if (File.Exists(path))
|
||||||
|
return Assembly.LoadFrom(path);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load plugin and find ICoreProcessFunctions
|
||||||
|
if (!LoadPlugin(pluginPath))
|
||||||
|
return 2; // no implementation found
|
||||||
|
|
||||||
|
// Write function pointers
|
||||||
|
WriteFunctionPointers(funcTablePtr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is ReflectionTypeLoadException || ex is FileNotFoundException)
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Plugin loading ---------------------------------------------------
|
||||||
|
|
||||||
|
private static bool LoadPlugin(string pluginPath)
|
||||||
|
{
|
||||||
|
Assembly asm = Assembly.LoadFrom(pluginPath);
|
||||||
|
|
||||||
|
// Find a concrete type that implements ICoreProcessFunctions.
|
||||||
|
// ReClass.NET plugins typically extend Plugin and directly
|
||||||
|
// implement ICoreProcessFunctions on the same class.
|
||||||
|
foreach (Type type in asm.GetExportedTypes())
|
||||||
|
{
|
||||||
|
if (type.IsAbstract || type.IsInterface) continue;
|
||||||
|
|
||||||
|
Type iface = type.GetInterfaces().FirstOrDefault(i =>
|
||||||
|
i.FullName == "ReClassNET.Core.ICoreProcessFunctions");
|
||||||
|
if (iface == null) continue;
|
||||||
|
|
||||||
|
object instance = Activator.CreateInstance(type);
|
||||||
|
|
||||||
|
// Try calling Initialize() but don't fail if it throws --
|
||||||
|
// plugins use it for UI integration with the host app,
|
||||||
|
// which we can't fully provide. The process functions
|
||||||
|
// (ReadRemoteMemory, etc.) work without it.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MethodInfo init = type.GetMethod("Initialize",
|
||||||
|
BindingFlags.Public | BindingFlags.Instance,
|
||||||
|
null, new[] { typeof(ReClassNET.Plugins.IPluginHost) }, null);
|
||||||
|
if (init != null)
|
||||||
|
init.Invoke(instance, new object[] { new StubPluginHost() });
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
s_functions = (ReClassNET.Core.ICoreProcessFunctions)instance;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Native-callable delegate types -----------------------------------
|
||||||
|
// These match the C++ RcNetFunctions struct field order exactly.
|
||||||
|
// On x64 Windows all calling conventions collapse to the Microsoft
|
||||||
|
// x64 ABI, so StdCall is used for documentation / x86 correctness.
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate void DelEnumProcesses(IntPtr callback);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate IntPtr DelOpenRemoteProcess(ulong id, int access);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
[return: MarshalAs(UnmanagedType.I1)]
|
||||||
|
delegate bool DelIsProcessValid(IntPtr handle);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate void DelCloseRemoteProcess(IntPtr handle);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
[return: MarshalAs(UnmanagedType.I1)]
|
||||||
|
delegate bool DelReadRemoteMemory(IntPtr handle, IntPtr address,
|
||||||
|
IntPtr buffer, int offset, int size);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
[return: MarshalAs(UnmanagedType.I1)]
|
||||||
|
delegate bool DelWriteRemoteMemory(IntPtr handle, IntPtr address,
|
||||||
|
IntPtr buffer, int offset, int size);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate void DelEnumSectionsAndModules(IntPtr handle,
|
||||||
|
IntPtr sectionCallback, IntPtr moduleCallback);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate void DelControlRemoteProcess(IntPtr handle, int action);
|
||||||
|
|
||||||
|
// Callback delegate types -- these point into C++ and are called by us.
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate void NativeProcessCallback(IntPtr data);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate void NativeSectionCallback(IntPtr data);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
delegate void NativeModuleCallback(IntPtr data);
|
||||||
|
|
||||||
|
// -- Write function pointers to the C++ struct ------------------------
|
||||||
|
|
||||||
|
private static void WriteFunctionPointers(IntPtr funcTable)
|
||||||
|
{
|
||||||
|
// RcNetFunctions layout: 8 consecutive function pointers.
|
||||||
|
int i = 0;
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelEnumProcesses>(EnumProcessesImpl));
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelOpenRemoteProcess>(OpenProcessImpl));
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelIsProcessValid>(IsProcessValidImpl));
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelCloseRemoteProcess>(CloseProcessImpl));
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelReadRemoteMemory>(ReadMemoryImpl));
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelWriteRemoteMemory>(WriteMemoryImpl));
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelEnumSectionsAndModules>(EnumSectionsModulesImpl));
|
||||||
|
WriteSlot(funcTable, i++, Pin<DelControlRemoteProcess>(ControlProcessImpl));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IntPtr Pin<T>(T del) where T : class
|
||||||
|
{
|
||||||
|
Delegate d = del as Delegate;
|
||||||
|
s_pinned.Add(d); // prevent GC
|
||||||
|
return Marshal.GetFunctionPointerForDelegate(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteSlot(IntPtr table, int index, IntPtr value)
|
||||||
|
{
|
||||||
|
Marshal.WriteIntPtr(table, index * IntPtr.Size, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Implementation methods -------------------------------------------
|
||||||
|
|
||||||
|
// -- EnumerateProcesses --
|
||||||
|
// C++ passes a native callback; we call the plugin, convert each
|
||||||
|
// managed EnumerateProcessData to the packed native layout, and
|
||||||
|
// forward to the native callback.
|
||||||
|
|
||||||
|
private static void EnumProcessesImpl(IntPtr nativeCallbackPtr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (s_functions == null || nativeCallbackPtr == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
NativeProcessCallback nativeCb =
|
||||||
|
Marshal.GetDelegateForFunctionPointer<NativeProcessCallback>(nativeCallbackPtr);
|
||||||
|
|
||||||
|
// Native layout (pack=1): uint64 Id + char16[260] Name + char16[260] Path
|
||||||
|
const int kStructSize = 8 + 520 + 520; // 1048 bytes
|
||||||
|
|
||||||
|
s_functions.EnumerateProcesses(
|
||||||
|
(ref ReClassNET.Core.EnumerateProcessData data) =>
|
||||||
|
{
|
||||||
|
IntPtr mem = Marshal.AllocHGlobal(kStructSize);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Zero-fill
|
||||||
|
byte[] zeros = new byte[kStructSize];
|
||||||
|
Marshal.Copy(zeros, 0, mem, kStructSize);
|
||||||
|
|
||||||
|
// Id (8 bytes at offset 0)
|
||||||
|
Marshal.WriteInt64(mem, 0, data.Id.ToInt64());
|
||||||
|
|
||||||
|
// Name (char16[260] at offset 8)
|
||||||
|
if (data.Name != null)
|
||||||
|
{
|
||||||
|
char[] chars = data.Name.ToCharArray();
|
||||||
|
int count = Math.Min(chars.Length, 259);
|
||||||
|
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 8), count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path (char16[260] at offset 528)
|
||||||
|
if (data.Path != null)
|
||||||
|
{
|
||||||
|
char[] chars = data.Path.ToCharArray();
|
||||||
|
int count = Math.Min(chars.Length, 259);
|
||||||
|
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 528), count);
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeCb(mem);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(mem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { /* swallow -- don't crash the host process */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- OpenRemoteProcess --
|
||||||
|
private static IntPtr OpenProcessImpl(ulong id, int access)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (s_functions == null) return IntPtr.Zero;
|
||||||
|
return s_functions.OpenRemoteProcess(
|
||||||
|
new IntPtr((long)id),
|
||||||
|
(ReClassNET.Core.ProcessAccess)access);
|
||||||
|
}
|
||||||
|
catch { return IntPtr.Zero; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- IsProcessValid --
|
||||||
|
private static bool IsProcessValidImpl(IntPtr handle)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (s_functions == null) return false;
|
||||||
|
return s_functions.IsProcessValid(handle);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- CloseRemoteProcess --
|
||||||
|
private static void CloseProcessImpl(IntPtr handle)
|
||||||
|
{
|
||||||
|
try { s_functions?.CloseRemoteProcess(handle); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- ReadRemoteMemory --
|
||||||
|
// C++ provides a native buffer pointer. We read into a managed array
|
||||||
|
// via the plugin's interface, then copy to the native buffer.
|
||||||
|
private static bool ReadMemoryImpl(IntPtr handle, IntPtr address,
|
||||||
|
IntPtr buffer, int offset, int size)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (s_functions == null || size <= 0) return false;
|
||||||
|
|
||||||
|
byte[] managed = new byte[size];
|
||||||
|
bool ok = s_functions.ReadRemoteMemory(
|
||||||
|
handle, address, ref managed, 0, size);
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
Marshal.Copy(managed, 0, new IntPtr(buffer.ToInt64() + offset), size);
|
||||||
|
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- WriteRemoteMemory --
|
||||||
|
private static bool WriteMemoryImpl(IntPtr handle, IntPtr address,
|
||||||
|
IntPtr buffer, int offset, int size)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (s_functions == null || size <= 0) return false;
|
||||||
|
|
||||||
|
byte[] managed = new byte[size];
|
||||||
|
Marshal.Copy(new IntPtr(buffer.ToInt64() + offset), managed, 0, size);
|
||||||
|
|
||||||
|
return s_functions.WriteRemoteMemory(
|
||||||
|
handle, address, ref managed, 0, size);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- EnumerateRemoteSectionsAndModules --
|
||||||
|
private static void EnumSectionsModulesImpl(IntPtr handle,
|
||||||
|
IntPtr sectionCallbackPtr, IntPtr moduleCallbackPtr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (s_functions == null) return;
|
||||||
|
|
||||||
|
// Section callback -- forward to native
|
||||||
|
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
|
||||||
|
// SectionType(4) + SectionCategory(4) + SectionProtection(4) +
|
||||||
|
// char16 Name[16](32) + char16 ModulePath[260](520) = 580 bytes
|
||||||
|
NativeSectionCallback nativeSectionCb = (sectionCallbackPtr != IntPtr.Zero)
|
||||||
|
? Marshal.GetDelegateForFunctionPointer<NativeSectionCallback>(sectionCallbackPtr)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Module callback -- forward to native
|
||||||
|
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
|
||||||
|
// char16 Path[260](520) = 536 bytes
|
||||||
|
NativeModuleCallback nativeModuleCb = (moduleCallbackPtr != IntPtr.Zero)
|
||||||
|
? Marshal.GetDelegateForFunctionPointer<NativeModuleCallback>(moduleCallbackPtr)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
s_functions.EnumerateRemoteSectionsAndModules(handle,
|
||||||
|
// Section callback
|
||||||
|
(ref ReClassNET.Core.EnumerateRemoteSectionData sdata) =>
|
||||||
|
{
|
||||||
|
if (nativeSectionCb == null) return;
|
||||||
|
|
||||||
|
const int kSize = 8 + 8 + 4 + 4 + 4 + 32 + 520; // 580
|
||||||
|
IntPtr mem = Marshal.AllocHGlobal(kSize);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] z = new byte[kSize];
|
||||||
|
Marshal.Copy(z, 0, mem, kSize);
|
||||||
|
|
||||||
|
Marshal.WriteInt64(mem, 0, sdata.BaseAddress.ToInt64());
|
||||||
|
Marshal.WriteInt64(mem, 8, sdata.Size.ToInt64());
|
||||||
|
Marshal.WriteInt32(mem, 16, (int)sdata.Type);
|
||||||
|
Marshal.WriteInt32(mem, 20, (int)sdata.Category);
|
||||||
|
Marshal.WriteInt32(mem, 24, (int)sdata.Protection);
|
||||||
|
|
||||||
|
if (sdata.Name != null)
|
||||||
|
{
|
||||||
|
char[] c = sdata.Name.ToCharArray();
|
||||||
|
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 28),
|
||||||
|
Math.Min(c.Length, 15));
|
||||||
|
}
|
||||||
|
if (sdata.ModulePath != null)
|
||||||
|
{
|
||||||
|
char[] c = sdata.ModulePath.ToCharArray();
|
||||||
|
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 60),
|
||||||
|
Math.Min(c.Length, 259));
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeSectionCb(mem);
|
||||||
|
}
|
||||||
|
finally { Marshal.FreeHGlobal(mem); }
|
||||||
|
},
|
||||||
|
// Module callback
|
||||||
|
(ref ReClassNET.Core.EnumerateRemoteModuleData mdata) =>
|
||||||
|
{
|
||||||
|
if (nativeModuleCb == null) return;
|
||||||
|
|
||||||
|
const int kSize = 8 + 8 + 520; // 536
|
||||||
|
IntPtr mem = Marshal.AllocHGlobal(kSize);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] z = new byte[kSize];
|
||||||
|
Marshal.Copy(z, 0, mem, kSize);
|
||||||
|
|
||||||
|
Marshal.WriteInt64(mem, 0, mdata.BaseAddress.ToInt64());
|
||||||
|
Marshal.WriteInt64(mem, 8, mdata.Size.ToInt64());
|
||||||
|
|
||||||
|
if (mdata.Path != null)
|
||||||
|
{
|
||||||
|
char[] c = mdata.Path.ToCharArray();
|
||||||
|
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 16),
|
||||||
|
Math.Min(c.Length, 259));
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeModuleCb(mem);
|
||||||
|
}
|
||||||
|
finally { Marshal.FreeHGlobal(mem); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- ControlRemoteProcess --
|
||||||
|
private static void ControlProcessImpl(IntPtr handle, int action)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
s_functions?.ControlRemoteProcess(handle,
|
||||||
|
(ReClassNET.Core.ControlRemoteProcessAction)action);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal file
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AssemblyName>RcNetBridge</AssemblyName>
|
||||||
|
<RootNamespace>RcNetBridge</RootNamespace>
|
||||||
|
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user