Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
https://github.com/nwnxee/unified/compare/build8193.36.12...HEAD

### Added
- N/A
- DotNET: Added `NWNX_DOTNET_METHOD` option to change entrypoint method (default: `Bootstrap`)
- DotNET: Added `NWNX_DOTNET_NEW_BOOTSTRAP` option to enable a new bootstrap method with less boilerplate code.

##### New Plugins
- Store: Enables getting and setting store data.
Expand All @@ -28,7 +29,8 @@ https://github.com/nwnxee/unified/compare/build8193.36.12...HEAD
- Damage: added iSpellId to the NWNX_Damage_DamageEventData struct.

### Deprecated
- N/A
- DotNET: GetFunctionPointer()
- DotNET: GetNWNXExportedGlobals()

### Removed
- N/A
Expand Down
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ function(add_plugin target)
configure_plugin(${target})
endfunction()

function(add_shared_plugin target)
add_library(${target} SHARED ${ARGN})
configure_plugin(${target})
endfunction()

Comment on lines +33 to +37
Copy link
Copy Markdown
Contributor Author

@jhett12321 jhett12321 May 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PInvoke cannot accessed exported symbols of "Module" visible libraries, so I added a new function here with different visibility.

We can also change the plugin default to have shared visibility, which would allow C# code to directly call exports from other NWNX plugins.

I can inline this directly in DotNET/CMakeLists if you do not want this added here.

