diff --git a/.editorconfig b/.editorconfig
index a5f783f..e971c4b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,7 +9,7 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace = true
-[*g.cs]
+[*.g.cs]
generated_code = true
[*.cs]
diff --git a/.idea/.idea.SDL3-CS.Desktop/.idea/.gitignore b/.idea/.idea.SDL3-CS.Desktop/.idea/.gitignore
new file mode 100644
index 0000000..090b233
--- /dev/null
+++ b/.idea/.idea.SDL3-CS.Desktop/.idea/.gitignore
@@ -0,0 +1,11 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/.idea.SDL3-CS.Desktop.iml
+/modules.xml
+/projectSettingsUpdater.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.SDL3-CS.Desktop/.idea/.name b/.idea/.idea.SDL3-CS.Desktop/.idea/.name
new file mode 100644
index 0000000..216c862
--- /dev/null
+++ b/.idea/.idea.SDL3-CS.Desktop/.idea/.name
@@ -0,0 +1 @@
+SDL3-CS.Desktop
\ No newline at end of file
diff --git a/.idea/.idea.SDL3-CS.Desktop/.idea/encodings.xml b/.idea/.idea.SDL3-CS.Desktop/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.SDL3-CS.Desktop/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.SDL3-CS.Desktop/.idea/indexLayout.xml b/.idea/.idea.SDL3-CS.Desktop/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.SDL3-CS.Desktop/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SDL3-CS.SourceGeneration/Changes.cs b/SDL3-CS.SourceGeneration/Changes.cs
index 02a70eb..ce40fa3 100644
--- a/SDL3-CS.SourceGeneration/Changes.cs
+++ b/SDL3-CS.SourceGeneration/Changes.cs
@@ -11,9 +11,9 @@ namespace SDL3.SourceGeneration
None,
///
- /// Change const char* function parameters to ReadOnlySpan<byte>.
+ /// Change const char* function parameters to .
///
- ChangeParamsToReadOnlySpan = 1 << 0,
+ ChangeParamsToUtf8String = 1 << 0,
///
/// Change char * or const char * return type to .
diff --git a/SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs b/SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs
index 0830bd1..263488c 100644
--- a/SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs
+++ b/SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs
@@ -83,8 +83,8 @@ using System;
{
if (param.IsTypeConstCharPtr())
{
- Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToReadOnlySpan));
- yield return param.WithType(SyntaxFactory.ParseTypeName("ReadOnlySpan"))
+ Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToUtf8String));
+ yield return param.WithType(SyntaxFactory.ParseTypeName(Helper.Utf8StringStructName))
.WithAttributeLists(SyntaxFactory.List());
}
else
@@ -102,7 +102,7 @@ using System;
foreach (var param in gm.NativeMethod.ParameterList.Parameters.Where(p => p.IsTypeConstCharPtr()).Reverse())
{
- Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToReadOnlySpan));
+ Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToUtf8String));
expr = SyntaxFactory.FixedStatement(
SyntaxFactory.VariableDeclaration(
@@ -161,7 +161,7 @@ using System;
{
if (param.IsTypeConstCharPtr())
{
- Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToReadOnlySpan));
+ Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToUtf8String));
yield return SyntaxFactory.Argument(SyntaxFactory.IdentifierName(param.Identifier.ValueText + pointer_suffix));
}
else
diff --git a/SDL3-CS.SourceGeneration/Helper.cs b/SDL3-CS.SourceGeneration/Helper.cs
index 2eb2dad..42e51d5 100644
--- a/SDL3-CS.SourceGeneration/Helper.cs
+++ b/SDL3-CS.SourceGeneration/Helper.cs
@@ -16,6 +16,8 @@ namespace SDL3.SourceGeneration
///
public const string UnsafePrefix = "Unsafe_";
+ public const string Utf8StringStructName = "Utf8String";
+
public static bool IsVoid(this TypeSyntax type) => type is PredefinedTypeSyntax predefined
&& predefined.Keyword.IsKind(SyntaxKind.VoidKeyword);
diff --git a/SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs b/SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs
index 944fa3c..81f40bc 100644
--- a/SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs
+++ b/SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs
@@ -42,7 +42,7 @@ namespace SDL3.SourceGeneration
foreach (var parameter in method.ParameterList.Parameters)
{
if (parameter.IsTypeConstCharPtr())
- changes |= Changes.ChangeParamsToReadOnlySpan;
+ changes |= Changes.ChangeParamsToUtf8String;
}
if (changes != Changes.None)
diff --git a/SDL3-CS.Tests/Program.cs b/SDL3-CS.Tests/Program.cs
index 609d1fe..3eeccb8 100644
--- a/SDL3-CS.Tests/Program.cs
+++ b/SDL3-CS.Tests/Program.cs
@@ -14,23 +14,11 @@ namespace SDL3.Tests
{
Console.OutputEncoding = Encoding.UTF8;
- unsafe
- {
- // Encoding.UTF8.GetBytes can churn out null pointers and doesn't guarantee null termination
- fixed (byte* badPointer = Encoding.UTF8.GetBytes(""))
- Debug.Assert(badPointer == null);
-
- fixed (byte* pointer = UTF8GetBytes(""))
- {
- Debug.Assert(pointer != null);
- Debug.Assert(pointer[0] == '\0');
- }
- }
-
SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "null byte \0 in string"u8);
Debug.Assert(SDL_GetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4) == "null byte ");
SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "1"u8);
+ SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "1");
using (var window = new MyWindow())
{
diff --git a/SDL3-CS.Tests/SDL3-CS.Tests.csproj b/SDL3-CS.Tests/SDL3-CS.Tests.csproj
index 0fe8dc8..f38b831 100644
--- a/SDL3-CS.Tests/SDL3-CS.Tests.csproj
+++ b/SDL3-CS.Tests/SDL3-CS.Tests.csproj
@@ -7,8 +7,15 @@
enable
enable
true
+ false
+
+
+
+
+
+
diff --git a/SDL3-CS.Tests/TestUtf8String.cs b/SDL3-CS.Tests/TestUtf8String.cs
new file mode 100644
index 0000000..b879568
--- /dev/null
+++ b/SDL3-CS.Tests/TestUtf8String.cs
@@ -0,0 +1,89 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using SDL;
+
+namespace SDL3.Tests
+{
+ [TestFixture]
+ public class TestUtf8String
+ {
+ [Test]
+ public void TestNoImplicitConversion()
+ {
+ checkNull(null);
+ checkNull(default);
+ checkNull(new Utf8String()); // don't do this in actual code
+ }
+
+ [TestCase(null, -1)]
+ [TestCase("", 1)]
+ [TestCase("\0", 1)]
+ [TestCase("test", 5)]
+ [TestCase("test\0", 5)]
+ [TestCase("test\0test", 10)]
+ [TestCase("test\0test\0", 10)]
+ public static void TestString(string? str, int expectedLength)
+ {
+ if (str == null)
+ checkNull(str);
+ else
+ check(str, expectedLength);
+ }
+
+ [Test]
+ public static void TestNullSpan()
+ {
+ ReadOnlySpan span = null;
+ checkNull(span);
+ }
+
+ [Test]
+ public static void TestDefaultSpan()
+ {
+ ReadOnlySpan span = default;
+ checkNull(span);
+ }
+
+ [Test]
+ public static void TestNewSpan()
+ {
+ ReadOnlySpan span = new ReadOnlySpan();
+ checkNull(span);
+ }
+
+ [Test]
+ public static void TestReadOnlySpan()
+ {
+ check(""u8, 1);
+ check("\0"u8, 1);
+ check("test"u8, 5);
+ check("test\0"u8, 5);
+ check("test\0test"u8, 10);
+ check("test\0test\0"u8, 10);
+ }
+
+ private static unsafe void checkNull(Utf8String s)
+ {
+ Assert.That(s.Raw == null, "s.Raw == null");
+ Assert.That(s.Raw.Length, Is.EqualTo(0));
+
+ fixed (byte* ptr = s)
+ {
+ Assert.That(ptr == null, "ptr == null");
+ }
+ }
+
+ private static unsafe void check(Utf8String s, int expectedLength)
+ {
+ Assert.That(s.Raw.Length, Is.EqualTo(expectedLength));
+
+ fixed (byte* ptr = s)
+ {
+ Assert.That(ptr != null, "ptr != null");
+ Assert.That(ptr[s.Raw.Length - 1], Is.EqualTo(0));
+ }
+ }
+ }
+}
diff --git a/SDL3-CS/Properties/AssemblyInfo.cs b/SDL3-CS/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4fabb01
--- /dev/null
+++ b/SDL3-CS/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("SDL3-CS.Tests")]
diff --git a/SDL3-CS/SDL3.cs b/SDL3-CS/SDL3.cs
index bf00f9f..5b46e55 100644
--- a/SDL3-CS/SDL3.cs
+++ b/SDL3-CS/SDL3.cs
@@ -2,9 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Diagnostics;
using System.Runtime.InteropServices;
-using System.Text;
namespace SDL
{
@@ -27,17 +25,5 @@ namespace SDL
return s;
}
-
- ///
- /// UTF8 encodes a managed string to a byte array suitable for use in ReadOnlySpan<byte> parameters of SDL functions.
- ///
- /// The string to encode.
- /// A null-terminated byte array.
- public static byte[] UTF8GetBytes(string s)
- {
- byte[] array = Encoding.UTF8.GetBytes(s + '\0');
- Debug.Assert(array[^1] == '\0');
- return array;
- }
}
}
diff --git a/SDL3-CS/SDL3/SDL_log.cs b/SDL3-CS/SDL3/SDL_log.cs
index dd19dc4..70ab926 100644
--- a/SDL3-CS/SDL3/SDL_log.cs
+++ b/SDL3-CS/SDL3/SDL_log.cs
@@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Runtime.CompilerServices;
-
namespace SDL
{
- public static partial class SDL3
+ public static partial class SDL3
{
- public static void SDL_LogSetPriority(SDL_LogCategory category, SDL_LogPriority priority) => SDL_LogSetPriority((int)category, priority);
+ public static void SDL_LogSetPriority(SDL_LogCategory category, SDL_LogPriority priority) => SDL_LogSetPriority((int)category, priority);
public static SDL_LogPriority SDL_LogGetPriority(SDL_LogCategory category) => SDL_LogGetPriority((int)category);
}
}
diff --git a/SDL3-CS/SDL3/SDL_messagebox.cs b/SDL3-CS/SDL3/SDL_messagebox.cs
index 764cbbc..e4b79eb 100644
--- a/SDL3-CS/SDL3/SDL_messagebox.cs
+++ b/SDL3-CS/SDL3/SDL_messagebox.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-
namespace SDL
{
public partial struct SDL_MessageBoxButtonData
@@ -18,7 +16,7 @@ namespace SDL
public static partial class SDL3
{
// public static int SDL_ShowSimpleMessageBox([NativeTypeName("Uint32")] uint flags, [NativeTypeName("const char *")] byte* title, [NativeTypeName("const char *")] byte* message, SDL_Window* window);
- public static unsafe int SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags flags, ReadOnlySpan title, ReadOnlySpan message, SDL_Window* window)
+ public static unsafe int SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags flags, Utf8String title, Utf8String message, SDL_Window* window)
=> SDL_ShowSimpleMessageBox((uint)flags, title, message, window);
}
}
diff --git a/SDL3-CS/SDL3/SDL_render.cs b/SDL3-CS/SDL3/SDL_render.cs
index 896953b..ed2e04d 100644
--- a/SDL3-CS/SDL3/SDL_render.cs
+++ b/SDL3-CS/SDL3/SDL_render.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-
namespace SDL
{
public partial struct SDL_RendererInfo
@@ -12,7 +10,7 @@ namespace SDL
public static partial class SDL3
{
- public static unsafe SDL_Renderer* SDL_CreateRenderer(SDL_Window* window, ReadOnlySpan name, SDL_RendererFlags flags)
+ public static unsafe SDL_Renderer* SDL_CreateRenderer(SDL_Window* window, Utf8String name, SDL_RendererFlags flags)
=> SDL_CreateRenderer(window, name, (uint)flags);
}
}
diff --git a/SDL3-CS/Utf8String.cs b/SDL3-CS/Utf8String.cs
new file mode 100644
index 0000000..832b44f
--- /dev/null
+++ b/SDL3-CS/Utf8String.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Text;
+
+namespace SDL
+{
+ ///
+ /// Null pointer or a null-byte terminated UTF8 string suitable for use in native methods.
+ ///
+ /// Should only be instantiated through implicit conversions or with null.
+ public readonly ref struct Utf8String
+ {
+ internal readonly ReadOnlySpan Raw;
+
+ private Utf8String(ReadOnlySpan raw)
+ {
+ Raw = raw;
+ }
+
+ public static implicit operator Utf8String(string? str)
+ {
+ if (str == null)
+ return new Utf8String(null);
+
+ if (str.EndsWith('\0'))
+ return new Utf8String(Encoding.UTF8.GetBytes(str));
+
+ return new Utf8String(Encoding.UTF8.GetBytes(str + '\0'));
+ }
+
+ public static implicit operator Utf8String(ReadOnlySpan raw)
+ {
+ if (raw == null)
+ return new Utf8String(null);
+
+ if (raw.Length == 0)
+ return new Utf8String(new ReadOnlySpan([0]));
+
+ if (raw[^1] != 0)
+ {
+ byte[] copy = new byte[raw.Length + 1];
+ raw.CopyTo(copy);
+ raw = copy;
+ }
+
+ return new Utf8String(raw);
+ }
+
+ internal ref readonly byte GetPinnableReference() => ref Raw.GetPinnableReference();
+ }
+}