You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dws/README.md

13 KiB

dws

dws (desktop web server) is a simple http fileserver app. It can be used to run an http server on a Mac as a native Mac application with a native Mac UI using Apple's Cocoa framework. This project functions primarily as an exploration into writing a Go application with a native UI. The app is very similar to Python's SimpleHTTP server, but demonstrates how to produce a single statically linked binary that integrates both the desktop UI and the webserver, running in a shared memory environment.

download

Compiled applications will be posted on the releases page on the project's github. You should be able to download the zip, unzip it, and run the app inside. I've only tested it on macOS Sierra so far.

build

Building this project requires that you have compilers for Go, C, and Objective-C, as well as the necessary header files for Cocoa. In practice, this only means having a Go compiler and the XCode command-line tools, since the XCode command-line tools will give you everything you need for developing Cocoa applications. You do not need to use XCode or Interface builder to compile or work on this project. Given a Go compiler and clang, the process for building the executable is as follows:

go build

The Go tool will invoke clang for you automatically. There are no nib files or resource files. Be aware that this will produce only a binary file, it will not produce an App bundle. The Go tool is not aware of the App Bundle structure, it is only responsible for building the executable.

building an app bundle

Building an App Bundle is reasonably straightforward. The App Bundle is just a special folder layout. A Makefile is included in the project to simplify this task. If you have Make, you can build and assemble a fresh App Bundle as follows:

make

If you don't have Make, you can recreate the folder structure by hand:

dws.app/
└── Contents
    ├── Info.plist
    └── MacOS
        └── dws

code walk

dws is both a Go application and a Cocoa application. It is compiled with the Go tool. 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 contains the definition of the main function, which gives us a bird's-eye view of the application's structure:

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() 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(). 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 is very short:

func Desktop() UI {
	return cocoa.Desktop()
}

Since our Desktop function is defined in ui/ui_darwin.go, 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). 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, which contains our cocoa.Desktop definition:

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:

/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa
#include <stdlib.h>
#include "ui.h"
*/
import "C"

This comment block is a 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. <stdlib.h> 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, our header file for our Cocoa ui. We see in this file the definition for the ui_init function:

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:

setting up a Cocoa application without XCode or Nib files

void ui_init() {
    defaultAutoreleasePool = [NSAutoreleasePool new];
    [NSApplication sharedApplication];
    [NSApp setDelegate: [[AppDelegate new] autorelease]];
    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}

We start by initializing our NSAutoreleasePool, which is used for 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, the base application type for managing a Mac application. Sending a sharedApplication message 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. It turns out that Cocoa's extensive use of the delegation pattern 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, which is a protocol.

Our AppDelegate is declared in AppDelegate.h:

@interface AppDelegate : NSObject <NSApplicationDelegate>
@end

and implemented in 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.