function(configure_plugin target)
add_sanitizers(${target})
target_link_libraries(${target} Core)
Expand Down
2 changes: 1 addition & 1 deletion Plugins/DotNET/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
add_plugin(DotNET
add_shared_plugin(DotNET
"DotNET.cpp"
"DotNETExports.cpp")
183 changes: 125 additions & 58 deletions Plugins/DotNET/DotNET.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting
#include "nwnx.hpp"
#include "DotNET.hpp"

#include <algorithm>
#include <string>
Expand All @@ -15,18 +16,53 @@ using namespace NWNXLib::API;

namespace DotNET {

std::vector<void*> GetExports();
static void DotNET() __attribute__((constructor));
extern std::vector<void*> GetExports();
extern AllHandlers s_handlers;

static void* LoadNetHost();
static bool LoadHostFxr(void* nethost);
static void LoadNetCore(const std::string& assembly);
static void CoreMessageHandler(const std::vector<std::string>& message);
static void Bootstrap();

static hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config;
static hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate;
static hostfxr_close_fn hostfxr_close;
static hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config;
static hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate;
static hostfxr_close_fn hostfxr_close;
static load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer;

static void DotNET() __attribute__((constructor));

static bool InitThunks()
static void DotNET()
{
void *nethost = nullptr;
void *hostfxr = nullptr;
void* nethost = LoadNetHost();
if (!nethost)
{
LOG_ERROR("Unable to load libnethost.so. .NET plugin will be unavailable.");
LOG_ERROR("If you're not using the .NET plugin, you can disable this message with 'NWNX_DOTNET_SKIP=y'");
return;
}

if (!LoadHostFxr(nethost))
{
LOG_ERROR("Unable to load hostfxr.so. .NET plugin will be unavailable.");
return;
}

const auto assembly = Config::Get<std::string>("ASSEMBLY", "");
if (assembly.empty())
{
LOG_ERROR("NWNX_DOTNET_ASSEMBLY not specified. DotNET plugin will not be loaded.");
LOG_ERROR("If you're not using the .NET plugin, you can disable this message with 'NWNX_DOTNET_SKIP=y'");
return;
}

LoadNetCore(assembly);
MessageBus::Subscribe("NWNX_CORE_SIGNAL", CoreMessageHandler);
}

void* LoadNetHost()
{
void* nethost = nullptr;
if (auto nethost_path = Config::Get<std::string>("NETHOST_PATH"))
{
nethost = dlopen(nethost_path->c_str(), RTLD_LAZY);
Expand All @@ -40,12 +76,12 @@ static bool InitThunks()
"./libnethost.so",
"lib/libnethost.so"
};
for (size_t i = 0; i < std::size(paths); i++)
for (auto& path : paths)
{
nethost = dlopen(paths[i], RTLD_LAZY);
nethost = dlopen(path, RTLD_LAZY);
if (nethost)
{
LOG_INFO("Loaded libnethost.so from: %s (autodetected)", paths[i]);
LOG_INFO("Loaded libnethost.so from: %s (autodetected)", path);
break;
}
}
Expand All @@ -56,11 +92,10 @@ static bool InitThunks()
const auto hostBaseDir = "/usr/share/dotnet/packs/Microsoft.NETCore.App.Host.linux-x64/";
const auto hostLibSuffix = "/runtimes/linux-x64/native/libnethost.so";

DIR* dir = opendir(hostBaseDir);

auto dir = opendir(hostBaseDir);
if (dir != nullptr)
{
dirent* directoryEntry = readdir(dir);
auto* directoryEntry = readdir(dir);
std::vector<std::string> paths;

while (directoryEntry != nullptr)
Expand All @@ -79,10 +114,9 @@ static bool InitThunks()
if (!paths.empty())
{
std::sort(paths.begin(), paths.end(), std::greater<std::string>());
for (std::string path : paths)
for (const std::string& path : paths)
{
nethost = dlopen(path.c_str(), RTLD_LAZY);

if (nethost)
{
LOG_INFO("Loaded libnethost.so from: %s (autodetected)", path);
Expand All @@ -93,13 +127,11 @@ static bool InitThunks()
}
}

if (!nethost)
{
LOG_ERROR("Unable to load libnethost.so. .NET plugin will be unavailable.");
LOG_ERROR("If you're not using the .NET plugin, you can disable this message with 'NWNX_DOTNET_SKIP=y'");
return false;
}
return nethost;
}

bool LoadHostFxr(void* nethost)
{
auto get_hostfxr_path = (int(*)(char*,size_t*,const void*))dlsym(nethost, "get_hostfxr_path");
ASSERT_OR_RETURN(get_hostfxr_path != nullptr, false);

Expand All @@ -108,7 +140,7 @@ static bool InitThunks()
ASSERT_OR_RETURN(get_hostfxr_path(buffer, &buffer_size, nullptr) == 0, false);
dlclose(nethost);

hostfxr = dlopen(buffer, RTLD_LAZY);
void *hostfxr = dlopen(buffer, RTLD_LAZY);
ASSERT_OR_RETURN(hostfxr != nullptr, false);

hostfxr_initialize_for_runtime_config = (hostfxr_initialize_for_runtime_config_fn)dlsym(hostfxr, "hostfxr_initialize_for_runtime_config");
Expand All @@ -122,50 +154,85 @@ static bool InitThunks()
return true;
}

static void DotNET()
void LoadNetCore(const std::string& assembly)
{
// If the initial lib loads failed, we probably don't have .NET installed.
if (!InitThunks())
return;

auto assembly = Config::Get<std::string>("ASSEMBLY");
auto entrypoint = Config::Get<std::string>("ENTRYPOINT", "NWN.Internal");

if (!assembly.has_value())
{
LOG_ERROR("NWNX_DOTNET_ASSEMBLY not specified. DotNET plugin will not be loaded.");
LOG_ERROR("If you're not using the .NET plugin, you can disable this message with 'NWNX_DOTNET_SKIP=y'");
return;
}

// Load .NET Core
hostfxr_handle cxt = nullptr;
auto runtimeconfig = *assembly + ".runtimeconfig.json";
int rc = hostfxr_initialize_for_runtime_config(runtimeconfig.c_str(), nullptr, &cxt);
if (rc != 0 || cxt == nullptr)
LOG_FATAL("Unable to load runtime config '%s'; rc=0x%x", runtimeconfig, rc);

auto runtimeConfig = assembly + ".runtimeConfig.json";
int returnCode = hostfxr_initialize_for_runtime_config(runtimeConfig.c_str(), nullptr, &cxt);
if (returnCode != 0 || cxt == nullptr)
LOG_FATAL("Unable to load runtime config '%s'; returnCode=0x%x", runtimeConfig, returnCode);

// Get the load assembly function pointer
load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = nullptr;
rc = hostfxr_get_runtime_delegate(cxt, hdt_load_assembly_and_get_function_pointer, (void**)&load_assembly_and_get_function_pointer);
if (rc != 0 || load_assembly_and_get_function_pointer == nullptr)
LOG_FATAL("Unable to get load_assembly_and_get_function_pointer; rc=0x%x", rc);
load_assembly_and_get_function_pointer = nullptr;
returnCode = hostfxr_get_runtime_delegate(cxt, hdt_load_assembly_and_get_function_pointer, (void**)&load_assembly_and_get_function_pointer);
if (returnCode != 0 || load_assembly_and_get_function_pointer == nullptr)
LOG_FATAL("Unable to get load_assembly_and_get_function_pointer; returnCode=0x%x", returnCode);

hostfxr_close(cxt);
}

void CoreMessageHandler(const std::vector<std::string> &message)
{
const std::string& messageType = message[0];

component_entry_point_fn bootstrap = nullptr;
auto dll = *assembly + ".dll";
auto full_ep = entrypoint + ", " + assembly->substr(assembly->find_last_of("/\\") + 1);
rc = load_assembly_and_get_function_pointer(dll.c_str(), full_ep.c_str(),
"Bootstrap", nullptr, nullptr, (void**)&bootstrap);
if (rc != 0 || bootstrap == nullptr)
LOG_FATAL("Unable to get %s.Bootstrap() function: dll='%s'; rc=0x%x", full_ep, dll, rc);
if (messageType == "ON_NWNX_LOADED")
{
Bootstrap();
}
Comment on lines +179 to +182
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bootstrapping was moved to the end of NWNX init to allow for all enabled NWNX plugins to be loaded, in-case C# code attempts to use exports from other plugins.

if (s_handlers.Signal)
{
if (API::Globals::VirtualMachine())
{
int spBefore = Utils::PushScriptContext(Constants::OBJECT_INVALID, 0, false);
s_handlers.Signal(message[0].c_str());
int spAfter = Utils::PopScriptContext();
ASSERT_MSG(spBefore == spAfter, "spBefore=%x, spAfter=%x", spBefore, spAfter);
}
else
{
s_handlers.Signal(message[0].c_str());
}
}
}

std::vector<void*> args = GetExports();
rc = bootstrap(args.data(), args.size()*sizeof(void*));
if (rc != 0)
LOG_FATAL("Failed to execute bootstrap function; rc=0x%x", rc);
void Bootstrap()
{
const auto assembly = Config::Get<std::string>("ASSEMBLY", "");
const auto entrypoint = Config::Get<std::string>("ENTRYPOINT", "NWN.Internal");
const auto method = Config::Get<std::string>("METHOD", "Bootstrap");
const auto newBootstrap = Config::Get<bool>("NEW_BOOTSTRAP", false);

int returnCode = 0;
auto assemblyPath = assembly + ".dll";
auto fullTypeName = entrypoint + ", " + assembly.substr(assembly.find_last_of("/\\") + 1);

if (newBootstrap)
{
void (*bootstrap)() = nullptr;
returnCode = load_assembly_and_get_function_pointer(assemblyPath.c_str(), fullTypeName.c_str(),
method.c_str(), "System.Action, System.Runtime", nullptr, (void**)&bootstrap);
if (returnCode != 0 || bootstrap == nullptr)
LOG_FATAL("Unable to get %s.%s() function: assemblyPath='%s'; returnCode=0x%x", fullTypeName, method, assemblyPath, returnCode);

bootstrap();
if (returnCode != 0)
LOG_FATAL("Failed to execute bootstrap function; returnCode=0x%x", returnCode);
}
else
{
LOG_WARNING("Using legacy bootstrap method for function exports. This will be removed in a future release. Set NWNX_DOTNET_NEW_BOOTSTRAP to use the new bootstrap method and hide this message.");
component_entry_point_fn bootstrap = nullptr;
returnCode = load_assembly_and_get_function_pointer(assemblyPath.c_str(), fullTypeName.c_str(),
method.c_str(), nullptr, nullptr, (void**)&bootstrap);
if (returnCode != 0 || bootstrap == nullptr)
LOG_FATAL("Unable to get %s.%s() function: assemblyPath='%s'; returnCode=0x%x", fullTypeName, method, assemblyPath, returnCode);

std::vector<void*> args = GetExports();
returnCode = bootstrap(args.data(), args.size() * sizeof(void*));
if (returnCode != 0)
LOG_FATAL("Failed to execute bootstrap function; returnCode=0x%x", returnCode);
}

LOG_INFO("Managed code bootstrapped.");
}
Expand Down
20 changes: 20 additions & 0 deletions Plugins/DotNET/DotNET.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

namespace DotNET {
using MainLoopHandler = void (*)(uint64_t);
using RunScriptHandler = int (*)(const char*, uint32_t);
using ClosureHandler = void (*)(uint64_t, uint32_t);
using SignalHandler = void (*)(const char*);
using AssertHandler = void (*)(const char*, const char*);
using CrashHandler = void (*)(int, const char*);

struct AllHandlers
{
MainLoopHandler MainLoop;
RunScriptHandler RunScript;
ClosureHandler Closure;
SignalHandler Signal;
AssertHandler Assert;
CrashHandler Crash;
};
}
Loading