diff --git a/cstrings/nsstrings_darwin.go b/cstrings/nsstrings_darwin.go new file mode 100644 index 00000000..d8835bc7 --- /dev/null +++ b/cstrings/nsstrings_darwin.go @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +package cstrings + +import ( + "fmt" + + "github.com/ebitengine/purego" + "github.com/ebitengine/purego/internal/strings" + "github.com/ebitengine/purego/objc" +) + +var ( + sel_isKindOf objc.SEL + sel_UTF8String objc.SEL + + class_NSString objc.Class +) + +func init() { + // Must pull in Foundation to get the NSString class. + _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW) + if err != nil { + panic(fmt.Errorf("cstrings: %w", err)) + } + sel_isKindOf = objc.RegisterName("isKindOfClass:") + sel_UTF8String = objc.RegisterName("UTF8String") + class_NSString = objc.GetClass("NSString") +} + +// NSStringToString returns a copy of the NSString contents as a Go string. +// If the ID is 0 then an empty string is returned. +// If the ID is not an NSString class then the function panics. +// This function is only available on darwin. +func NSStringToString(str objc.ID) string { + if str == 0 { + return "" + } + if str.Send(sel_isKindOf, class_NSString) == 0 { + panic("cstrings: provided ID is not an NSString") + } + return strings.GoString(uintptr(str.Send(sel_UTF8String))) +} diff --git a/cstrings/nsstrings_darwin_test.go b/cstrings/nsstrings_darwin_test.go new file mode 100644 index 00000000..df42ea36 --- /dev/null +++ b/cstrings/nsstrings_darwin_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +package cstrings_test + +import ( + "testing" + + "github.com/ebitengine/purego/cstrings" + "github.com/ebitengine/purego/objc" +) + +func TestNSStringToString(t *testing.T) { + t.Run("nil ID returns empty string", func(t *testing.T) { + result := cstrings.NSStringToString(0) + if result != "" { + t.Errorf("expected empty string for nil ID, got %q", result) + } + }) + + t.Run("valid NSString returns correct string", func(t *testing.T) { + sel := objc.RegisterName("stringWithUTF8String:") + nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "Hello, World!\x00") + + result := cstrings.NSStringToString(nsString) + if result != "Hello, World!" { + t.Errorf("expected %q, got %q", "Hello, World!", result) + } + }) + + t.Run("empty NSString returns empty string", func(t *testing.T) { + sel := objc.RegisterName("stringWithUTF8String:") + nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "\x00") + + result := cstrings.NSStringToString(nsString) + if result != "" { + t.Errorf("expected empty string, got %q", result) + } + }) + + t.Run("non-NSString panics", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic for non-NSString ID") + } + }() + + classNSNumber := objc.GetClass("NSNumber") + sel := objc.RegisterName("numberWithInt:") + nsNumber := objc.ID(classNSNumber).Send(sel, 42) + + cstrings.NSStringToString(nsNumber) + }) +}