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(); + } +}