Skip to content

Commit 438ccf5

Browse files
authored
Update NWNX/NWN.Core. Fix broken function hooks after dispose. (#831)
* Update NWNX to 292a2c0. * Update NWN.Core to 8193.37.3. * Add HookService tests. * Recalculate function call delegate when function pointer changes.
1 parent 07d5d42 commit 438ccf5

File tree

8 files changed

+149
-16
lines changed

8 files changed

+149
-16
lines changed

NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
</ItemGroup>
6464

6565
<ItemGroup>
66-
<PackageReference Include="NWN.Core" Version="8193.37.2" PrivateAssets="compile" />
66+
<PackageReference Include="NWN.Core" Version="8193.37.3" PrivateAssets="compile" />
6767
<PackageReference Include="NWN.Native" Version="8193.37.3" PrivateAssets="compile" />
6868
</ItemGroup>
6969

NWN.Anvil.Tests/NWN.Anvil.Tests.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<NoWarn>1591</NoWarn>
1616
<Nullable>enable</Nullable>
1717

18+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
19+
1820
<RootNamespace>Anvil.Tests</RootNamespace>
1921
</PropertyGroup>
2022

@@ -43,7 +45,7 @@
4345

4446
<ItemGroup>
4547
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
46-
<PackageReference Include="NWN.Core" Version="8193.37.2" PrivateAssets="compile" />
48+
<PackageReference Include="NWN.Core" Version="8193.37.3" PrivateAssets="compile" />
4749
<PackageReference Include="NWN.Native" Version="8193.37.3" PrivateAssets="compile" />
4850
</ItemGroup>
4951

NWN.Anvil.Tests/NWN.Anvil.Tests.csproj.DotSettings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Capi_005Cutils/@EntryIndexedValue">True</s:Boolean>
1717
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Capi_005Cvariable/@EntryIndexedValue">True</s:Boolean>
1818
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Capi_005Cvariables/@EntryIndexedValue">True</s:Boolean>
19+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Cservices_005Ccore/@EntryIndexedValue">True</s:Boolean>
20+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Cservices_005Ccore_005Chooking/@EntryIndexedValue">True</s:Boolean>
1921
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Cservices_005Cresources/@EntryIndexedValue">True</s:Boolean>
2022
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Cservices_005Cscheduler/@EntryIndexedValue">True</s:Boolean>
2123
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cmain_005Cservices_005Cservices/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Runtime.InteropServices;
2+
using Anvil.Services;
3+
using NUnit.Framework;
4+
using NWN.Native.API;
5+
6+
namespace Anvil.Tests.Services
7+
{
8+
[TestFixture]
9+
public unsafe class HookServiceTests
10+
{
11+
[NativeFunction("_ZN11CAppManager26GetDungeonMasterEXERunningEv", "?GetDungeonMasterEXERunning@CAppManager@@QEAAHXZ")]
12+
private delegate int GetDungeonMasterExeRunning(void* pAppManager);
13+
14+
[Inject]
15+
private static HookService HookService { get; set; } = null!;
16+
17+
private static readonly delegate* unmanaged<void*, int> PHook1 = &OnHook1;
18+
private static readonly delegate* unmanaged<void*, int> PHook2 = &OnHook2;
19+
private static readonly delegate* unmanaged<void*, int> PHook3 = &OnHook3;
20+
21+
private static FunctionHook<GetDungeonMasterExeRunning>? hook1;
22+
private static FunctionHook<GetDungeonMasterExeRunning>? hook2;
23+
private static FunctionHook<GetDungeonMasterExeRunning>? hook3;
24+
25+
private static bool hook1Called;
26+
private static bool hook2Called;
27+
private static bool hook3Called;
28+
29+
[SetUp]
30+
public void Setup()
31+
{
32+
hook1Called = false;
33+
hook2Called = false;
34+
hook3Called = false;
35+
36+
hook1 = HookService.RequestHook<GetDungeonMasterExeRunning>(PHook1);
37+
hook2 = HookService.RequestHook<GetDungeonMasterExeRunning>(PHook2);
38+
hook3 = HookService.RequestHook<GetDungeonMasterExeRunning>(PHook3);
39+
}
40+
41+
[TearDown]
42+
public void TearDown()
43+
{
44+
hook1?.Dispose();
45+
hook2?.Dispose();
46+
hook3?.Dispose();
47+
}
48+
49+
[Test]
50+
public void HookOrderedDisposeTest()
51+
{
52+
hook1?.Dispose();
53+
hook1 = null;
54+
hook2?.Dispose();
55+
hook2 = null;
56+
57+
int dmExeRunning = NWNXLib.AppManager().GetDungeonMasterEXERunning();
58+
59+
Assert.That(dmExeRunning, Is.EqualTo(0));
60+
Assert.That(hook1Called, Is.False);
61+
Assert.That(hook2Called, Is.False);
62+
Assert.That(hook3Called, Is.True);
63+
}
64+
65+
[Test]
66+
public void HookUnorderedDisposeTest()
67+
{
68+
hook2?.Dispose();
69+
hook2 = null;
70+
hook1?.Dispose();
71+
hook1 = null;
72+
73+
int dmExeRunning = NWNXLib.AppManager().GetDungeonMasterEXERunning();
74+
75+
Assert.That(dmExeRunning, Is.EqualTo(0));
76+
Assert.That(hook1Called, Is.False);
77+
Assert.That(hook2Called, Is.False);
78+
Assert.That(hook3Called, Is.True);
79+
}
80+
81+
[UnmanagedCallersOnly]
82+
private static int OnHook1(void* pAppManager)
83+
{
84+
hook1Called = true;
85+
hook1?.CallOriginal(pAppManager);
86+
return 0;
87+
}
88+
89+
[UnmanagedCallersOnly]
90+
private static int OnHook2(void* pAppManager)
91+
{
92+
hook2Called = true;
93+
hook2?.CallOriginal(pAppManager);
94+
return 0;
95+
}
96+
97+
[UnmanagedCallersOnly]
98+
private static int OnHook3(void* pAppManager)
99+
{
100+
hook3Called = true;
101+
hook3?.CallOriginal(pAppManager);
102+
return 0;
103+
}
104+
}
105+
}

NWN.Anvil/NWN.Anvil.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
6868
<PackageReference Include="NLog" Version="5.4.0" />
6969
<PackageReference Include="Paket.Core" Version="8.0.3" PrivateAssets="all" />
70-
<PackageReference Include="NWN.Core" Version="8193.37.2" PrivateAssets="compile" />
70+
<PackageReference Include="NWN.Core" Version="8193.37.3" PrivateAssets="compile" />
7171
<PackageReference Include="NWN.Native" Version="8193.37.3" PrivateAssets="compile" />
7272
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
7373
</ItemGroup>
Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Runtime.InteropServices;
34
using JetBrains.Annotations;
45
using NWNX.NET;
@@ -8,31 +9,59 @@ namespace Anvil.Services
89
{
910
public sealed unsafe class FunctionHook<T> : IDisposable where T : Delegate
1011
{
11-
/// <summary>
12-
/// The original function call - invoke this to run the standard game behaviour.
13-
/// </summary>
14-
public readonly T CallOriginal;
15-
1612
// We hold a reference to the delegate to prevent clean up from the garbage collector.
1713
[UsedImplicitly]
1814
private readonly T? managedHandle;
1915

2016
private readonly HookService hookService;
2117
private readonly FunctionHook* functionHook;
2218

19+
private T functionDelegate;
20+
private void* functionDelegatePointer;
21+
22+
/// <summary>
23+
/// The original function call - invoke this to run the standard game behaviour.
24+
/// </summary>
25+
public T CallOriginal
26+
{
27+
get
28+
{
29+
void* functionPointer = functionHook->m_trampoline;
30+
if (functionPointer != functionDelegatePointer)
31+
{
32+
UpdateFunctionDelegate(functionPointer);
33+
}
34+
35+
return functionDelegate;
36+
}
37+
}
38+
2339
internal FunctionHook(HookService hookService, FunctionHook* functionHook, T? managedHandle = null)
2440
{
2541
this.hookService = hookService;
2642
this.functionHook = functionHook;
2743
this.managedHandle = managedHandle;
28-
CallOriginal = Marshal.GetDelegateForFunctionPointer<T>((IntPtr)functionHook->m_trampoline);
44+
45+
UpdateFunctionDelegate(functionHook->m_trampoline);
46+
}
47+
48+
~FunctionHook()
49+
{
50+
ReleaseUnmanagedResources();
2951
}
3052

3153
private void ReleaseUnmanagedResources()
3254
{
3355
NWNXAPI.ReturnFunctionHook(functionHook);
3456
}
3557

58+
[MemberNotNull(nameof(functionDelegate), nameof(functionDelegatePointer))]
59+
private void UpdateFunctionDelegate(void* functionPointer)
60+
{
61+
functionDelegate = Marshal.GetDelegateForFunctionPointer<T>((IntPtr)functionPointer);
62+
functionDelegatePointer = functionPointer;
63+
}
64+
3665
/// <summary>
3766
/// Releases the FunctionHook, restoring the previous behaviour.
3867
/// </summary>
@@ -42,10 +71,5 @@ public void Dispose()
4271
GC.SuppressFinalize(this);
4372
hookService.RemoveHook(this);
4473
}
45-
46-
~FunctionHook()
47-
{
48-
ReleaseUnmanagedResources();
49-
}
5074
}
5175
}

dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Load nwnx image to import nwserver + nwnx plugins
2-
FROM nwnxee/unified:7fc892a as nwnx
2+
FROM nwnxee/unified:292a2c0 as nwnx
33

44
# Remove incompatible plugins
55
RUN rm -rf /nwn/nwnx/NWNX_Ruby.so \

docs/NWN.Anvil.Samples.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
</PropertyGroup>
3131

3232
<ItemGroup>
33-
<PackageReference Include="NWN.Core" Version="8193.37.2" PrivateAssets="compile" />
33+
<PackageReference Include="NWN.Core" Version="8193.37.3" PrivateAssets="compile" />
3434
</ItemGroup>
3535

3636
<ItemGroup>

0 commit comments

Comments
 (0)