From 63faf3fd2d02b40d1643692238ce13d094bb85bc Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Thu, 12 Mar 2026 19:43:02 -0400 Subject: [PATCH 1/7] nsstring --- objc/objc_runtime_darwin.go | 31 ++++++++++++++++++++++ objc/objc_runtime_darwin_test.go | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index 82d4632a..0092ef30 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -707,3 +707,34 @@ func clone[S ~[]E, E any](s S) S { // zero-length slice of a large array; see https://go.dev/issue/68488. return append(S{}, s...) } + +var ( + selIsKindOf SEL + selUTF8String SEL + + classNSString 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("objc: %w", err)) + } + selIsKindOf = RegisterName("isKindOfClass:") + selUTF8String = RegisterName("UTF8String") + classNSString = 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. +func NSStringToString(str ID) string { + if str == 0 { + return "" + } + if str.Send(selIsKindOf, classNSString) == 0 { + panic("objc: provided ID is not an NSString") + } + return strings.GoString(uintptr(str.Send(selUTF8String))) +} diff --git a/objc/objc_runtime_darwin_test.go b/objc/objc_runtime_darwin_test.go index 1e73bfb2..7a603441 100644 --- a/objc/objc_runtime_darwin_test.go +++ b/objc/objc_runtime_darwin_test.go @@ -244,3 +244,47 @@ func ExampleAllocateProtocol() { // accessibilityElement TB,GisBar // isFoo B16@0:8 } + +func TestNSStringToString(t *testing.T) { + t.Run("nil ID returns empty string", func(t *testing.T) { + result := objc.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 := objc.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 := objc.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") + } + }() + + // Use NSNumber instead of NSString + classNSNumber := objc.GetClass("NSNumber") + sel := objc.RegisterName("numberWithInt:") + nsNumber := objc.ID(classNSNumber).Send(sel, 42) + + objc.NSStringToString(nsNumber) + }) +} From 2b7b290fe66b02116b5bed0cbef4b5079259ff52 Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Sat, 9 May 2026 10:14:47 -0400 Subject: [PATCH 2/7] nsstrings: add NSStringToString Closes #438 --- cstrings/nsstrings_darwin.go | 41 +++++++++++++++++++++++++ cstrings/nsstrings_darwin_test.go | 51 +++++++++++++++++++++++++++++++ objc/objc_runtime_darwin.go | 31 ------------------- objc/objc_runtime_darwin_test.go | 44 -------------------------- 4 files changed, 92 insertions(+), 75 deletions(-) create mode 100644 cstrings/nsstrings_darwin.go create mode 100644 cstrings/nsstrings_darwin_test.go diff --git a/cstrings/nsstrings_darwin.go b/cstrings/nsstrings_darwin.go new file mode 100644 index 00000000..74c073d8 --- /dev/null +++ b/cstrings/nsstrings_darwin.go @@ -0,0 +1,41 @@ +package cstrings + +import ( + "fmt" + + "github.com/ebitengine/purego" + "github.com/ebitengine/purego/internal/strings" + "github.com/ebitengine/purego/objc" +) + +var ( + selIsKindOf objc.SEL + selUTF8String objc.SEL + + classNSString 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("nsstrings: %w", err)) + } + selIsKindOf = objc.RegisterName("isKindOfClass:") + selUTF8String = objc.RegisterName("UTF8String") + classNSString = 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(selIsKindOf, classNSString) == 0 { + panic("nsstrings: provided ID is not an NSString") + } + return strings.GoString(uintptr(str.Send(selUTF8String))) +} diff --git a/cstrings/nsstrings_darwin_test.go b/cstrings/nsstrings_darwin_test.go new file mode 100644 index 00000000..dcc47106 --- /dev/null +++ b/cstrings/nsstrings_darwin_test.go @@ -0,0 +1,51 @@ +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) + }) +} diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index 0092ef30..82d4632a 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -707,34 +707,3 @@ func clone[S ~[]E, E any](s S) S { // zero-length slice of a large array; see https://go.dev/issue/68488. return append(S{}, s...) } - -var ( - selIsKindOf SEL - selUTF8String SEL - - classNSString 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("objc: %w", err)) - } - selIsKindOf = RegisterName("isKindOfClass:") - selUTF8String = RegisterName("UTF8String") - classNSString = 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. -func NSStringToString(str ID) string { - if str == 0 { - return "" - } - if str.Send(selIsKindOf, classNSString) == 0 { - panic("objc: provided ID is not an NSString") - } - return strings.GoString(uintptr(str.Send(selUTF8String))) -} diff --git a/objc/objc_runtime_darwin_test.go b/objc/objc_runtime_darwin_test.go index 7a603441..1e73bfb2 100644 --- a/objc/objc_runtime_darwin_test.go +++ b/objc/objc_runtime_darwin_test.go @@ -244,47 +244,3 @@ func ExampleAllocateProtocol() { // accessibilityElement TB,GisBar // isFoo B16@0:8 } - -func TestNSStringToString(t *testing.T) { - t.Run("nil ID returns empty string", func(t *testing.T) { - result := objc.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 := objc.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 := objc.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") - } - }() - - // Use NSNumber instead of NSString - classNSNumber := objc.GetClass("NSNumber") - sel := objc.RegisterName("numberWithInt:") - nsNumber := objc.ID(classNSNumber).Send(sel, 42) - - objc.NSStringToString(nsNumber) - }) -} From 86c38469ab9ffacf89473a9c47fbda34351de8a8 Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Sat, 9 May 2026 10:23:07 -0400 Subject: [PATCH 3/7] fix panic package name --- cstrings/nsstrings_darwin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cstrings/nsstrings_darwin.go b/cstrings/nsstrings_darwin.go index 74c073d8..51d4aa90 100644 --- a/cstrings/nsstrings_darwin.go +++ b/cstrings/nsstrings_darwin.go @@ -19,7 +19,7 @@ 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("nsstrings: %w", err)) + panic(fmt.Errorf("cstrings: %w", err)) } selIsKindOf = objc.RegisterName("isKindOfClass:") selUTF8String = objc.RegisterName("UTF8String") @@ -35,7 +35,7 @@ func NSStringToString(str objc.ID) string { return "" } if str.Send(selIsKindOf, classNSString) == 0 { - panic("nsstrings: provided ID is not an NSString") + panic("cstrings: provided ID is not an NSString") } return strings.GoString(uintptr(str.Send(selUTF8String))) } From 618ead65abada37cef5b791f44dc201008928981 Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Sat, 9 May 2026 11:09:54 -0400 Subject: [PATCH 4/7] license --- cstrings/nsstrings_darwin.go | 3 +++ cstrings/nsstrings_darwin_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cstrings/nsstrings_darwin.go b/cstrings/nsstrings_darwin.go index 51d4aa90..95e5efbc 100644 --- a/cstrings/nsstrings_darwin.go +++ b/cstrings/nsstrings_darwin.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + package cstrings import ( diff --git a/cstrings/nsstrings_darwin_test.go b/cstrings/nsstrings_darwin_test.go index dcc47106..4b0bc17f 100644 --- a/cstrings/nsstrings_darwin_test.go +++ b/cstrings/nsstrings_darwin_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + package cstrings_test import ( From 4da8d9c3f161857dc5b4bd728ceb0bd421c29b87 Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Sat, 9 May 2026 11:12:47 -0400 Subject: [PATCH 5/7] fix year --- cstrings/nsstrings_darwin.go | 2 +- cstrings/nsstrings_darwin_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cstrings/nsstrings_darwin.go b/cstrings/nsstrings_darwin.go index 95e5efbc..660ee085 100644 --- a/cstrings/nsstrings_darwin.go +++ b/cstrings/nsstrings_darwin.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2025 The Ebitengine Authors +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors package cstrings diff --git a/cstrings/nsstrings_darwin_test.go b/cstrings/nsstrings_darwin_test.go index 4b0bc17f..df42ea36 100644 --- a/cstrings/nsstrings_darwin_test.go +++ b/cstrings/nsstrings_darwin_test.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2025 The Ebitengine Authors +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors package cstrings_test From d181d5351081c9d7595586db661cc5e88016e061 Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Sat, 9 May 2026 13:49:30 -0400 Subject: [PATCH 6/7] underscore in global variables --- cstrings/nsstrings_darwin.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cstrings/nsstrings_darwin.go b/cstrings/nsstrings_darwin.go index 660ee085..2683c30a 100644 --- a/cstrings/nsstrings_darwin.go +++ b/cstrings/nsstrings_darwin.go @@ -12,10 +12,10 @@ import ( ) var ( - selIsKindOf objc.SEL - selUTF8String objc.SEL + sel_IsKindOf objc.SEL + sel_UTF8String objc.SEL - classNSString objc.Class + class_NSString objc.Class ) func init() { @@ -24,9 +24,9 @@ func init() { if err != nil { panic(fmt.Errorf("cstrings: %w", err)) } - selIsKindOf = objc.RegisterName("isKindOfClass:") - selUTF8String = objc.RegisterName("UTF8String") - classNSString = objc.GetClass("NSString") + 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. @@ -37,8 +37,8 @@ func NSStringToString(str objc.ID) string { if str == 0 { return "" } - if str.Send(selIsKindOf, classNSString) == 0 { + if str.Send(sel_IsKindOf, class_NSString) == 0 { panic("cstrings: provided ID is not an NSString") } - return strings.GoString(uintptr(str.Send(selUTF8String))) + return strings.GoString(uintptr(str.Send(sel_UTF8String))) } From b61728dbe0a6ad31627a0f23b023fe3fbabf4df1 Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Sat, 9 May 2026 14:51:35 -0400 Subject: [PATCH 7/7] rename isKindOf --- cstrings/nsstrings_darwin.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cstrings/nsstrings_darwin.go b/cstrings/nsstrings_darwin.go index 2683c30a..d8835bc7 100644 --- a/cstrings/nsstrings_darwin.go +++ b/cstrings/nsstrings_darwin.go @@ -12,7 +12,7 @@ import ( ) var ( - sel_IsKindOf objc.SEL + sel_isKindOf objc.SEL sel_UTF8String objc.SEL class_NSString objc.Class @@ -24,7 +24,7 @@ func init() { if err != nil { panic(fmt.Errorf("cstrings: %w", err)) } - sel_IsKindOf = objc.RegisterName("isKindOfClass:") + sel_isKindOf = objc.RegisterName("isKindOfClass:") sel_UTF8String = objc.RegisterName("UTF8String") class_NSString = objc.GetClass("NSString") } @@ -37,7 +37,7 @@ func NSStringToString(str objc.ID) string { if str == 0 { return "" } - if str.Send(sel_IsKindOf, class_NSString) == 0 { + if str.Send(sel_isKindOf, class_NSString) == 0 { panic("cstrings: provided ID is not an NSString") } return strings.GoString(uintptr(str.Send(sel_UTF8String)))