Skip to content

Commit

Permalink
Merge pull request #37 from Susko3/add-Utf8String-helper
Browse files Browse the repository at this point in the history
Add `Utf8String` helper for safely passing in strings to native methods
  • Loading branch information
smoogipoo committed Apr 17, 2024
2 parents ac582bd + c40e8b1 commit f07bc5b
Show file tree
Hide file tree
Showing 18 changed files with 194 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*g.cs]
[*.g.cs]
generated_code = true

[*.cs]
Expand Down
11 changes: 11 additions & 0 deletions .idea/.idea.SDL3-CS.Desktop/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.idea.SDL3-CS.Desktop/.idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.SDL3-CS.Desktop/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.SDL3-CS.Desktop/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions SDL3-CS.SourceGeneration/Changes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ public enum Changes
None,

/// <summary>
/// Change <c>const char*</c> function parameters to <c>ReadOnlySpan&lt;byte&gt;</c>.
/// Change <c>const char*</c> function parameters to <see cref="Helper.Utf8StringStructName"/>.
/// </summary>
ChangeParamsToReadOnlySpan = 1 << 0,
ChangeParamsToUtf8String = 1 << 0,

/// <summary>
/// Change <c>char *</c> or <c>const char *</c> return type to <see cref="string"/>.
Expand Down
8 changes: 4 additions & 4 deletions SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ private static IEnumerable<ParameterSyntax> transformParams(GeneratedMethod gm)
{
if (param.IsTypeConstCharPtr())
{
Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToReadOnlySpan));
yield return param.WithType(SyntaxFactory.ParseTypeName("ReadOnlySpan<byte>"))
Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToUtf8String));
yield return param.WithType(SyntaxFactory.ParseTypeName(Helper.Utf8StringStructName))
.WithAttributeLists(SyntaxFactory.List<AttributeListSyntax>());
}
else
Expand All @@ -102,7 +102,7 @@ private static StatementSyntax makeMethodBody(GeneratedMethod gm)

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(
Expand Down Expand Up @@ -161,7 +161,7 @@ private static IEnumerable<ArgumentSyntax> makeArguments(GeneratedMethod gm)
{
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
Expand Down
2 changes: 2 additions & 0 deletions SDL3-CS.SourceGeneration/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public static class Helper
/// </remarks>
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);

Expand Down
2 changes: 1 addition & 1 deletion SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
foreach (var parameter in method.ParameterList.Parameters)
{
if (parameter.IsTypeConstCharPtr())
changes |= Changes.ChangeParamsToReadOnlySpan;
changes |= Changes.ChangeParamsToUtf8String;
}

if (changes != Changes.None)
Expand Down
14 changes: 1 addition & 13 deletions SDL3-CS.Tests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,11 @@ public static void Main()
{
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())
{
Expand Down
7 changes: 7 additions & 0 deletions SDL3-CS.Tests/SDL3-CS.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>

<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SDL3-CS\SDL3-CS.csproj"/>
</ItemGroup>
Expand Down
89 changes: 89 additions & 0 deletions SDL3-CS.Tests/TestUtf8String.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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<byte> span = null;
checkNull(span);
}

[Test]
public static void TestDefaultSpan()
{
ReadOnlySpan<byte> span = default;
checkNull(span);
}

[Test]
public static void TestNewSpan()
{
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>();
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));
}
}
}
}
6 changes: 6 additions & 0 deletions SDL3-CS/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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")]
14 changes: 0 additions & 14 deletions SDL3-CS/SDL3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -27,17 +25,5 @@ public static unsafe partial class SDL3

return s;
}

/// <summary>
/// UTF8 encodes a managed <c>string</c> to a <c>byte</c> array suitable for use in <c>ReadOnlySpan&lt;byte&gt;</c> parameters of SDL functions.
/// </summary>
/// <param name="s">The <c>string</c> to encode.</param>
/// <returns>A null-terminated byte array.</returns>
public static byte[] UTF8GetBytes(string s)
{
byte[] array = Encoding.UTF8.GetBytes(s + '\0');
Debug.Assert(array[^1] == '\0');
return array;
}
}
}
6 changes: 2 additions & 4 deletions SDL3-CS/SDL3/SDL_log.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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);
}
}
4 changes: 1 addition & 3 deletions SDL3-CS/SDL3/SDL_messagebox.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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
Expand All @@ -18,7 +16,7 @@ public partial struct SDL_MessageBoxData
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<byte> title, ReadOnlySpan<byte> 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);
}
}
4 changes: 1 addition & 3 deletions SDL3-CS/SDL3/SDL_render.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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
Expand All @@ -12,7 +10,7 @@ public partial struct SDL_RendererInfo

public static partial class SDL3
{
public static unsafe SDL_Renderer* SDL_CreateRenderer(SDL_Window* window, ReadOnlySpan<byte> 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);
}
}
53 changes: 53 additions & 0 deletions SDL3-CS/Utf8String.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Text;

namespace SDL
{
/// <summary>
/// Null pointer or a null-byte terminated UTF8 string suitable for use in native methods.
/// </summary>
/// <remarks>Should only be instantiated through implicit conversions or with <c>null</c>.</remarks>
public readonly ref struct Utf8String
{
internal readonly ReadOnlySpan<byte> Raw;

private Utf8String(ReadOnlySpan<byte> 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<byte> raw)
{
if (raw == null)
return new Utf8String(null);

if (raw.Length == 0)
return new Utf8String(new ReadOnlySpan<byte>([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();
}
}

0 comments on commit f07bc5b

Please sign in to comment.