Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve docs and APIs #180

Merged
merged 5 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ clobber:
.PHONY: clobber

example:
$(GOEXE) run ./macos/_examples/widgets/main.go
$(GOEXE) run ./macos/_examples/helloworld/main.go
.PHONY: example

generate/symbols.zip:
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ Native Apple APIs for Golang!
<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>
<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>

> [!IMPORTANT]
> 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.

------

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:
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:

```go
package main
Expand All @@ -27,11 +28,13 @@ import (
)

func init() {
// ensure main is run on the startup thread
runtime.LockOSThread()
}

func main() {
macos.Launch(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
// runs macOS application event loop with a callback on success
macos.RunApp(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular)
app.ActivateIgnoringOtherApps(true)

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

* 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.
* 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).
* 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).
* 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.
* Goroutines that interact with GUI objects need to dispatch operations on the main thread otherwise it will segfault.
* 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.
* 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.
* This is all tenable for simple programs, but these are the reasons we don't *recommend* large/complex programs using DarwinKit.

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

## How it works

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.
<details>
<summary>Brief background on Objective-C</summary>
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.
</details>

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:

```go
app := objc.CallMethod[objc.Object](objc.GetClass("NSApplication"), objc.Sel("sharedApplication"))
objc.CallMethod[objc.Void](app, objc.Sel("run"))
app := objc.Call[objc.Object](objc.GetClass("NSApplication"), objc.Sel("sharedApplication"))
objc.Call[objc.Void](app, objc.Sel("run"))
```

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

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.
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).

Objects in Objective-C are passed around as typed pointer values. When we receive an object from a method
call in Go, the `objc` package receives it as a pointer, which it first puts into an `unsafe.Pointer`. The
bindings for a class define a struct type that embeds an `objc.Object` struct, which contains a single
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.
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).

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

Expand Down
5 changes: 5 additions & 0 deletions dispatch/dispatch.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system.
//
// [Apple Documentation]
//
// [AppleDocumentation]: https://developer.apple.com/documentation/dispatch?language=objc
package dispatch

// #cgo CFLAGS: -x objective-c
Expand Down
12 changes: 10 additions & 2 deletions dispatch/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ const (
QueuePriorityBackground QueuePriority = math.MinInt16
)

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

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

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

// Submits a block object for execution and returns after that block finishes executing.
func (q Queue) DispatchSync(task func()) {
id := cgo.NewHandle(task)
C.Dispatch_Sync(q.ptr, C.uintptr_t(id))
Expand Down
33 changes: 33 additions & 0 deletions docs/bindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Bindings API

* Frameworks have their own packages
* Lowercase framework name (AppKit => appkit)
* Lowercase prefix if super long (uniformtypeidentifiers => uti)
* Symbol prefixes are removed (CGPoint => Point)
* Constants and enums are 1:1
* Extra k prefix is kept (kCGImageStatusInvalidData => KImageStatusInvalidData)
* Classes (ex: NSWindow)
* Unexported struct type for class (_WindowClass)
* Variable for class singleton (WindowClass)
* Interface for class prefixed with I (IWindow)
* Embeds superclass interface (IView)
* Struct for class (Window)
* Embeds superclass struct (View)
* Instance methods are 1:1
* Selector names (setFrame:display: => SetFrameDisplay)
* Arguments with protocols get alt methods with argument as object
* ...
* Longer overlapping selector gets _ suffix
* reload => Reload
* reload: => Reload_
* New function (NewWindow)
* alloc/init/autorelease
* Init instance methods get New function variants
* initWithFrame: => NewWindowWithFrame(...)
* autorelease
* Class methods are 1:1 on class type
* Class methods get function variants
* windowNumbersWithOptions: => Window_WindowNumbersWithOptions(...)
* removeFrameUsingName: => Window_RemoveFrameUsingName(...)
* Protocols (NSWindowDelegate)
* ...
3 changes: 3 additions & 0 deletions docs/generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generation

TODO
28 changes: 28 additions & 0 deletions docs/memorymanagement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Memory Management

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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()`.

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.



2 changes: 1 addition & 1 deletion helper/action/action_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func Wrap(handler Handler) (target Target, selector objc.Selector) {
h := cgo.NewHandle(handler)
return Target{
Object: objc.ObjectFrom(C.C_NewAction(C.uintptr_t(h))),
}, objc.SelectorRegisterName("onAction:")
}, objc.RegisterSelectorName("onAction:")
}

// Set set action for an ojbc instance, if it has target and setAction method.
Expand Down
2 changes: 1 addition & 1 deletion macos/_examples/helloworld/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func init() {
}

func main() {
macos.Launch(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
macos.RunApp(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular)
app.ActivateIgnoringOtherApps(true)

Expand Down
2 changes: 1 addition & 1 deletion macos/_examples/opengl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func didLaunch(foundation.Notification) {
// Request updates at 60Hz
go func() {
for range time.Tick(time.Second / 60) {
dispatch.GetMainQueue().DispatchAsync(func() { objc.CallMethod[objc.Void](view, objc.GetSelector("setNeedsDisplay")) })
dispatch.MainQueue().DispatchAsync(func() { objc.CallMethod[objc.Void](view, objc.GetSelector("setNeedsDisplay")) })
}
}()

Expand Down
2 changes: 1 addition & 1 deletion macos/_examples/pomodoro/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func main() {
3: "⏸️ Break %02d:%02d",
}
// updates to the ui should happen on the main thread to avoid segfaults
dispatch.GetMainQueue().DispatchAsync(func() {
dispatch.MainQueue().DispatchAsync(func() {
item.Button().SetTitle(fmt.Sprintf(labels[state], timer/60, timer%60))
})
}
Expand Down
2 changes: 1 addition & 1 deletion macos/_examples/webshot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func initAndRun() {

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