diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..da4ee07 --- /dev/null +++ b/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleGetInfoString + dws + + CFBundleDisplayName + dws + + CFBundleName + dws + + CFBundleIndentifier + com.jordanorelli.dws + + CFBundleVersion + 1 + + CFBundleExecutable + dws + + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e30ce4a --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +bundle: + go build -o dws + rm -rf dws.app + mkdir -p dws.app/Contents/MacOS + cp dws dws.app/Contents/MacOS + cp Info.plist dws.app/Contents + diff --git a/README.md b/README.md new file mode 100644 index 0000000..afe1a4f --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# 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](https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/OSX_Technology_Overview/CocoaApplicationLayer/CocoaApplicationLayer.html) +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](/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](https://developer.apple.com/app-store/app-bundles/). 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](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): + +``` go +func main() { + // ... +} +``` + +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. + +Next, we initialize our Cocoa app by [calling +`ui.Desktop()`](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/main.go#L31). +Our `Desktop` function is defined [in +`ui/ui_darwin.go`](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/ui/ui_darwin.go#L7), +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](https://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool)). +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](https://github.com/jordanorelli/dws/blob/8dee9e5b564edb92c6cbd10103701495dd33f5d6/ui/ui_darwin.go#L7-L9) +is very short: + +``` go +func Desktop() UI { + return cocoa.Desktop() +} +``` + +This is where we first encounter [the `ui/cocoa` +package](https://github.com/jordanorelli/dws/tree/8dee9e5b564edb92c6cbd10103701495dd33f5d6/ui/cocoa). +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). In this file we see a cgo import statement: + + +``` go +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Cocoa +#include +#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. `` 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. + +