Skip to content

Commit 363adef

Browse files
authored
improve docs and APIs (#180)
* objc: docs and api tweaks. add Retain * dispatch: docs and api tweaks * macos: update examples and helpers to api tweaks * Makefile: use helloworld as main example * docs: start basic docs
1 parent b2dbbae commit 363adef

27 files changed

+185
-77
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ clobber:
99
.PHONY: clobber
1010

1111
example:
12-
$(GOEXE) run ./macos/_examples/widgets/main.go
12+
$(GOEXE) run ./macos/_examples/helloworld/main.go
1313
.PHONY: example
1414

1515
generate/symbols.zip:

README.md

+15-9
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ Native Apple APIs for Golang!
88
<a href="https://github.com/progrium/macdriver/discussions" title="Project Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Project Forum"></a>
99
<a href="https://github.com/sponsors/progrium" title="Sponsor Project"><img src="https://img.shields.io/static/v1?label=sponsor&message=%E2%9D%A4&logo=GitHub" alt="Sponsor Project" /></a>
1010

11+
> [!IMPORTANT]
1112
> Aug 15, 2023: **MacDriver is becoming DarwinKit**, which increases API coverage by an order of magnitude and is an overall upgrade in quality and scope. It has been rewritten, reorganized, and definitely has breaking API changes. The [legacy branch](https://github.com/progrium/macdriver/tree/legacy) and [previous releases](https://github.com/progrium/macdriver/releases) are still available for existing code to work against. We're working towards a [0.5.0-preview release](https://github.com/progrium/macdriver/issues/177) followed by a [0.5.0 release](https://github.com/progrium/macdriver/milestone/4) finalizing the new API and rename to DarwinKit.
1213
1314
------
1415

15-
DarwinKit lets you work with [supported Apple frameworks](https://pkg.go.dev/github.com/progrium/macdriver/macos@main#section-directories) and build native applications using Go. It makes developing simple applications simple. With XCode and Go 1.18+ installed, you can write this program in a `main.go` file:
16+
DarwinKit lets you work with [supported Apple frameworks](https://pkg.go.dev/github.com/progrium/macdriver/macos@main#section-directories) and build native applications using Go. With XCode and Go 1.18+ installed, you can write this program in a `main.go` file:
1617

1718
```go
1819
package main
@@ -27,11 +28,13 @@ import (
2728
)
2829

2930
func init() {
31+
// ensure main is run on the startup thread
3032
runtime.LockOSThread()
3133
}
3234

3335
func main() {
34-
macos.Launch(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
36+
// runs macOS application event loop with a callback on success
37+
macos.RunApp(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
3538
app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular)
3639
app.ActivateIgnoringOtherApps(true)
3740

@@ -74,9 +77,9 @@ Although currently outside the scope of this project, if you wanted you could pu
7477

7578
* You still need to know or learn how Apple frameworks work, so you'll have to use Apple documentation and understand how to translate Objective-C example code to the equivalent Go with DarwinKit.
7679
* Your programs link against the actual Apple frameworks, so XCode needs to be installed for the framework headers and any program built will use [cgo](https://pkg.go.dev/cmd/cgo).
80+
* You will be using two memory management systems. Framework objects are managed by the [Objective-C memory manager](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011-SW1), so be sure to read our docs on [memory management](docs/memorymanagement.md).
7781
* Exceptions in frameworks will segfault, giving you both an Objective-C stacktrace and a Go panic stacktrace. You will be debugging a hybrid Go and Objective-C program.
78-
* Goroutines that interact with GUI objects need to dispatch operations on the main thread otherwise it will segfault.
79-
* You will be using two memory management systems. Framework objects are managed by the [Objective-C memory manager](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011-SW1) and you will use Retain, Release, or Autorelease on them on top of considering Go memory management.
82+
* Goroutines that interact with GUI objects need to [dispatch](https://pkg.go.dev/github.com/progrium/macdriver@main/dispatch) operations on the main thread otherwise it will segfault.
8083
* This is all tenable for simple programs, but these are the reasons we don't *recommend* large/complex programs using DarwinKit.
8184

8285
## Examples
@@ -90,13 +93,16 @@ Although currently outside the scope of this project, if you wanted you could pu
9093

9194
## How it works
9295

93-
After acquiring NeXT Computer in the 90s, Apple used [NeXTSTEP](https://en.wikipedia.org/wiki/NeXTSTEP) as the basis of their software stack, which was written in Objective-C. Unlike most systems languages with object orientation, especially of the C lineage, Objective-C implements OOP as a runtime library. The weird OOP specific *syntax* is effectively rewritten into C calls to [libobjc](https://developer.apple.com/documentation/objectivec/objective-c_runtime?language=objc), which is a normal C library implementing the Objective-C runtime. This runtime could be used to bring OOP to any language that can make calls to C code. It also lets you interact with objects and classes registered by other libraries, such as the Apple frameworks.
96+
<details>
97+
<summary>Brief background on Objective-C</summary>
98+
Ever since acquiring NeXT Computer in the 90s, Apple has used [NeXTSTEP](https://en.wikipedia.org/wiki/NeXTSTEP) as the basis of their software stack, which is written in Objective-C. Unlike most systems languages with object orientation, Objective-C implements OOP as a runtime library. In fact, Objective-C is just C with the weird OOP specific syntax rewritten into C calls to [libobjc](https://developer.apple.com/documentation/objectivec/objective-c_runtime?language=objc), which is a normal C library implementing an object runtime. This runtime could be used to bring OOP to any language that can make calls to C code. It also lets you interact with objects and classes registered by other libraries, such as the Apple frameworks.
99+
</details>
94100

95101
At the heart of DarwinKit is a package wrapping the Objective-C runtime using cgo and libffi. This is actually all you need to interact with Objective-C objects and classes, it'll just look like this:
96102

97103
```go
98-
app := objc.CallMethod[objc.Object](objc.GetClass("NSApplication"), objc.Sel("sharedApplication"))
99-
objc.CallMethod[objc.Void](app, objc.Sel("run"))
104+
app := objc.Call[objc.Object](objc.GetClass("NSApplication"), objc.Sel("sharedApplication"))
105+
objc.Call[objc.Void](app, objc.Sel("run"))
100106
```
101107

102108
So we wrap these calls in a Go API that lets us write code like this:
@@ -110,12 +116,12 @@ These bindings are great, but we need to define them for every API we want to us
110116
Apple has around 200 frameworks of nearly 5000 classes with 77k combined methods and properties. Not to
111117
mention all the constants, functions, structs, unions, and enums we need to work with those objects.
112118

113-
So DarwinKit generates its bindings. This is the hard part. Making sure the generation pipeline accurately produces usable bindings for all possible symbols is quite an arduous, iterative, manual process. Then since we're moving symbols that lived in a single namespace into Go packages, we have to manually decouple dependencies between them enough to avoid circular imports. If you want to help add frameworks, this whole process is documented here.
119+
So DarwinKit generates its bindings. This is the hard part. Making sure the generation pipeline accurately produces usable bindings for all possible symbols is quite an arduous, iterative, manual process. Then since we're moving symbols that lived in a single namespace into Go packages, we have to manually decouple dependencies between them enough to avoid circular imports. If you want to help add frameworks, read our documentation on [generation](docs/generation.md).
114120

115121
Objects in Objective-C are passed around as typed pointer values. When we receive an object from a method
116122
call in Go, the `objc` package receives it as a pointer, which it first puts into an `unsafe.Pointer`. The
117123
bindings for a class define a struct type that embeds an `objc.Object` struct, which contains a single
118-
field to hold the `unsafe.Pointer`. So unless working with a primitive type, you're working with an `unsafe.Pointer` wrapped in an `objc.Object` wrapped in a struct type that has the methods for the class of the object of the pointer.
124+
field to hold the `unsafe.Pointer`. So unless working with a primitive type, you're working with an `unsafe.Pointer` wrapped in an `objc.Object` wrapped in a struct type that has the methods for the class of the object of the pointer. Be sure to read our documentation on [memory management](docs/memorymanagement.md).
119125

120126
If you have questions, feel free to ask in the [discussion forums](https://github.com/progrium/macdriver/discussions).
121127

dispatch/dispatch.go

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// Execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system.
2+
//
3+
// [Apple Documentation]
4+
//
5+
// [AppleDocumentation]: https://developer.apple.com/documentation/dispatch?language=objc
16
package dispatch
27

38
// #cgo CFLAGS: -x objective-c

dispatch/queue.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ const (
3030
QueuePriorityBackground QueuePriority = math.MinInt16
3131
)
3232

33-
func GetMainQueue() Queue {
33+
// Returns the serial dispatch queue associated with the application’s main thread. [Full Topic]
34+
//
35+
// [Full Topic]: https://developer.apple.com/documentation/dispatch/1452921-dispatch_get_main_queue?language=objc
36+
func MainQueue() Queue {
3437
p := C.Dispatch_Get_Main_Queue()
3538
return Queue{p}
3639
}
3740

38-
func GetGlobalQueue(identifier QueuePriority, flags uintptr) Queue {
41+
// Returns a system-defined global concurrent queue with the specified quality-of-service class. [Full Topic]
42+
//
43+
// [Full Topic]: https://developer.apple.com/documentation/dispatch/1452927-dispatch_get_global_queue?language=objc
44+
func GlobalQueue(identifier QueuePriority, flags uintptr) Queue {
3945
p := C.Dispatch_Get_Global_Queue(C.intptr_t(identifier), C.uintptr_t(flags))
4046
return Queue{p}
4147
}
@@ -48,11 +54,13 @@ func (q Queue) Release() {
4854
C.Dispatch_Release(q.ptr)
4955
}
5056

57+
// Submits a block for asynchronous execution on a dispatch queue and returns immediately.
5158
func (q Queue) DispatchAsync(task func()) {
5259
id := cgo.NewHandle(task)
5360
C.Dispatch_Async(q.ptr, C.uintptr_t(id))
5461
}
5562

63+
// Submits a block object for execution and returns after that block finishes executing.
5664
func (q Queue) DispatchSync(task func()) {
5765
id := cgo.NewHandle(task)
5866
C.Dispatch_Sync(q.ptr, C.uintptr_t(id))

docs/bindings.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Bindings API
2+
3+
* Frameworks have their own packages
4+
* Lowercase framework name (AppKit => appkit)
5+
* Lowercase prefix if super long (uniformtypeidentifiers => uti)
6+
* Symbol prefixes are removed (CGPoint => Point)
7+
* Constants and enums are 1:1
8+
* Extra k prefix is kept (kCGImageStatusInvalidData => KImageStatusInvalidData)
9+
* Classes (ex: NSWindow)
10+
* Unexported struct type for class (_WindowClass)
11+
* Variable for class singleton (WindowClass)
12+
* Interface for class prefixed with I (IWindow)
13+
* Embeds superclass interface (IView)
14+
* Struct for class (Window)
15+
* Embeds superclass struct (View)
16+
* Instance methods are 1:1
17+
* Selector names (setFrame:display: => SetFrameDisplay)
18+
* Arguments with protocols get alt methods with argument as object
19+
* ...
20+
* Longer overlapping selector gets _ suffix
21+
* reload => Reload
22+
* reload: => Reload_
23+
* New function (NewWindow)
24+
* alloc/init/autorelease
25+
* Init instance methods get New function variants
26+
* initWithFrame: => NewWindowWithFrame(...)
27+
* autorelease
28+
* Class methods are 1:1 on class type
29+
* Class methods get function variants
30+
* windowNumbersWithOptions: => Window_WindowNumbersWithOptions(...)
31+
* removeFrameUsingName: => Window_RemoveFrameUsingName(...)
32+
* Protocols (NSWindowDelegate)
33+
* ...

docs/generation.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Generation
2+
3+
TODO

docs/memorymanagement.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Memory Management
2+
3+
Working with Objective-C from Go requires understanding how [Objective-C memory management works](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011-SW1) and how DarwinKit facilitates the co-existance of the two memory management systems.
4+
5+
Objective-C uses reference counting or a "retain and release" model where objects are deallocated when their internal retain count reaches zero. When an object is allocated, its retain count is set to 1. It is the responsibility of the allocating code to release the object when finished, decrementing its retain count by 1. This would deallocate the object unless other code has retained the object, incrementing the retain count by 1. Every alloc or retain must have a subsequent release.
6+
7+
It should be noted that modern Objective-C and Swift code don't have to do this manual retain and release process because of Automatic Reference Counting, or ARC, which is a feature of their compiler that figures out where to insert retains and releases for you. Our code is compiled by Go so we don't get to take advantage of this, but we do have access to the underlying retain and release methods.
8+
9+
That said, even without ARC there is a way to ease retain and release by using autorelease pools. Along with retain and release methods on all objects, there is also an autorelease method. This marks the object for a deferred release, somewhat like using Go defer to cleanup a resource. Autoreleased objects will be released when the last created autorelease pool on the stack is drained.
10+
11+
In DarwinKit we have `objc.WithAutoreleasePool()` which takes a function that is immediately executed with a new autorelease pool that is drained when the function is finished. In other words, any objects that have autorelease called from the given function will get released after the function returns. This is equivalent to the `@autoreleasepool` [block syntax](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html#//apple_ref/doc/uid/20000047-CJBFBEDI) in Objective-C.
12+
13+
Luckily, most of the code we write using Apple frameworks lives in delegate methods or callbacks that are called from an event loop managed by the AppKit framework, and each iteration of the loop has its own autorelease pool. So the common scenario for objects is simply making sure autorelease is called on them after allocating so they will be released at the end of the current cycle of the event loop.
14+
15+
The Objective-C [memory management policy](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH) specifies basic rules to know how to use retain and release. The basic idea is that if you allocate an object (using `alloc`, `new`, `copy`, or `mutableCopy`), you own it and are responsible for releasing or autoreleasing it. When an object that you did not explicitly allocate is returned by a function, you do not need to release it unless you take ownership of it by retaining it. This also applies to objects returned by class methods that create new objects like `NSString#stringWithFormat`. Since they are allocating we can assume they are calling autorelease before returning.
16+
17+
As it turns out, DarwinKit generates Go idiomatic New functions for all classes that does an `alloc` and `init` followed by an `autorelease` before returning. You can always do your own explicit allocation if this is not the desired behavior, but this means in the common case where you are writing code in a delegate or callback that will be called from the application event loop, or are otherwise in an autorelease pool, you can basically write Go code as usual and not have to do anything special. UNLESS you *do* need to take ownership of an object.
18+
19+
If you need the object to live longer than this event loop iteration, you will need to take ownership. So if you assign it to a variable declared outside the event loop, like a global variable, or assign to a struct field or append to a slice that was declared outside the loop, or pass by reference to anything that will need it to stick around, you will probably want to retain the object and have the Go garbage collector be responsible for releasing the object. Another situation you will need to take ownership like this is using the object in a new goroutine.
20+
21+
DarwinKit provides `objc.Retain()`, which calls retain on the object and creates a [Go finalizer](https://pkg.go.dev/runtime#SetFinalizer) that will release the object when Go garbage collects it. We recommend this instead of calling retain and release directly on the object, but you have that option if you know what you're doing.
22+
23+
Long story short, there's only one special thing you have to do in your Go code to accomodate the Objective-C memory management system, which is use `objc.Retain()` when you need to keep an object around or use it from a goroutine. If you work with objects outside the application event loop, either in programs that don't start it or in the code before starting it, you can just wrap it all with an `objc.WithAutoreleasePool()`.
24+
25+
That's it. The majority of the concern is dealing with Objective-C objects in "Go space". Go values in "Objective-C space" are not so much a problem because values are either passed by value, are already Objective-C objects, or are delegates or callbacks that are kept from Go garbage collection by the `objc` package.
26+
27+
28+

helper/action/action_handler.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func Wrap(handler Handler) (target Target, selector objc.Selector) {
3535
h := cgo.NewHandle(handler)
3636
return Target{
3737
Object: objc.ObjectFrom(C.C_NewAction(C.uintptr_t(h))),
38-
}, objc.SelectorRegisterName("onAction:")
38+
}, objc.RegisterSelectorName("onAction:")
3939
}
4040

4141
// Set set action for an ojbc instance, if it has target and setAction method.

macos/_examples/helloworld/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func init() {
1414
}
1515

1616
func main() {
17-
macos.Launch(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
17+
macos.RunApp(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
1818
app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular)
1919
app.ActivateIgnoringOtherApps(true)
2020

macos/_examples/opengl/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func didLaunch(foundation.Notification) {
6868
// Request updates at 60Hz
6969
go func() {
7070
for range time.Tick(time.Second / 60) {
71-
dispatch.GetMainQueue().DispatchAsync(func() { objc.CallMethod[objc.Void](view, objc.GetSelector("setNeedsDisplay")) })
71+
dispatch.MainQueue().DispatchAsync(func() { objc.CallMethod[objc.Void](view, objc.GetSelector("setNeedsDisplay")) })
7272
}
7373
}()
7474

macos/_examples/pomodoro/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func main() {
6161
3: "⏸️ Break %02d:%02d",
6262
}
6363
// updates to the ui should happen on the main thread to avoid segfaults
64-
dispatch.GetMainQueue().DispatchAsync(func() {
64+
dispatch.MainQueue().DispatchAsync(func() {
6565
item.Button().SetTitle(fmt.Sprintf(labels[state], timer/60, timer%60))
6666
})
6767
}

macos/_examples/webshot/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func initAndRun() {
4545

4646
navigationDelegate := &webkit.NavigationDelegate{}
4747
navigationDelegate.SetWebViewDidFinishNavigation(func(webView webkit.WebView, navigation webkit.Navigation) {
48-
dispatch.GetMainQueue().DispatchAsync(func() {
48+
dispatch.MainQueue().DispatchAsync(func() {
4949
script := `var rect = {"width":document.body.scrollWidth, "height":document.body.scrollHeight}; rect`
5050
webView.EvaluateJavaScriptCompletionHandler(script, func(value objc.Object, err foundation.Error) {
5151
rect := foundation.DictToMap[string, foundation.Number](foundation.DictionaryFrom(value.Ptr()))

0 commit comments

Comments
 (0)