1
0
mirror of synced 2025-01-18 09:04:52 +01:00

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:
Nik 2023-07-15 14:29:14 +02:00 committed by GitHub
parent 4e3b8111fd
commit 5171bea0bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1219 additions and 34 deletions

View File

@ -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:

View File

@ -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

View File

@ -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)

View 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()

View File

@ -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
View File

@ -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

View File

@ -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;
}

View File

@ -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();

View File

@ -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

View 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})

View File

@ -0,0 +1,6 @@
.vs/
bin/
obj/
*.dll
*.pdb
*.json

View File

@ -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>

View 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;
}
}
}

View File

@ -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>

View File

@ -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>

View 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)

View File

@ -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}")

View File

@ -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;
};
}

View 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;
};
}

View 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__)

View 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"
}
}

View 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;
}
}

View 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();
});
}
}

View 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);
}

View 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;
}

View 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();
}

View File

@ -0,0 +1,3 @@
.vs/
ImHexScript/bin/
ImHexScript/obj/

View 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

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace ImHex
{
public static class Library
{
public const string Name = "script_loader.hexplug";
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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>

View File

@ -0,0 +1,10 @@
using ImHex;
using System.Drawing;
class Script
{
public static void Main()
{
}
}

View File

@ -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>

View File

@ -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>