feat: Added cross-platform .NET scripts support (#1185)
This PR intends to add support for .NET scripts that can extend ImHex's functionality in a portable and cross-platform way. --------- Co-authored-by: Justus Garbe <55301990+Nowilltolife@users.noreply.github.com>
This commit is contained in:
parent
4e3b8111fd
commit
5171bea0bf
39
.github/workflows/build.yml
vendored
39
.github/workflows/build.yml
vendored
@ -60,6 +60,11 @@ jobs:
|
||||
libbacktrace:p
|
||||
ninja:p
|
||||
|
||||
- name: ⬇️ Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: 📜 Set version variable
|
||||
run: |
|
||||
echo "IMHEX_VERSION=`cat VERSION`" >> $GITHUB_ENV
|
||||
@ -164,6 +169,11 @@ jobs:
|
||||
run: |
|
||||
brew install glfw
|
||||
|
||||
- name: ⬇️ Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: 🧰 Checkout glfw
|
||||
if: ${{matrix.custom_glfw}}
|
||||
uses: actions/checkout@v3
|
||||
@ -243,8 +253,8 @@ jobs:
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Install git
|
||||
run: apt update && apt install -y git
|
||||
- name: ⬇️ Install setup dependencies
|
||||
run: apt update && apt install -y git curl
|
||||
|
||||
- name: 🧰 Checkout
|
||||
uses: actions/checkout@v3
|
||||
@ -270,6 +280,11 @@ jobs:
|
||||
apt update
|
||||
bash dist/get_deps_debian.sh
|
||||
|
||||
- name: ⬇️ Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
# Ubuntu cmake build
|
||||
- name: 🛠️ Build
|
||||
run: |
|
||||
@ -280,7 +295,7 @@ jobs:
|
||||
mkdir -p build
|
||||
cd build
|
||||
CC=gcc-12 CXX=g++-12 cmake -G "Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \
|
||||
-DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \
|
||||
-DCMAKE_INSTALL_PREFIX="/usr" \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
@ -290,6 +305,7 @@ jobs:
|
||||
-DIMHEX_COMMIT_BRANCH="${{ env.COMMIT_BRANCH }}" \
|
||||
-DIMHEX_ENABLE_LTO=ON \
|
||||
-DIMHEX_USE_GTK_FILE_PICKER=ON \
|
||||
-DDOTNET_EXECUTABLE="dotnet" \
|
||||
..
|
||||
DESTDIR=DebDir ninja install
|
||||
|
||||
@ -344,6 +360,11 @@ jobs:
|
||||
sudo chmod +x /usr/local/bin/appimagetool
|
||||
sudo pip3 install git+https://github.com/iTrooz/appimage-builder@dpkg-package-versions
|
||||
|
||||
- name: ⬇️ Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: 📜 Set version variable
|
||||
run: |
|
||||
echo "IMHEX_VERSION=`cat VERSION`" >> $GITHUB_ENV
|
||||
@ -362,7 +383,7 @@ jobs:
|
||||
-DIMHEX_COMMIT_HASH_SHORT="${GITHUB_SHA::7}" \
|
||||
-DIMHEX_COMMIT_HASH_LONG="${GITHUB_SHA}" \
|
||||
-DIMHEX_COMMIT_BRANCH="${GITHUB_REF##*/}" \
|
||||
-DIMHEX_ENABLE_LTO=ON \
|
||||
-DIMHEX_ENABLE_LTO=ON \
|
||||
-DIMHEX_PLUGINS_IN_SHARE=ON \
|
||||
-DIMHEX_USE_BUNDLED_CA=ON \
|
||||
..
|
||||
@ -412,6 +433,11 @@ jobs:
|
||||
run: |
|
||||
dist/get_deps_archlinux.sh --noconfirm
|
||||
|
||||
- name: ⬇️ Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: 📜 Setup ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
@ -536,6 +562,11 @@ jobs:
|
||||
fedpkg \
|
||||
ccache
|
||||
|
||||
- name: ⬇️ Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: 📜 Setup ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.5
|
||||
with:
|
||||
|
@ -10,6 +10,7 @@ option(IMHEX_PATTERNS_PULL_MASTER "Download latest files from master branch of t
|
||||
option(IMHEX_IGNORE_BAD_COMPILER "Allow compiling with an unsupported compiler" OFF)
|
||||
option(IMHEX_USE_GTK_FILE_PICKER "Use GTK file picker instead of xdg-desktop-portals" OFF)
|
||||
option(IMHEX_DISABLE_STACKTRACE "Disables support for printing stack traces" OFF)
|
||||
option(IMHEX_BUNDLE_DOTNET "Bundle .NET runtime" ON)
|
||||
option(IMHEX_ENABLE_LTO "Enables Link Time Optimizations if possible" OFF)
|
||||
option(IMHEX_USE_DEFAULT_BUILD_SETTINGS "Use default build settings" OFF)
|
||||
option(IMHEX_STRICT_WARNINGS "Enable most available warnings and treat them as errors" ON)
|
||||
@ -40,6 +41,7 @@ verifyCompiler()
|
||||
set(PLUGINS
|
||||
builtin
|
||||
windows
|
||||
script_loader
|
||||
)
|
||||
|
||||
# Add various defines
|
||||
|
@ -24,7 +24,7 @@ macro(addDefines)
|
||||
set(IMHEX_VERSION_STRING ${IMHEX_VERSION_STRING}-Debug)
|
||||
add_compile_definitions(DEBUG _GLIBCXX_DEBUG _GLIBCXX_VERBOSE)
|
||||
elseif (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
|
||||
set(IMHEX_VERSION_STRING ${IMHEX_VERSION_STRING}-RelWithDebInfo)
|
||||
set(IMHEX_VERSION_STRING ${IMHEX_VERSION_STRING})
|
||||
add_compile_definitions(NDEBUG)
|
||||
elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
|
||||
set(IMHEX_VERSION_STRING ${IMHEX_VERSION_STRING}-MinSizeRel)
|
||||
|
81
cmake/modules/FindCoreClrEmbed.cmake
Normal file
81
cmake/modules/FindCoreClrEmbed.cmake
Normal file
@ -0,0 +1,81 @@
|
||||
set(CoreClrEmbed_FOUND FALSE)
|
||||
|
||||
set(CORECLR_ARCH "linux-x64")
|
||||
set(CORECLR_SUBARCH "x64")
|
||||
if (WIN32)
|
||||
set(CORECLR_ARCH "win-x64")
|
||||
endif()
|
||||
if (UNIX)
|
||||
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64")
|
||||
set(CORECLR_ARCH "linux-arm64")
|
||||
set(CORECLR_SUBARCH "arm64")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT DOTNET_EXECUTABLE)
|
||||
set(DOTNET_EXECUTABLE dotnet)
|
||||
endif ()
|
||||
|
||||
set(CORECLR_VERSION "7.0")
|
||||
|
||||
execute_process(COMMAND ${DOTNET_EXECUTABLE} "--list-runtimes" OUTPUT_VARIABLE CORECLR_LIST_RUNTIMES_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
if (CORECLR_LIST_RUNTIMES_OUTPUT STREQUAL "")
|
||||
message(STATUS "Unable to find any .NET runtimes")
|
||||
return()
|
||||
endif ()
|
||||
|
||||
set(_ALL_RUNTIMES ${CORECLR_LIST_RUNTIMES_OUTPUT})
|
||||
string(REPLACE "\n" ";" _ALL_RUNTIMES_LIST ${_ALL_RUNTIMES})
|
||||
|
||||
foreach(X ${_ALL_RUNTIMES_LIST})
|
||||
string(REGEX MATCH "Microsoft\.NETCore\.App ([0-9]+)\.([0-9]+)\.([a-zA-Z0-9.-]+) [\[](.*)Microsoft\.NETCore\.App[\]]"
|
||||
CORECLR_VERSION_REGEX_MATCH ${X})
|
||||
|
||||
set(_RUNTIME_VERSION ${CMAKE_MATCH_1}.${CMAKE_MATCH_2})
|
||||
|
||||
if (CMAKE_MATCH_1 AND CMAKE_MATCH_4)
|
||||
if (${_RUNTIME_VERSION} STREQUAL ${CORECLR_VERSION})
|
||||
set(CORECLR_RUNTIME_VERSION ${_RUNTIME_VERSION})
|
||||
set(CORECLR_RUNTIME_VERSION_FULL ${CORECLR_VERSION}.${CMAKE_MATCH_3})
|
||||
set(CORECLR_RUNTIME_ROOT_PATH ${CMAKE_MATCH_4})
|
||||
message(STATUS "Found matching .NET runtime version '${CORECLR_RUNTIME_VERSION_FULL}' path='${CORECLR_RUNTIME_ROOT_PATH}'")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (CORECLR_RUNTIME_ROOT_PATH)
|
||||
get_filename_component(CORECLR_RUNTIME_ROOT_PATH ${CORECLR_RUNTIME_ROOT_PATH} DIRECTORY)
|
||||
endif()
|
||||
set(CoreClrEmbed_ROOT_PATH "${CORECLR_RUNTIME_ROOT_PATH}")
|
||||
|
||||
|
||||
file(GLOB _CORECLR_HOST_ARCH_PATH "${CORECLR_RUNTIME_ROOT_PATH}/packs/Microsoft.NETCore.App.Host.*-${CORECLR_SUBARCH}")
|
||||
if (_CORECLR_HOST_ARCH_PATH)
|
||||
get_filename_component(_CORECLR_HOST_ARCH_FILENAME ${_CORECLR_HOST_ARCH_PATH} NAME)
|
||||
string(REPLACE "Microsoft.NETCore.App.Host." "" _CORECLR_COMPUTED_ARCH "${_CORECLR_HOST_ARCH_FILENAME}")
|
||||
if (_CORECLR_COMPUTED_ARCH)
|
||||
set(CORECLR_ARCH "${_CORECLR_COMPUTED_ARCH}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CORECLR_HOST_BASE_PATH "${CORECLR_RUNTIME_ROOT_PATH}/packs/Microsoft.NETCore.App.Host.${CORECLR_ARCH}/${CORECLR_RUNTIME_VERSION_FULL}")
|
||||
file(GLOB _CORECLR_FOUND_PATH ${CORECLR_HOST_BASE_PATH})
|
||||
if (_CORECLR_FOUND_PATH)
|
||||
set(CORECLR_NETHOST_ROOT "${_CORECLR_FOUND_PATH}/runtimes/${CORECLR_ARCH}/native")
|
||||
endif()
|
||||
|
||||
find_library(CoreClrEmbed_LIBRARY nethost PATHS
|
||||
${CORECLR_NETHOST_ROOT}
|
||||
)
|
||||
find_path(CoreClrEmbed_INCLUDE_DIR nethost.h PATHS
|
||||
${CORECLR_NETHOST_ROOT}
|
||||
)
|
||||
find_file(CoreClrEmbed_SHARED_LIBRARY nethost.dll nethost.so libnethost.so nethost.dylib libnethost.dylib PATHS
|
||||
${CORECLR_NETHOST_ROOT})
|
||||
|
||||
if (CoreClrEmbed_INCLUDE_DIR AND CoreClrEmbed_LIBRARY)
|
||||
set(CoreClrEmbed_FOUND TRUE)
|
||||
set(CoreClrEmbed_LIBRARIES "${CoreClrEmbed_LIBRARY}")
|
||||
set(CoreClrEmbed_SHARED_LIBRARIES "${CoreClrEmbed_SHARED_LIBRARY}")
|
||||
set(CoreClrEmbed_INCLUDE_DIRS "${CoreClrEmbed_INCLUDE_DIR}")
|
||||
endif()
|
@ -15,7 +15,9 @@ if(CMAKE_GENERATOR)
|
||||
set(_POSTPROCESS_BUNDLE_MODULE_LOCATION "${CMAKE_CURRENT_LIST_FILE}")
|
||||
function(postprocess_bundle out_target in_target)
|
||||
add_custom_command(TARGET ${out_target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -DBUNDLE_PATH="$<TARGET_FILE_DIR:${in_target}>/../.." -DCODE_SIGN_CERTIFICATE_ID="${CODE_SIGN_CERTIFICATE_ID}"
|
||||
COMMAND ${CMAKE_COMMAND} -DBUNDLE_PATH="$<TARGET_FILE_DIR:${in_target}>/../.."
|
||||
-DCODE_SIGN_CERTIFICATE_ID="${CODE_SIGN_CERTIFICATE_ID}"
|
||||
-DEXTRA_BUNDLE_LIBRARY_PATHS="${EXTRA_BUNDLE_LIBRARY_PATHS}"
|
||||
-P "${_POSTPROCESS_BUNDLE_MODULE_LOCATION}"
|
||||
)
|
||||
endfunction()
|
||||
@ -29,13 +31,13 @@ message(STATUS "Fixing up application bundle: ${BUNDLE_PATH}")
|
||||
# Make sure to fix up any included ImHex plugin.
|
||||
file(GLOB_RECURSE extra_libs "${BUNDLE_PATH}/Contents/MacOS/plugins/*.hexplug")
|
||||
|
||||
message(STATUS "Fixing up application bundle: ${extra_dirs}")
|
||||
|
||||
# BundleUtilities doesn't support DYLD_FALLBACK_LIBRARY_PATH behavior, which
|
||||
# makes it sometimes break on libraries that do weird things with @rpath. Specify
|
||||
# equivalent search directories until https://gitlab.kitware.com/cmake/cmake/issues/16625
|
||||
# is fixed and in our minimum CMake version.
|
||||
set(extra_dirs "/usr/local/lib" "/lib" "/usr/lib")
|
||||
set(extra_dirs "/usr/local/lib" "/lib" "/usr/lib" ${EXTRA_BUNDLE_LIBRARY_PATHS})
|
||||
message(STATUS "Fixing up application bundle: ${extra_dirs}")
|
||||
|
||||
# BundleUtilities is overly verbose, so disable most of its messages
|
||||
function(message)
|
||||
|
2
dist/rpm/imhex.spec
vendored
2
dist/rpm/imhex.spec
vendored
@ -26,6 +26,7 @@ BuildRequires: llvm-devel
|
||||
BuildRequires: mbedtls-devel
|
||||
BuildRequires: yara-devel
|
||||
BuildRequires: nativefiledialog-extended-devel
|
||||
BuildRequires: dotnet-sdk-7.0
|
||||
%if 0%{?rhel}
|
||||
BuildRequires: gcc-toolset-12
|
||||
%endif
|
||||
@ -78,6 +79,7 @@ CXXFLAGS+=" -std=gnu++2b"
|
||||
-D USE_SYSTEM_YARA=ON \
|
||||
-D USE_SYSTEM_NFD=ON \
|
||||
-D IMHEX_USE_GTK_FILE_PICKER=ON \
|
||||
-D IMHEX_BUNDLE_DOTNET=OFF \
|
||||
# when capstone >= 5.x is released we should be able to build against \
|
||||
# system libs of it \
|
||||
# -D USE_SYSTEM_CAPSTONE=ON
|
||||
|
@ -15,7 +15,7 @@ namespace hex {
|
||||
#if defined(OS_WINDOWS)
|
||||
this->m_handle = LoadLibraryW(path.c_str());
|
||||
|
||||
if (this->m_handle == nullptr) {
|
||||
if (this->m_handle == INVALID_HANDLE_VALUE || this->m_handle == nullptr) {
|
||||
log::error("LoadLibraryW failed: {}!", std::system_category().message(::GetLastError()));
|
||||
return;
|
||||
}
|
||||
@ -76,6 +76,8 @@ namespace hex {
|
||||
if (this->m_handle == nullptr)
|
||||
return false;
|
||||
|
||||
const auto pluginName = wolv::util::toUTF8String(this->m_path.filename());
|
||||
|
||||
const auto requestedVersion = getCompatibleVersion();
|
||||
if (requestedVersion != ImHexApi::System::getImHexVersion()) {
|
||||
if (requestedVersion.empty()) {
|
||||
@ -87,7 +89,13 @@ namespace hex {
|
||||
}
|
||||
|
||||
if (this->m_initializePluginFunction != nullptr) {
|
||||
this->m_initializePluginFunction();
|
||||
try {
|
||||
this->m_initializePluginFunction();
|
||||
} catch (const std::exception &e) {
|
||||
log::error("Plugin '{}' threw an exception on init: {}", pluginName, e.what());
|
||||
} catch (...) {
|
||||
log::error("Plugin '{}' threw an exception on init", pluginName);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ namespace hex::init {
|
||||
GLFWwindow *m_window;
|
||||
std::mutex m_progressMutex;
|
||||
std::atomic<float> m_progress = 0;
|
||||
std::string m_currTaskName;
|
||||
std::list<std::string> m_currTaskNames;
|
||||
|
||||
void initGLFW();
|
||||
void initImGui();
|
||||
|
@ -59,34 +59,41 @@ namespace hex::init {
|
||||
std::atomic<u32> tasksCompleted = 0;
|
||||
for (const auto &[name, task, async] : this->m_tasks) {
|
||||
auto runTask = [&, task = task, name = name] {
|
||||
{
|
||||
std::lock_guard guard(this->m_progressMutex);
|
||||
this->m_currTaskName = name;
|
||||
}
|
||||
try {
|
||||
decltype(this->m_currTaskNames)::iterator taskNameIter;
|
||||
{
|
||||
std::lock_guard guard(this->m_progressMutex);
|
||||
this->m_currTaskNames.push_back(name + "...");
|
||||
taskNameIter = std::prev(this->m_currTaskNames.end());
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT {
|
||||
tasksCompleted++;
|
||||
this->m_progress = float(tasksCompleted) / this->m_tasks.size();
|
||||
};
|
||||
ON_SCOPE_EXIT {
|
||||
tasksCompleted++;
|
||||
this->m_progress = float(tasksCompleted) / this->m_tasks.size();
|
||||
};
|
||||
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
if (!task())
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
if (!task())
|
||||
status = false;
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
log::info("Task '{}' finished in {} ms", name, std::chrono::duration_cast<std::chrono::milliseconds>(endTime-startTime).count());
|
||||
|
||||
{
|
||||
std::lock_guard guard(this->m_progressMutex);
|
||||
this->m_currTaskNames.erase(taskNameIter);
|
||||
}
|
||||
} catch (std::exception &e) {
|
||||
log::error("Init task '{}' threw an exception: {}", name, e.what());
|
||||
status = false;
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
log::info("Task '{}' finished in {} ms", name, std::chrono::duration_cast<std::chrono::milliseconds>(endTime-startTime).count());
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (async) {
|
||||
TaskManager::createBackgroundTask(name, [runTask](auto&){ runTask(); });
|
||||
} else {
|
||||
runTask();
|
||||
}
|
||||
|
||||
} catch (std::exception &e) {
|
||||
log::error("Init task '{}' threw an exception: {}", name, e.what());
|
||||
status = false;
|
||||
if (async) {
|
||||
TaskManager::createBackgroundTask(name, [runTask](auto&){ runTask(); });
|
||||
} else {
|
||||
runTask();
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +153,9 @@ namespace hex::init {
|
||||
{
|
||||
std::lock_guard guard(this->m_progressMutex);
|
||||
drawList->AddRectFilled(ImVec2(0, splashTexture.getSize().y - 5) * scale, ImVec2(splashTexture.getSize().x * this->m_progress, splashTexture.getSize().y) * scale, 0xFFFFFFFF);
|
||||
drawList->AddText(ImVec2(15, splashTexture.getSize().y - 25) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("[{}] {}...", "|/-\\"[ImU32(ImGui::GetTime() * 15) % 4], this->m_currTaskName).c_str());
|
||||
|
||||
if (!this->m_currTaskNames.empty())
|
||||
drawList->AddText(ImVec2(15, splashTexture.getSize().y - 25) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("[{}] {}", "|/-\\"[ImU32(ImGui::GetTime() * 15) % 4], fmt::join(this->m_currTaskNames, " | ")).c_str());
|
||||
}
|
||||
|
||||
// Render the frame
|
||||
|
71
plugins/script_loader/CMakeLists.txt
Normal file
71
plugins/script_loader/CMakeLists.txt
Normal file
@ -0,0 +1,71 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Change this to the name of your plugin #
|
||||
project(script_loader)
|
||||
|
||||
# Add your source files here #
|
||||
|
||||
find_package(CoreClrEmbed)
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
source/plugin_script_loader.cpp
|
||||
)
|
||||
# Add additional include directories here #
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE include)
|
||||
# Add additional libraries here #
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE libimhex ${FMT_LIBRARIES})
|
||||
|
||||
if (CoreClrEmbed_FOUND)
|
||||
add_library(nethost SHARED IMPORTED)
|
||||
target_include_directories(nethost INTERFACE "${CoreClrEmbed_INCLUDE_DIRS}")
|
||||
get_filename_component(CoreClrEmbed_FOLDER ${CoreClrEmbed_SHARED_LIBRARIES} DIRECTORY)
|
||||
set_target_properties(nethost
|
||||
PROPERTIES
|
||||
IMPORTED_IMPLIB ${CoreClrEmbed_SHARED_LIBRARIES}
|
||||
IMPORTED_LOCATION ${CoreClrEmbed_LIBRARIES}
|
||||
BUILD_RPATH ${CoreClrEmbed_FOLDER}
|
||||
INSTALL_RPATH ${CoreClrEmbed_FOLDER})
|
||||
|
||||
target_link_directories(${PROJECT_NAME} PRIVATE ${CoreClrEmbed_FOLDER})
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${CoreClrEmbed_INCLUDE_DIRS})
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE DOTNET_PLUGINS=1)
|
||||
target_sources(${PROJECT_NAME} PRIVATE
|
||||
source/loaders/dotnet/dotnet_loader.cpp
|
||||
|
||||
source/script_api/v1/mem.cpp
|
||||
source/script_api/v1/bookmarks.cpp
|
||||
source/script_api/v1/ui.cpp
|
||||
)
|
||||
|
||||
set(EXTRA_BUNDLE_LIBRARY_PATHS "${CoreClrEmbed_FOLDER}" PARENT_SCOPE)
|
||||
|
||||
if (IMHEX_BUNDLE_DOTNET)
|
||||
install(FILES ${CoreClrEmbed_SHARED_LIBRARIES} DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
endif ()
|
||||
|
||||
add_subdirectory(dotnet)
|
||||
add_dependencies(${PROJECT_NAME} AssemblyLoader)
|
||||
|
||||
endif ()
|
||||
|
||||
|
||||
# ---- No need to change anything from here downwards unless you know what you're doing ---- #
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_SHARED_LIBRARY_PREFIX "")
|
||||
set(CMAKE_SHARED_LIBRARY_SUFFIX ".hexplug")
|
||||
|
||||
if (WIN32)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--allow-multiple-definition -fvisibility=hidden")
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE IMHEX_PROJECT_NAME="${PROJECT_NAME}")
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE IMHEX_VERSION="${IMHEX_VERSION_STRING}")
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
setupCompilerFlags(${PROJECT_NAME})
|
||||
|
||||
set(LIBROMFS_RESOURCE_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/romfs)
|
||||
set(LIBROMFS_PROJECT_NAME ${PROJECT_NAME})
|
||||
add_subdirectory(../../lib/external/libromfs ${CMAKE_CURRENT_BINARY_DIR}/libromfs)
|
||||
set_target_properties(${LIBROMFS_LIBRARY} PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBROMFS_LIBRARY})
|
6
plugins/script_loader/dotnet/AssemblyLoader/.gitignore
vendored
Normal file
6
plugins/script_loader/dotnet/AssemblyLoader/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.vs/
|
||||
bin/
|
||||
obj/
|
||||
*.dll
|
||||
*.pdb
|
||||
*.json
|
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>AssemblyLoader</AssemblyName>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<SelfContained>true</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
70
plugins/script_loader/dotnet/AssemblyLoader/Program.cs
Normal file
70
plugins/script_loader/dotnet/AssemblyLoader/Program.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace ImHex
|
||||
{
|
||||
|
||||
public class EntryPoint
|
||||
{
|
||||
|
||||
public static int ExecuteScript(IntPtr arg, int argLength)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteScript(Marshal.PtrToStringUTF8(arg, argLength)) ? 0 : 1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("[.NET Script] Exception in AssemblyLoader: " + e.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static bool ExecuteScript(string path)
|
||||
{
|
||||
AssemblyLoadContext? context = new("ScriptDomain_" + Path.GetFileNameWithoutExtension(path), true);
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = context.LoadFromStream(new MemoryStream(File.ReadAllBytes(path)));
|
||||
|
||||
var entryPointType = assembly.GetType("Script");
|
||||
if (entryPointType == null)
|
||||
{
|
||||
Console.WriteLine("[.NET Script] Failed to find Script type");
|
||||
return false;
|
||||
}
|
||||
|
||||
var entryPointMethod = entryPointType.GetMethod("Main", BindingFlags.Static | BindingFlags.Public);
|
||||
if (entryPointMethod == null)
|
||||
{
|
||||
Console.WriteLine("[.NET Script] Failed to find ScriptMain method");
|
||||
return false;
|
||||
}
|
||||
|
||||
entryPointMethod.Invoke(null, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("[.NET Script] Exception in AssemblyLoader: " + e.Message);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Unload();
|
||||
context = null;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net7.0\publish\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<History>True|2023-06-16T15:24:52.9876162Z;</History>
|
||||
<LastFailureDetails />
|
||||
</PropertyGroup>
|
||||
</Project>
|
28
plugins/script_loader/dotnet/CMakeLists.txt
Normal file
28
plugins/script_loader/dotnet/CMakeLists.txt
Normal file
@ -0,0 +1,28 @@
|
||||
project(dotnet_assemblies)
|
||||
|
||||
function(add_dotnet_assembly name)
|
||||
set(OUTPUT_DLL ${CMAKE_CURRENT_BINARY_DIR}/../../${name}.dll)
|
||||
set(OUTPUT_RUNTIMECONFIG ${CMAKE_CURRENT_BINARY_DIR}/../../${name}.runtimeconfig.json)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/../../${name}.dll
|
||||
COMMAND ${DOTNET_EXECUTABLE} build ${CMAKE_CURRENT_SOURCE_DIR}/${name}/${name}.csproj --nologo -c Release -o ${CMAKE_CURRENT_BINARY_DIR}/../.. && ${CMAKE_COMMAND} -DOUTPUT_RUNTIMECONFIG="${OUTPUT_RUNTIMECONFIG}" -P ${CMAKE_CURRENT_SOURCE_DIR}/post_process_runtimeconfig.cmake
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${name}/${name}.csproj
|
||||
COMMENT "Building ${name}.dll"
|
||||
)
|
||||
|
||||
|
||||
add_custom_target(${name} ALL DEPENDS ${OUTPUT_DLL})
|
||||
|
||||
install(FILES
|
||||
${OUTPUT_DLL}
|
||||
${OUTPUT_RUNTIMECONFIG}
|
||||
DESTINATION
|
||||
${PLUGINS_INSTALL_LOCATION}
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE sources ${CMAKE_CURRENT_SOURCE_DIR}/${name}/*.cs)
|
||||
target_sources(${name} PRIVATE ${sources})
|
||||
endfunction()
|
||||
|
||||
add_dotnet_assembly(AssemblyLoader)
|
@ -0,0 +1,7 @@
|
||||
file(READ ${OUTPUT_RUNTIMECONFIG} FILE_CONTENTS)
|
||||
|
||||
set(VERSION_REGEX [["version": "([0-9]+\.[0-9]+\.[0-9]+)"]])
|
||||
set(REPLACE_VALUE [["version": "7.0.0"]])
|
||||
string(REGEX REPLACE "${VERSION_REGEX}" ${REPLACE_VALUE} FILE_CONTENTS_OUT "${FILE_CONTENTS}")
|
||||
|
||||
file(WRITE ${OUTPUT_RUNTIMECONFIG} "${FILE_CONTENTS_OUT}")
|
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <loaders/loader.hpp>
|
||||
|
||||
#include <wolv/io/fs.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace hex::script::loader {
|
||||
|
||||
class DotNetLoader : public ScriptLoader {
|
||||
public:
|
||||
DotNetLoader() = default;
|
||||
~DotNetLoader() override = default;
|
||||
|
||||
bool initialize() override;
|
||||
bool loadAll() override;
|
||||
|
||||
private:
|
||||
std::function<bool(const std::fs::path&)> m_loadAssembly;
|
||||
};
|
||||
|
||||
}
|
39
plugins/script_loader/include/loaders/loader.hpp
Normal file
39
plugins/script_loader/include/loaders/loader.hpp
Normal file
@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace hex::script::loader {
|
||||
|
||||
struct Script {
|
||||
std::string name;
|
||||
std::function<void()> entryPoint;
|
||||
};
|
||||
|
||||
class ScriptLoader {
|
||||
public:
|
||||
ScriptLoader() = default;
|
||||
virtual ~ScriptLoader() = default;
|
||||
|
||||
virtual bool initialize() = 0;
|
||||
virtual bool loadAll() = 0;
|
||||
|
||||
void addScript(std::string name, std::function<void()> entryPoint) {
|
||||
this->m_scripts.emplace_back(std::move(name), std::move(entryPoint));
|
||||
}
|
||||
|
||||
const auto& getScripts() const {
|
||||
return this->m_scripts;
|
||||
}
|
||||
|
||||
protected:
|
||||
void clearScripts() {
|
||||
this->m_scripts.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Script> m_scripts;
|
||||
};
|
||||
|
||||
}
|
7
plugins/script_loader/include/script_api.hpp
Normal file
7
plugins/script_loader/include/script_api.hpp
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#define CONCAT_IMPL(x, y) x##y
|
||||
#define CONCAT(x, y) CONCAT_IMPL(x, y)
|
||||
|
||||
#define SCRIPT_API_IMPL(VERSION, ReturnAndName, ...) extern "C" [[maybe_unused, gnu::visibility("default")]] CONCAT(ReturnAndName, VERSION) (__VA_ARGS__)
|
||||
#define SCRIPT_API(ReturnAndName, ...) SCRIPT_API_IMPL(VERSION, ReturnAndName, __VA_ARGS__)
|
10
plugins/script_loader/romfs/lang/en_US.json
Normal file
10
plugins/script_loader/romfs/lang/en_US.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"code": "en-US",
|
||||
"country": "United States",
|
||||
"language": "English",
|
||||
"translations": {
|
||||
"hex.script_loader.menu.run_script": "Run Script...",
|
||||
"hex.script_loader.menu.loading": "Loading...",
|
||||
"hex.script_loader.menu.no_scripts": "No scripts found"
|
||||
}
|
||||
}
|
217
plugins/script_loader/source/loaders/dotnet/dotnet_loader.cpp
Normal file
217
plugins/script_loader/source/loaders/dotnet/dotnet_loader.cpp
Normal file
@ -0,0 +1,217 @@
|
||||
#include <loaders/dotnet/dotnet_loader.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#if defined(OS_WINDOWS)
|
||||
#include <Windows.h>
|
||||
#define STRING(str) L##str
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#define STRING(str) str
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <nethost.h>
|
||||
#include <coreclr_delegates.h>
|
||||
#include <hostfxr.h>
|
||||
|
||||
#include <hex/helpers/fs.hpp>
|
||||
#include <wolv/io/fs.hpp>
|
||||
#include <wolv/utils/guards.hpp>
|
||||
#include <wolv/utils/string.hpp>
|
||||
#include <hex/helpers/fmt.hpp>
|
||||
#include <hex/helpers/logger.hpp>
|
||||
|
||||
namespace hex::script::loader {
|
||||
|
||||
namespace {
|
||||
|
||||
using get_hostfxr_path_fn = int(*)(char_t * buffer, size_t * buffer_size, const struct get_hostfxr_parameters *parameters);
|
||||
|
||||
#if defined(OS_WINDOWS)
|
||||
void *loadLibrary(const char_t *path) {
|
||||
try {
|
||||
HMODULE h = ::LoadLibraryW(path);
|
||||
return (void*)h;
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T getExport(void *h, const char *name) {
|
||||
try {
|
||||
FARPROC f = ::GetProcAddress((HMODULE)h, name);
|
||||
return reinterpret_cast<T>((void*)f);
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
#else
|
||||
void *loadLibrary(const char_t *path) {
|
||||
void *h = dlopen(path, RTLD_LAZY | RTLD_LOCAL);
|
||||
return h;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T getExport(void *h, const char *name) {
|
||||
void *f = dlsym(h, name);
|
||||
return reinterpret_cast<T>(f);
|
||||
}
|
||||
#endif
|
||||
|
||||
hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config = nullptr;
|
||||
hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate = nullptr;
|
||||
hostfxr_close_fn hostfxr_close = nullptr;
|
||||
|
||||
bool loadHostfxr() {
|
||||
#if defined(OS_WINDOWS)
|
||||
auto netHostLibrary = loadLibrary(L"nethost.dll");
|
||||
#elif defined(OS_LINUX)
|
||||
auto netHostLibrary = loadLibrary("libnethost.so");
|
||||
#elif defined(OS_MACOS)
|
||||
auto netHostLibrary = loadLibrary("libnethost.dylib");
|
||||
#endif
|
||||
|
||||
if (netHostLibrary == nullptr)
|
||||
return false;
|
||||
|
||||
auto get_hostfxr_path_ptr = getExport<get_hostfxr_path_fn>(netHostLibrary, "get_hostfxr_path");
|
||||
|
||||
std::array<char_t, 300> buffer = { 0 };
|
||||
size_t bufferSize = buffer.size();
|
||||
if (get_hostfxr_path_ptr(buffer.data(), &bufferSize, nullptr) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void *hostfxrLibrary = loadLibrary(buffer.data());
|
||||
if (hostfxrLibrary == nullptr)
|
||||
return false;
|
||||
|
||||
{
|
||||
hostfxr_initialize_for_runtime_config
|
||||
= getExport<hostfxr_initialize_for_runtime_config_fn>(hostfxrLibrary, "hostfxr_initialize_for_runtime_config");
|
||||
hostfxr_get_runtime_delegate
|
||||
= getExport<hostfxr_get_runtime_delegate_fn>(hostfxrLibrary, "hostfxr_get_runtime_delegate");
|
||||
hostfxr_close
|
||||
= getExport<hostfxr_close_fn>(hostfxrLibrary, "hostfxr_close");
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
hostfxr_initialize_for_runtime_config != nullptr &&
|
||||
hostfxr_get_runtime_delegate != nullptr &&
|
||||
hostfxr_close != nullptr;
|
||||
}
|
||||
|
||||
load_assembly_and_get_function_pointer_fn getLoadAssemblyFunction(const std::fs::path &path) {
|
||||
load_assembly_and_get_function_pointer_fn loadAssemblyFunction = nullptr;
|
||||
|
||||
hostfxr_handle ctx = nullptr;
|
||||
|
||||
u32 result = hostfxr_initialize_for_runtime_config(path.c_str(), nullptr, &ctx);
|
||||
ON_SCOPE_EXIT {
|
||||
hostfxr_close(ctx);
|
||||
};
|
||||
|
||||
if (result > 2 || ctx == nullptr) {
|
||||
throw std::runtime_error(hex::format("Failed to initialize command line {:X}", result));
|
||||
}
|
||||
|
||||
result = hostfxr_get_runtime_delegate(
|
||||
ctx,
|
||||
hdt_load_assembly_and_get_function_pointer,
|
||||
(void**)&loadAssemblyFunction
|
||||
);
|
||||
|
||||
if (result != 0 || loadAssemblyFunction == nullptr) {
|
||||
throw std::runtime_error("Failed to get load_assembly_and_get_function_pointer delegate");
|
||||
}
|
||||
|
||||
return loadAssemblyFunction;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool DotNetLoader::initialize() {
|
||||
try {
|
||||
AT_FIRST_TIME {
|
||||
if (!loadHostfxr()) {
|
||||
throw std::runtime_error("Failed to load hostfxr");
|
||||
}
|
||||
};
|
||||
} catch (const std::exception &e) {
|
||||
log::error("Failed to initialize DotNetLoader: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& path : hex::fs::getDefaultPaths(hex::fs::ImHexPath::Plugins)) {
|
||||
auto assemblyLoader = path / "AssemblyLoader.dll";
|
||||
if (!wolv::io::fs::exists(assemblyLoader))
|
||||
continue;
|
||||
|
||||
auto loadAssembly = getLoadAssemblyFunction(std::fs::absolute(path) / "AssemblyLoader.runtimeconfig.json");
|
||||
|
||||
auto dotnetType = STRING("ImHex.EntryPoint, AssemblyLoader");
|
||||
|
||||
const char_t *dotnetTypeMethod = STRING("ExecuteScript");
|
||||
const auto &assemblyPathStr = assemblyLoader.native();
|
||||
|
||||
component_entry_point_fn entryPoint = nullptr;
|
||||
u32 result = loadAssembly(
|
||||
assemblyPathStr.c_str(),
|
||||
dotnetType,
|
||||
dotnetTypeMethod,
|
||||
nullptr,
|
||||
nullptr,
|
||||
(void**)&entryPoint
|
||||
);
|
||||
|
||||
if (result != 0 || entryPoint == nullptr) {
|
||||
log::error("Failed to load assembly loader '{}'", assemblyLoader.string());
|
||||
continue;
|
||||
}
|
||||
|
||||
this->m_loadAssembly = [entryPoint](const std::fs::path &path) -> bool {
|
||||
auto string = wolv::util::toUTF8String(path);
|
||||
auto result = entryPoint(string.data(), string.size());
|
||||
|
||||
return result == 0;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DotNetLoader::loadAll() {
|
||||
this->clearScripts();
|
||||
|
||||
for (const auto &imhexPath : hex::fs::getDefaultPaths(hex::fs::ImHexPath::Scripts)) {
|
||||
auto directoryPath = imhexPath / "custom" / "dotnet";
|
||||
if (!wolv::io::fs::exists(directoryPath))
|
||||
wolv::io::fs::createDirectories(directoryPath);
|
||||
|
||||
if (!wolv::io::fs::exists(directoryPath))
|
||||
continue;
|
||||
|
||||
for (const auto &entry : std::fs::directory_iterator(directoryPath)) {
|
||||
if (!entry.is_regular_file())
|
||||
continue;
|
||||
|
||||
const auto &scriptPath = entry.path();
|
||||
if (!std::fs::exists(scriptPath))
|
||||
continue;
|
||||
|
||||
this->addScript(scriptPath.stem().string(), [this, scriptPath] {
|
||||
this->m_loadAssembly(scriptPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
118
plugins/script_loader/source/plugin_script_loader.cpp
Normal file
118
plugins/script_loader/source/plugin_script_loader.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
#include <hex/plugin.hpp>
|
||||
#include <hex/api/content_registry.hpp>
|
||||
#include <hex/api/task.hpp>
|
||||
#include <hex/api/localization.hpp>
|
||||
|
||||
#include <hex/helpers/logger.hpp>
|
||||
#include <hex/ui/imgui_imhex_extensions.h>
|
||||
|
||||
#include <loaders/dotnet/dotnet_loader.hpp>
|
||||
|
||||
#include <romfs/romfs.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using namespace hex;
|
||||
using namespace hex::script::loader;
|
||||
|
||||
using ScriptLoaders = std::tuple<
|
||||
#if defined(DOTNET_PLUGINS)
|
||||
DotNetLoader
|
||||
#endif
|
||||
>;
|
||||
|
||||
namespace {
|
||||
|
||||
ScriptLoaders s_loaders;
|
||||
|
||||
void loadScript(std::vector<const Script*> &scripts, auto &loader) {
|
||||
loader.loadAll();
|
||||
|
||||
for (auto &script : loader.getScripts())
|
||||
scripts.emplace_back(&script);
|
||||
}
|
||||
|
||||
std::vector<const Script*> loadAllScripts() {
|
||||
std::vector<const Script*> plugins;
|
||||
|
||||
try {
|
||||
std::apply([&plugins](auto&&... args) {
|
||||
(loadScript(plugins, args), ...);
|
||||
}, s_loaders);
|
||||
} catch (const std::exception &e) {
|
||||
log::error("Error when loading scripts: {}", e.what());
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
void initializeLoader(u32 &count, auto &loader) {
|
||||
try {
|
||||
if (loader.initialize())
|
||||
count += 1;
|
||||
} catch (const std::exception &e) {
|
||||
log::error("Error when initializing script loader: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
bool initializeAllLoaders() {
|
||||
u32 count = 0;
|
||||
|
||||
std::apply([&count](auto&&... args) {
|
||||
try {
|
||||
(initializeLoader(count, args), ...);
|
||||
} catch (const std::exception &e) {
|
||||
log::error("Error when initializing script loaders: {}", e.what());
|
||||
}
|
||||
}, s_loaders);
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IMHEX_PLUGIN_SETUP("Script Loader", "WerWolv", "Script Loader plugin") {
|
||||
hex::log::debug("Using romfs: '{}'", romfs::name());
|
||||
for (auto &path : romfs::list("lang"))
|
||||
hex::ContentRegistry::Language::addLocalization(nlohmann::json::parse(romfs::get(path).string()));
|
||||
|
||||
if (initializeAllLoaders()) {
|
||||
static TaskHolder runnerTask, updaterTask;
|
||||
hex::ContentRegistry::Interface::addMenuItemSubMenu({ "hex.builtin.menu.extras" }, 5000, [] {
|
||||
static bool menuJustOpened = true;
|
||||
static std::vector<const Script*> scripts;
|
||||
|
||||
if (ImGui::BeginMenu("hex.script_loader.menu.run_script"_lang)) {
|
||||
if (menuJustOpened) {
|
||||
menuJustOpened = false;
|
||||
if (!updaterTask.isRunning()) {
|
||||
updaterTask = TaskManager::createBackgroundTask("Updating Scripts...", [] (auto&) {
|
||||
scripts = loadAllScripts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (updaterTask.isRunning()) {
|
||||
ImGui::TextSpinner("hex.script_loader.menu.loading"_lang);
|
||||
} else if (scripts.empty()) {
|
||||
ImGui::TextUnformatted("hex.script_loader.menu.no_scripts"_lang);
|
||||
}
|
||||
|
||||
for (const auto &script : scripts) {
|
||||
const auto &[name, entryPoint] = *script;
|
||||
|
||||
if (ImGui::MenuItem(name.c_str())) {
|
||||
runnerTask = TaskManager::createTask("Running script...", TaskManager::NoProgress, [entryPoint](auto&) {
|
||||
entryPoint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
} else {
|
||||
menuJustOpened = true;
|
||||
}
|
||||
}, [] {
|
||||
return !runnerTask.isRunning();
|
||||
});
|
||||
}
|
||||
}
|
9
plugins/script_loader/source/script_api/v1/bookmarks.cpp
Normal file
9
plugins/script_loader/source/script_api/v1/bookmarks.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#include <script_api.hpp>
|
||||
|
||||
#include <hex/api/imhex_api.hpp>
|
||||
|
||||
#define VERSION V1
|
||||
|
||||
SCRIPT_API(void createBookmark, u64 address, u64 size, u32 color, const char *name, const char *description) {
|
||||
hex::ImHexApi::Bookmarks::add(address, size, name, description, color);
|
||||
}
|
44
plugins/script_loader/source/script_api/v1/mem.cpp
Normal file
44
plugins/script_loader/source/script_api/v1/mem.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include <script_api.hpp>
|
||||
|
||||
#include <hex/api/imhex_api.hpp>
|
||||
#include <hex/providers/provider.hpp>
|
||||
|
||||
#define VERSION V1
|
||||
|
||||
SCRIPT_API(void readMemory, u64 address, size_t size, void *buffer) {
|
||||
auto provider = hex::ImHexApi::Provider::get();
|
||||
|
||||
if (provider == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
provider->read(address, buffer, size);
|
||||
}
|
||||
|
||||
SCRIPT_API(void writeMemory, u64 address, size_t size, void *buffer) {
|
||||
auto provider = hex::ImHexApi::Provider::get();
|
||||
|
||||
if (provider == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
provider->write(address, buffer, size);
|
||||
}
|
||||
|
||||
SCRIPT_API(bool getSelection, u64 *start, u64 *end) {
|
||||
if (start == nullptr || end == nullptr)
|
||||
return false;
|
||||
|
||||
if (!hex::ImHexApi::Provider::isValid())
|
||||
return false;
|
||||
|
||||
if (!hex::ImHexApi::HexEditor::isSelectionValid())
|
||||
return false;
|
||||
|
||||
auto selection = hex::ImHexApi::HexEditor::getSelection();
|
||||
|
||||
*start = selection->getStartAddress();
|
||||
*end = selection->getEndAddress();
|
||||
|
||||
return true;
|
||||
}
|
139
plugins/script_loader/source/script_api/v1/ui.cpp
Normal file
139
plugins/script_loader/source/script_api/v1/ui.cpp
Normal file
@ -0,0 +1,139 @@
|
||||
#include <script_api.hpp>
|
||||
|
||||
#include <hex/api/imhex_api.hpp>
|
||||
#include <hex/api/event.hpp>
|
||||
#include <hex/api/localization.hpp>
|
||||
|
||||
#include <hex/ui/popup.hpp>
|
||||
|
||||
using namespace hex;
|
||||
|
||||
#define VERSION V1
|
||||
|
||||
static std::optional<std::string> s_inputTextBoxResult;
|
||||
static std::optional<bool> s_yesNoQuestionBoxResult;
|
||||
|
||||
class PopupYesNo : public Popup<PopupYesNo> {
|
||||
public:
|
||||
PopupYesNo(std::string title, std::string message)
|
||||
: hex::Popup<PopupYesNo>(std::move(title), false),
|
||||
m_message(std::move(message)) { }
|
||||
|
||||
void drawContent() override {
|
||||
ImGui::TextFormattedWrapped("{}", this->m_message.c_str());
|
||||
ImGui::NewLine();
|
||||
ImGui::Separator();
|
||||
|
||||
auto width = ImGui::GetWindowWidth();
|
||||
ImGui::SetCursorPosX(width / 9);
|
||||
if (ImGui::Button("hex.builtin.common.yes"_lang, ImVec2(width / 3, 0))) {
|
||||
s_yesNoQuestionBoxResult = true;
|
||||
this->close();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(width / 9 * 5);
|
||||
if (ImGui::Button("hex.builtin.common.no"_lang, ImVec2(width / 3, 0))) {
|
||||
s_yesNoQuestionBoxResult = false;
|
||||
this->close();
|
||||
}
|
||||
|
||||
ImGui::SetWindowPos((ImHexApi::System::getMainWindowSize() - ImGui::GetWindowSize()) / 2, ImGuiCond_Appearing);
|
||||
}
|
||||
|
||||
[[nodiscard]] ImGuiWindowFlags getFlags() const override {
|
||||
return ImGuiWindowFlags_AlwaysAutoResize;
|
||||
}
|
||||
|
||||
[[nodiscard]] ImVec2 getMinSize() const override {
|
||||
return scaled({ 400, 100 });
|
||||
}
|
||||
|
||||
[[nodiscard]] ImVec2 getMaxSize() const override {
|
||||
return scaled({ 600, 300 });
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_message;
|
||||
};
|
||||
|
||||
class PopupInputText : public Popup<PopupInputText> {
|
||||
public:
|
||||
PopupInputText(std::string title, std::string message, size_t maxSize)
|
||||
: hex::Popup<PopupInputText>(std::move(title), false),
|
||||
m_message(std::move(message)), m_maxSize(maxSize) { }
|
||||
|
||||
void drawContent() override {
|
||||
ImGui::TextFormattedWrapped("{}", this->m_message.c_str());
|
||||
ImGui::NewLine();
|
||||
|
||||
ImGui::SetItemDefaultFocus();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
bool submitted = ImGui::InputText("##input", this->m_input, ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
if (this->m_input.size() > this->m_maxSize)
|
||||
this->m_input.resize(this->m_maxSize);
|
||||
|
||||
ImGui::NewLine();
|
||||
ImGui::Separator();
|
||||
|
||||
auto width = ImGui::GetWindowWidth();
|
||||
ImGui::SetCursorPosX(width / 9);
|
||||
ImGui::BeginDisabled(this->m_input.empty());
|
||||
if (ImGui::Button("hex.builtin.common.okay"_lang, ImVec2(width / 3, 0)) || submitted) {
|
||||
s_inputTextBoxResult = this->m_input;
|
||||
this->close();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(width / 9 * 5);
|
||||
if (ImGui::Button("hex.builtin.common.cancel"_lang, ImVec2(width / 3, 0))) {
|
||||
s_inputTextBoxResult = "";
|
||||
this->close();
|
||||
}
|
||||
|
||||
ImGui::SetWindowPos((ImHexApi::System::getMainWindowSize() - ImGui::GetWindowSize()) / 2, ImGuiCond_Appearing);
|
||||
}
|
||||
|
||||
[[nodiscard]] ImGuiWindowFlags getFlags() const override {
|
||||
return ImGuiWindowFlags_AlwaysAutoResize;
|
||||
}
|
||||
|
||||
[[nodiscard]] ImVec2 getMinSize() const override {
|
||||
return scaled({ 400, 100 });
|
||||
}
|
||||
|
||||
[[nodiscard]] ImVec2 getMaxSize() const override {
|
||||
return scaled({ 600, 300 });
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_message;
|
||||
std::string m_input;
|
||||
size_t m_maxSize;
|
||||
};
|
||||
|
||||
SCRIPT_API(void showMessageBox, const char *message) {
|
||||
hex::EventManager::post<hex::RequestOpenInfoPopup>(message);
|
||||
}
|
||||
|
||||
SCRIPT_API(void showInputTextBox, const char *title, const char *message, char *buffer, u32 bufferSize) {
|
||||
PopupInputText::open(std::string(title), std::string(message), bufferSize - 1);
|
||||
|
||||
while (!s_inputTextBoxResult.has_value()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
auto &value = s_inputTextBoxResult.value();
|
||||
std::memcpy(buffer, value.c_str(), std::min<size_t>(value.size() + 1, bufferSize));
|
||||
s_inputTextBoxResult.reset();
|
||||
}
|
||||
|
||||
SCRIPT_API(void showYesNoQuestionBox, const char *title, const char *message, bool *result) {
|
||||
PopupYesNo::open(std::string(title), std::string(message));
|
||||
|
||||
while (!s_yesNoQuestionBoxResult.has_value()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
*result = s_yesNoQuestionBoxResult.value();
|
||||
s_yesNoQuestionBoxResult.reset();
|
||||
}
|
3
plugins/script_loader/templates/CSharp/.gitignore
vendored
Normal file
3
plugins/script_loader/templates/CSharp/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.vs/
|
||||
ImHexScript/bin/
|
||||
ImHexScript/obj/
|
25
plugins/script_loader/templates/CSharp/ImHexScript.sln
Normal file
25
plugins/script_loader/templates/CSharp/ImHexScript.sln
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.6.33801.468
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImHexScript", "ImHexScript\ImHexScript.csproj", "{F5DDEF0E-0CD2-4724-87A6-96FAF1FD64B0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{F5DDEF0E-0CD2-4724-87A6-96FAF1FD64B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F5DDEF0E-0CD2-4724-87A6-96FAF1FD64B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F5DDEF0E-0CD2-4724-87A6-96FAF1FD64B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F5DDEF0E-0CD2-4724-87A6-96FAF1FD64B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8AFEBD4F-842F-4F5A-BE07-797E863F6E5C}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
@ -0,0 +1,22 @@
|
||||
#pragma warning disable SYSLIB1054
|
||||
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ImHex
|
||||
{
|
||||
public class Bookmarks
|
||||
{
|
||||
[DllImport(Library.Name)]
|
||||
private static extern void createBookmarkV1(UInt64 address, UInt64 size, UInt32 color, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string description);
|
||||
|
||||
public static void CreateBookmark(long address, long size, Color color, string name = "", string description = "")
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
createBookmarkV1((UInt64)address, (UInt64)size, (UInt32)(0xA0 << 24 | color.B << 16 | color.G << 8 | color.R), name, description);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace ImHex
|
||||
{
|
||||
public static class Library
|
||||
{
|
||||
public const string Name = "script_loader.hexplug";
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
#pragma warning disable SYSLIB1054
|
||||
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ImHex
|
||||
{
|
||||
public class Memory
|
||||
{
|
||||
[DllImport(Library.Name)]
|
||||
private static extern void readMemoryV1(UInt64 address, UInt64 size, IntPtr buffer);
|
||||
|
||||
[DllImport(Library.Name)]
|
||||
private static extern void writeMemoryV1(UInt64 address, UInt64 size, IntPtr buffer);
|
||||
|
||||
[DllImport(Library.Name)]
|
||||
private static extern bool getSelectionV1(IntPtr start, IntPtr end);
|
||||
|
||||
|
||||
public static byte[] Read(ulong address, ulong size)
|
||||
{
|
||||
byte[] bytes = new byte[size];
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* buffer = bytes)
|
||||
{
|
||||
readMemoryV1(address, size, (IntPtr)buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static void Write(ulong address, byte[] bytes)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* buffer = bytes)
|
||||
{
|
||||
writeMemoryV1(address, (UInt64)bytes.Length, (IntPtr)buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (UInt64, UInt64)? GetSelection()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
UInt64 start = 0, end = 0;
|
||||
if (!getSelectionV1((nint)(&start), (nint)(&end)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (start, end);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
#pragma warning disable SYSLIB1054
|
||||
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace ImHex
|
||||
{
|
||||
public class UI
|
||||
{
|
||||
[DllImport(Library.Name)]
|
||||
private static extern void showMessageBoxV1([MarshalAs(UnmanagedType.LPStr)] string message);
|
||||
|
||||
[DllImport(Library.Name)]
|
||||
private static extern void showInputTextBoxV1([MarshalAs(UnmanagedType.LPStr)] string title, [MarshalAs(UnmanagedType.LPStr)] string message, StringBuilder buffer, int bufferSize);
|
||||
|
||||
[DllImport(Library.Name)]
|
||||
private static extern unsafe void showYesNoQuestionBoxV1([MarshalAs(UnmanagedType.LPStr)] string title, [MarshalAs(UnmanagedType.LPStr)] string message, bool* result);
|
||||
|
||||
public static void ShowMessageBox(string message)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
showMessageBoxV1(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShowYesNoQuestionBox(string title, string message)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
bool result = false;
|
||||
showYesNoQuestionBoxV1(title, message, &result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? ShowInputTextBox(string title, string message, int maxSize)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
StringBuilder buffer = new(maxSize);
|
||||
showInputTextBoxV1(title, message, buffer, buffer.Capacity);
|
||||
|
||||
if (buffer.Length == 0 || buffer[0] == '\x00')
|
||||
return null;
|
||||
else
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Main</AssemblyName>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<SelfContained>true</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
@ -0,0 +1,10 @@
|
||||
using ImHex;
|
||||
using System.Drawing;
|
||||
|
||||
class Script
|
||||
{
|
||||
public static void Main()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net7.0\publish\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<History>True|2023-06-16T15:24:52.9876162Z;</History>
|
||||
<LastFailureDetails />
|
||||
</PropertyGroup>
|
||||
</Project>
|
Loading…
x
Reference in New Issue
Block a user