Skip to content
Open
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
77 changes: 77 additions & 0 deletions src/typstsharp.tests/NativeStringManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@

namespace typstsharp.tests;

#if DEBUG
public class NativeStringManagerTests
{
[Test]
public async Task EnsureAdded()
{
using var manager = new NativeStringManager();
await Assert.That(manager.CurrentCount).IsEqualTo(0);

var ptr = manager.GetNativeUtf8FromString("test");
await Assert.That(manager.CurrentCount).IsEqualTo(1);
await Assert.That(manager.DebugNativeStringPointers[0]).IsEqualTo(ptr);
}

[Test]
public async Task EnsureCorrectCount()
{
var expectedCount = Math.Min(NativeStringManager.DefaultCapacity, 10);
using var manager = new NativeStringManager();
for(var i=0; i < expectedCount; ++i)
{
_ = manager.GetNativeUtf8FromString($"test{i}");
}
await Assert.That(manager.CurrentCount).IsEqualTo(expectedCount);
await Assert.That(manager.DebugNativeStringPointers.Length).IsEqualTo(expectedCount);
await Assert.That(manager.NativeStringPointersArray).IsNotNull();
await Assert.That(manager.NativeStringPointersArray[expectedCount - 1]).IsNotEqualTo(IntPtr.Zero);
await Assert.That(manager.NativeStringPointersArray[expectedCount]).IsEqualTo(IntPtr.Zero);
}

[Test]
[Arguments(16)]
[Arguments(64)]
[Arguments(256)]
[Arguments(1024)]
public async Task EnsureContainsAllPointers(int pointerCount)
{
var pointers = new List<IntPtr>(pointerCount);
using var manager = new NativeStringManager();
for(var i=0; i < pointerCount; ++i)
{
var ptr = manager.GetNativeUtf8FromString($"test{i}");
pointers.Add(ptr);
}

await Assert.That(manager.DebugNativeStringPointers.Length).IsEqualTo(pointers.Count);

var managerPointers = manager.DebugNativeStringPointers.ToArray();
await Assert.That(managerPointers).IsEquivalentTo(pointers);
}

[Test]
[Arguments(1)]
[Arguments(16)]
[Arguments(128)]
[Arguments(1024)]
public async Task EnsureDispose(int pointerCount)
{
var manager = new NativeStringManager();
var pointers = new List<IntPtr>(pointerCount);
for(var i=0; i < pointerCount; ++i)
{
var ptr = manager.GetNativeUtf8FromString($"test{i}");
pointers.Add(ptr);
}
await Assert.That(manager.CurrentCount).IsEqualTo(pointerCount);

manager.Dispose();
await Assert.That(manager.CurrentCount).IsEqualTo(0);
await Assert.That(manager.NativeStringPointersArray).IsNull();
await Assert.That(manager.DisposedPointers).IsEquivalentTo(pointers);
}
}
#endif
82 changes: 82 additions & 0 deletions src/typstsharp/NativeStringManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Buffers;
using System.Runtime.InteropServices;

namespace typstsharp;

public sealed class NativeStringManager : IDisposable
{
public const int DefaultCapacity = 64;

private IntPtr[]? _nativeStringPointers;
private int _currentIndex = 0;

public NativeStringManager()
{
_nativeStringPointers = ArrayPool<IntPtr>.Shared.Rent(DefaultCapacity);
_nativeStringPointers.AsSpan().Clear();
}

public IntPtr GetNativeUtf8FromString(string? str)
{
var ptr = Marshal.StringToCoTaskMemUTF8(str);
AddPointer(ptr);

return ptr;
}

private void AddPointer(IntPtr ptr)
{
if (_nativeStringPointers is null)
{
throw new ObjectDisposedException(nameof(NativeStringManager));
}

if (_currentIndex >= _nativeStringPointers.Length)
{
var tempArray = _nativeStringPointers;
_nativeStringPointers = ArrayPool<IntPtr>.Shared.Rent(_nativeStringPointers.Length * 2);
_nativeStringPointers.AsSpan().Clear();
tempArray.AsSpan(0, _currentIndex).CopyTo(_nativeStringPointers);

ArrayPool<IntPtr>.Shared.Return(tempArray);
}

_nativeStringPointers[_currentIndex++] = ptr;
}

public void Dispose()
{
InternalDispose();
GC.SuppressFinalize(this);
}

private void InternalDispose()
{
if (_nativeStringPointers is { Length: > 0 } && _currentIndex > 0)
{
for (var i = 0; i < _currentIndex; i++)
{
Marshal.FreeCoTaskMem(_nativeStringPointers[i]);
#if DEBUG
DisposedPointers.Add(_nativeStringPointers[i]);
#endif
}

ArrayPool<IntPtr>.Shared.Return(_nativeStringPointers);
_currentIndex = 0;
_nativeStringPointers = null;
}
}

~NativeStringManager()
{
InternalDispose();
}

#if DEBUG
public List<IntPtr> DisposedPointers { get; } = new();
public int CurrentCount => _currentIndex;
public ReadOnlySpan<IntPtr> DebugNativeStringPointers => _nativeStringPointers.AsSpan(0, _currentIndex);
public IntPtr[]? NativeStringPointersArray => _nativeStringPointers;
#endif
}
Loading