diff --git a/SDL3-CS.SourceGeneration/Changes.cs b/SDL3-CS.SourceGeneration/Changes.cs index 02a70eb..e564da3 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 SDLUtf8String. /// - 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..c8a2935 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( @@ -112,7 +112,10 @@ using System; param.Identifier.ValueText + pointer_suffix) .WithInitializer( SyntaxFactory.EqualsValueClause( - SyntaxFactory.IdentifierName(param.Identifier))))), + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(param.Identifier), + SyntaxFactory.IdentifierName(Helper.Utf8StringReadOnlySpanFieldName)))))), expr); } @@ -161,7 +164,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..65d84f9 100644 --- a/SDL3-CS.SourceGeneration/Helper.cs +++ b/SDL3-CS.SourceGeneration/Helper.cs @@ -16,6 +16,10 @@ namespace SDL3.SourceGeneration /// public const string UnsafePrefix = "Unsafe_"; + public const string Utf8StringStructName = "Utf8String"; + + public const string Utf8StringReadOnlySpanFieldName = "Raw"; + 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..d8db8dc 100644 --- a/SDL3-CS.Tests/Program.cs +++ b/SDL3-CS.Tests/Program.cs @@ -31,6 +31,7 @@ namespace SDL3.Tests 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/TestUtf8String.cs b/SDL3-CS.Tests/TestUtf8String.cs new file mode 100644 index 0000000..f4ddd51 --- /dev/null +++ b/SDL3-CS.Tests/TestUtf8String.cs @@ -0,0 +1,87 @@ +// 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 TestNullImplicitConversion() + { + checkNull(null); + checkNull(default); + } + + [TestCase(null, -1)] + [TestCase("", 1)] + [TestCase("\0", 1)] + [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.Raw) + { + 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.Raw) + { + Assert.That(ptr != null, "ptr != null"); + Assert.That(ptr[s.Raw.Length - 1], Is.EqualTo(0)); + } + } + } +} 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..56b8d74 --- /dev/null +++ b/SDL3-CS/Utf8String.cs @@ -0,0 +1,52 @@ +// 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.Runtime.CompilerServices; +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. + public readonly ref struct Utf8String + { + public readonly ReadOnlySpan Raw; + + private Utf8String(ReadOnlySpan raw) + { + Raw = raw; + } + + public static implicit operator Utf8String(string? str) + { + if (str == null) + return new Utf8String(null); + + return new Utf8String(Encoding.UTF8.GetBytes(ensureTrailingNull(str))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ensureTrailingNull(string str) => str.EndsWith('\0') ? str : 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); + } + } +}