From 55cf6e3b4469c5b85f197fe01d1ac90e0df8ad4d Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Fri, 30 Jun 2017 23:40:17 -0500 Subject: [PATCH] kill the code walk this is way too tedious. it's boring, too. i'd rather do this as a blog post. --- README.md | 242 ------------------------------------------------------ 1 file changed, 242 deletions(-) diff --git a/README.md b/README.md index 55faa0a..756f3a7 100644 --- a/README.md +++ b/README.md @@ -57,245 +57,3 @@ dws.app/ └── MacOS └── dws ``` - -# code walk - -dws is both a Go application and a Cocoa application. It is compiled with [the -Go tool](https://golang.org/cmd/go/). Although it may be tempting to speak of -the Go and the Cocoa portions of the project as "the Go application" and "the -Cocoa application", that would be misleading: there is just one application. -The Go portion and the Cocoa portion of the application reside in the same -process, with the same address space. - -During compilation, the Go tool invokes clang via cgo, and clang compiles a -mixture of C and Objective-C files into object files, which are linked by the -Go linker. cgo will generate some bridging C code for us that will create -function call bindings to allow Go code to call C code and vice-versa. Go is -able to access C type definitions, including struct definitions, but C is not -able to access Go structs. Go is not aware that it is linking against the -Objective-C runtime. Indeed, Go and Objective-C are incapable of interacting -directly, so there is a thin bridging layer written in C that can connect the -two. - -## project structure - -The project consists of the following packages: -- `main`: the root of the git project. This package defines the application's - entry point and imports the other packages. -- `bg`: the background package. This package contains the http server - implementation. It is written entirely in Go. -- `events`: defines data types that can be used by the bg and ui packages to - communicate. We define these communication primitives in their own package to - avoid a circular import, which is forbidden in Go. -- `ui`: defines our user interface. -- `ui/cocoa`: contains our Cocoa application. This package is the only package - that uses cgo. - -## the entry point - -The application's entry point is defined in Go. -[`main.go`](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/main.go) -contains the definition of [the `main` -function](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/main.go#L27), -which gives us a bird's-eye view of the application's structure: - -``` go -func main() { - runtime.LockOSThread() - - desktop := ui.Desktop() - - uiEvents := make(chan events.UserEvent, 1) - bgEvents := make(chan events.BackgroundEvent, 1) - - go bg.Run(bgEvents, uiEvents) - - if err := desktop.Run(uiEvents, bgEvents); err != nil { - exit(1, "UI Error: %v", err) - } -} -``` - -We start by [calling -`runtime.LockOSThread()`](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/main.go#L28) -to lock the `main` function to the main thread of the application. This is -because of a Cocoa requirement: OSX will only send events to an application's -main thread, so we need to make sure that we're launching our Cocoa app from -the main thread. Boring. - -Next, we initialize our Cocoa app by [calling -`ui.Desktop()`](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/main.go#L31). -The UI itself is written to be opaque to the backend so that we can build many -different UI implementations over the same backend. - -Next we create a pair of channels, `uiEvents` and `bgEvents`. These channels -will be passed to both the backend and the frontend. The UI will listen on the -`bgEvents` channel for messages from the backend and send any user events such -as clicks or menu selections as `events.UserEvent` messages on the `uiEvents` -channel. Similarly, the backend will listen on the `uiEvents` channel to accept -and respond to the user's actions. The `bgEvents` channel is used by the -backend to send messages that should be displayed to the user. Since our -application is an http server, that's things like "started handling an http -request" and "finished handling an http request". Next we kick off the -background in its own goroutine so that it can run independently of the UI's -run loop. Finally, we call `desktop.Run` to start the application's run loop -and block until it has completed. We'll talk about the presentation layer -first, then come back to the background server functionality. - -## setting up the Cocoa UI - -Our [`ui.Desktop` -definition](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/ui/ui_darwin.go#L7-L9) -is very short: - -``` go -func Desktop() UI { - return cocoa.Desktop() -} -``` - -Since our `Desktop` function is defined [in -`ui/ui_darwin.go`](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/ui/ui_darwin.go#L7), -it will only be compiled on MacOS. The `_darwin.go` suffix tells the Go -compiler to only compile the file on Darwin. Attempting to compile this program -will fail on all other operating systems because `ui.Desktop` will only be -defined on MacOS. (more about conditional compilation in Go can be found [here -on Dave Cheney's -blog](https://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool)). -We could conceivably write a win32 presentation layer by defining a conformant -`ui.Desktop()` function in `ui_windows.go`, but no such win32 implementation -has been written yet. In essence, this file tells the Go compiler to only -define the `Desktop` function on MacOS, and that its invocations should be -forwarded to `cocoa.Desktop`. We'll use the `ui/cocoa` package to contain all -of our Mac-specific code. - -### the cgo import - -Since we've imported the `cocoa` package, the Go tool compiles all of the `.go` -files, which is just one file: -[`ui.go`](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/ui/cocoa/ui.go), -which contains our `cocoa.Desktop` definition: - -``` go -func Desktop() *ui { - if instance != nil { - return instance - } - C.ui_init() - instance = new(ui) - return instance -} -``` - -`instance` in this case is just a pointer to a package global, which exists to -ensure that we only attempt to initialize the Cocoa UI once, like a poorly-made -singleton with no synchronization. Not very strong, but fine for our use case. -We then initialize our Cocoa app by calling `C.ui_init`. This demands closer -inspection. - -First, it should be immediately clear to Go programmers that we're calling what -appears to be an unexported function on a different package. `C` in this case, -and in all cases of cgo, is not a package in the sense that it is a true Go -package: it is a pseudo-package that acts as an interface to our C code. Let's -take a look at how we imported this package: - -``` go -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Cocoa -#include -#include "ui.h" -*/ -import "C" -``` - -This comment block is a [cgo](https://golang.org/cmd/cgo/) import statement. It -instructs the Go tool to use cgo, which will in turn, invoke a C compiler. In -our case, that means clang, because that is the default C compiler on Darwin. -More literally: clang is selected because the value of the `CC` environment -variable is `clang`. We provide a few options to clang that are relevant to our -project using the `#cgo` statements. The `CFLAGS` statement allows us to pass -arguments to clang. Specifically, we enable Objective-C compilation. `LDFLAGS` -allows us to specify linker flags: this is where we ask the linker to link -against the Cocoa framework. Cgo will follow similar rules to the rest of the -Go tool: all of the source code files in the directory will be included in the -compilation. This means _all_ of the `.go`, `.h`, and `.m` files will be -compiled and included in our binary automatically, and we don't need to write a -Makefile. (If you're curious about compiler caching, it's the same situation as -Go in general: `go build` will always rebuild the entire application, while `go -install` will cache the intermediate objects. `go install` is, in general, -faster beyond the first compile, but since it moves your binary, we use `go -build` in our explainers later because it is simpler to reason about.) - -The next two lines are plain C. We could write more C code here if we so -desired, but restricting ourselves to only includes helps to keep our languages -in order. `` has to be included in order to define `C.free`, which -allows us to call C's `free` from Go to release memory allocated on the C heap. - -We also include -[`ui.h`](https://github.com/jordanorelli/dws/blob/360224ce612984b83ad033a549192cbd0df08672/ui/cocoa/ui.h), -our header file for our Cocoa ui. We see in this file the definition for the -`ui_init` function: - -``` c -void ui_init(); -``` - -This C declaration corresponds to the `C.ui_init()` call we saw in `cocoa/ui.go`. Its implementation can be seen [in `ui.m`](https://github.com/jordanorelli/dws/blob/360224ce612984b83ad033a549192cbd0df08672/ui/cocoa/ui.m#L9-L14): - -### setting up a Cocoa application without XCode or Nib files - -``` objective-c -void ui_init() { - defaultAutoreleasePool = [NSAutoreleasePool new]; - [NSApplication sharedApplication]; - [NSApp setDelegate: [[AppDelegate new] autorelease]]; - [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; -} -``` - -We start by initializing our -[NSAutoreleasePool](https://developer.apple.com/documentation/foundation/nsautoreleasepool), -which is used for -[reference-counting](https://en.wikipedia.org/wiki/Reference_counting) in both -our code and in Cocoa's code. Without this line, any Objective-C objects that -were memory-managed using `autorelease` messages will never be released, and -our application will leak memory. Even if we didn't use `autorelease` -ourselves, Cocoa uses it internally, so leaving this off would cause Cocoa to -leak. We'll see quickly that the differing memory management systems between -Go, C, and Objective-C have noticeable effects on our application. - -Next, we initialize our -[NSApplication](https://developer.apple.com/documentation/appkit/nsapplication?language=objc), -the base application type for managing a Mac application. Sending a -[`sharedApplication` -message](https://developer.apple.com/documentation/appkit/nsapplication/1428360-sharedapplication?language=objc) -to NSApplication returns the application instance, creating it if it doesn't -exist yet. We're not interested in the return value, we just call this function -for its side effects. - -Next we set our application's delegate with a [`setDelegate` -message](https://developer.apple.com/documentation/appkit/nsapplication/1428705-delegate?language=objc). -It turns out that Cocoa's extensive use of the [delegation -pattern](https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html) -makes Cocoa programming very easy for Go programmers to reason about. An -Objective-C _delegate_ property in the general case is a reference to an -object that implements a _protocol_ in the Objective-C parlance, but for a Go -programmer, we can think of a delegate property as being a struct field with -an interface type. NSApplication's `delegate` property is of -[`NSApplicationDelegate` -type](https://developer.apple.com/documentation/appkit/nsapplicationdelegate?language=objc), -which is a protocol. - -Our AppDelegate is declared [in `AppDelegate.h`](https://github.com/jordanorelli/dws/blob/360224ce612984b83ad033a549192cbd0df08672/ui/cocoa/AppDelegate.h#L4-L5): - -``` objective-c -@interface AppDelegate : NSObject -@end -``` - -and implemented [in -`AppDelegate.m`](https://github.com/jordanorelli/dws/blob/360224ce612984b83ad033a549192cbd0df08672/ui/cocoa/AppDelegate.m). -We'll take a look at some of the methods we've decided to implement when we -send our `NSApplication` a `run` message. -