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

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

func main() {
    // ...
}

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.

Next, we initialize our Cocoa app by calling ui.Desktop(). Our Desktop function is defined in ui/ui_darwin.go, which is compiled on OSX and OSX alone because the file ends with _darwin.go (more about conditional compilation in Go can be found here on Dave Cheney's blog). We could conceivably write a drop-in win32 presentation layer by defining a conformant ui.Desktop() function in ui_windows.go, but no such win32 implementation has been written yet.

Our ui.Desktop definition is very short:

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

This is where we first encounter the ui/cocoa package. Since we've imported the cocoa package, the Go tool compiles all of the .go files, which is just one file: ui.go. In this file we see a cgo import statement:

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

This is where the Go tool observes that it needs to use cgo to invoke a C compiler. The #cgo CFLAGS line allows us to pass arguments to our C compiler (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. The next two lines are plain C. We could write more C code here, but I prefer to keep it to header includes and to avoid writing too much mixed Go and C in the same file. <stdlib.h> has to be included 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'll take a look at that in a second.