shiny/driver/windriver: new package providing a Windows driver
This provides the initial implementation of a Windows driver for shiny.
It will only open a window and wait for you to close it; Buffer and
Texture will come next, when I figure out how they will work.
I tried to lay down the design of the package in doc.go. If you are still
confused, I'll be glad to rewrite or expand it.
Currently this uses cgo.
Patch set 2 changes the C formatting to match the Go sourcce tree's.
It also includes a quick change to driver_fallback.go.
Patch set 3 rewrites doc.go, hopefully to be clearer.
Patch set 4 implements changes suggested in code review and
removes a block of comments that was accidentally left in
when doc.go was written.
Patch set 5 formats this commit message.
Change-Id: I2b060455243f445dd0f4c62f6f0c346768491547
Reviewed-on: https://go-review.googlesource.com/13617
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/shiny/driver/driver_fallback.go b/shiny/driver/driver_fallback.go
index bbe99f5..ac37746 100644
--- a/shiny/driver/driver_fallback.go
+++ b/shiny/driver/driver_fallback.go
@@ -4,6 +4,7 @@
// +build !darwin
// +build !linux android
+// +build !windows
package driver
diff --git a/shiny/driver/driver_windows.go b/shiny/driver/driver_windows.go
new file mode 100644
index 0000000..4ae016c
--- /dev/null
+++ b/shiny/driver/driver_windows.go
@@ -0,0 +1,14 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package driver
+
+import (
+ "golang.org/x/exp/shiny/driver/windriver"
+ "golang.org/x/exp/shiny/screen"
+)
+
+func main(f func(screen.Screen)) {
+ windriver.Main(f)
+}
diff --git a/shiny/driver/windriver/doc.go b/shiny/driver/windriver/doc.go
new file mode 100644
index 0000000..36ef294
--- /dev/null
+++ b/shiny/driver/windriver/doc.go
@@ -0,0 +1,104 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package windriver provides the Windows driver for accessing a screen.
+package windriver
+
+/*
+Implementation Details
+
+On Windows, UI can run on any thread, but any windows created
+on a thread must only be manipulated from that thread. You can send
+"window messages" to any window; when you send a window
+message to a window owned by another thread, Windows will
+temporarily switch to that thread to dispatch the message. As such,
+windows serve as the communication endpoints between threads
+on Windows. In addition, each thread that hosts UI must handle
+incoming window messages from the OS through a "message pump".
+These messages include paint events and input events.
+
+windriver designates the thread that calls Main as the UI thread.
+It locks this thread, creates a special window to handle screen.Screen
+calls, runs the function passed to Main on another goroutine, and
+runs a message pump.
+
+The window that handles screen.Screen functions is currently called
+the "utility window". A better name can be chosen later. This window
+handles creating screen.Windows/Buffers/Textures. As such, all shiny
+Windows are owned by a single thread.
+
+Each function in windriver, be it on screen.Screen or screen.Window,
+is translated into a window message and sent to a window, namely
+the utility window and the screen.Window window, respectively.
+This is how windriver remains thread-safe.
+
+(TODO(andlabs): actually move per-window messages to the window itself)
+
+Presently, the actual Windows API work is implemented in C. This is
+to encapsulate Windows's data structures, ensure properly handling
+signed -> unsigned conversions in constants, handle pointer casts
+cleanly, and properly handle the "last error", which I will describe
+later.
+
+Here is a demonstration of all of the above. When you call
+screen.NewWindow(opts), the Go code calls the C function
+createWindow, which is implemented as something similar to
+
+ HRESULT createWindow(newWindowOpts *opts, HWND *phwnd) {
+ return (HRESULT) SendMessageW(utilityWindow,
+ msgCreateWindow,
+ (WPARAM) opts,
+ (LPARAM) phwnd);
+ }
+
+HRESULT is another type for errors in Windows; I will again describe
+this later. This function tells the utility window to make a new window,
+using the given options, storing the window's OS handle in phwnd, and
+returning any error directly to us through SendMessageW.
+
+This code is running on another goroutine, which will definitely be
+run on another OS thread. As such, Windows will switch to the UI
+thread to dispatch this new window message. The code for the
+implementation of the utility window (called a "window procedure")
+contains something like this:
+
+ case msgCreateWindow:
+ return utilCreateWindow((newWindowOpts *) wParam,
+ (HWND *) lParam);
+
+and the utilCreateWindow function does the actual work:
+
+ LRESULT utilCreateWindow(newWindowOpts *opts, HWND *phwnd) {
+ *phwnd = CreateWindowExW(...);
+ if (*phwnd == NULL) {
+ return lastErrorAsLRESULT();
+ }
+ return lS_OK;
+ }
+
+When this returns, Windows switches back to the previous thread,
+which can now use the window handle and error value.
+
+Older Windows API functions return a Boolean flag to indicate if they
+succeeded or failed, storing the actual reason for failure in what is
+called the "last error". This is NOT contractual; functions are free to
+fail without setting the last error, or free to clear the last error on
+success.
+
+To simplify error reporting, we instead convert all last errors to the
+newer HRESULT error code system. The rules are simple: if the
+function succeeded, we return the standard success code, S_OK.
+If the function failed, we get the last error. If it's zero (no error),
+we return the special value E_FAIL. Otherwise, we convert the last
+error to an HRESULT (this is a well-defined operation that we can
+reverse later when we're ready to report the error to the user).
+This is all done by the C lastErrorToHRESULT function. Error
+reporting on the Go side is handled by th winerror function.
+
+Because window messages return LRESULTs, not HRESULTs,
+the lastErrorToLRESULT and lS_OK macros are provided, which
+automatically insert the necessary casts. An LRESULT (which is
+pointer-sized) will always be either the same size as or larger than
+an HRESULT (which is strictly 32 bits wide).
+*/
diff --git a/shiny/driver/windriver/errors.go b/shiny/driver/windriver/errors.go
new file mode 100644
index 0000000..ab3c67f
--- /dev/null
+++ b/shiny/driver/windriver/errors.go
@@ -0,0 +1,20 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package windriver
+
+// #include "windriver.h"
+import "C"
+
+import (
+ "fmt"
+)
+
+func winerror(msg string, hr C.HRESULT) error {
+ // TODO(andlabs): get long description
+ if hr == C.E_FAIL {
+ return fmt.Errorf("windriver: %s: unknown error", msg)
+ }
+ return fmt.Errorf("windriver: %s: last error %d", msg, hr&0xFFFF)
+}
diff --git a/shiny/driver/windriver/screen.go b/shiny/driver/windriver/screen.go
new file mode 100644
index 0000000..6581c92
--- /dev/null
+++ b/shiny/driver/windriver/screen.go
@@ -0,0 +1,30 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package windriver
+
+import (
+ "fmt"
+ "image"
+
+ "golang.org/x/exp/shiny/screen"
+)
+
+type screenimpl struct{}
+
+func newScreenImpl() screen.Screen {
+ return &screenimpl{}
+}
+
+func (*screenimpl) NewBuffer(size image.Point) (screen.Buffer, error) {
+ return nil, fmt.Errorf("TODO")
+}
+
+func (*screenimpl) NewTexture(size image.Point) (screen.Texture, error) {
+ return nil, fmt.Errorf("TODO")
+}
+
+func (*screenimpl) NewWindow(opts *screen.NewWindowOptions) (screen.Window, error) {
+ return newWindow(opts)
+}
diff --git a/shiny/driver/windriver/utilwindow.c b/shiny/driver/windriver/utilwindow.c
new file mode 100644
index 0000000..37a4fa3
--- /dev/null
+++ b/shiny/driver/windriver/utilwindow.c
@@ -0,0 +1,54 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "windriver.h"
+
+HWND utilityWindow = NULL;
+
+static LRESULT CALLBACK utilityWindowWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+ HWND *phwnd;
+
+ switch (uMsg) {
+ case msgCreateWindow:
+ phwnd = (HWND *) lParam;
+ return utilCreateWindow(phwnd);
+ case msgDestroyWindow:
+ return utilDestroyWindow((HWND) wParam);
+ }
+ return DefWindowProcW(hwnd, uMsg, wParam, lParam);
+}
+
+HRESULT initUtilityWindow(void) {
+ WNDCLASSW wc;
+
+ ZeroMemory(&wc, sizeof (WNDCLASSW));
+ wc.lpszClassName = L"shiny_utilityWindow";
+ wc.lpfnWndProc = utilityWindowWndProc;
+ wc.hInstance = thishInstance;
+ wc.hIcon = LoadIconW(NULL, IDI_APPLICATION);
+ if (wc.hIcon == NULL) {
+ return lastErrorToHRESULT();
+ }
+ wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
+ if (wc.hCursor == NULL) {
+ return lastErrorToHRESULT();
+ }
+ // TODO(andlabs): change this to something else? NULL? the hollow brush?
+ wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
+ if (RegisterClassW(&wc) == 0) {
+ return lastErrorToHRESULT();
+ }
+
+ utilityWindow = CreateWindowExW(0,
+ L"shiny_utilityWindow", L"Shiny Utility Window",
+ WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT, CW_USEDEFAULT,
+ CW_USEDEFAULT, CW_USEDEFAULT,
+ HWND_MESSAGE, NULL, thishInstance, NULL);
+ if (utilityWindow == NULL) {
+ return lastErrorToHRESULT();
+ }
+
+ return S_OK;
+}
diff --git a/shiny/driver/windriver/window.c b/shiny/driver/windriver/window.c
new file mode 100644
index 0000000..6e54d95
--- /dev/null
+++ b/shiny/driver/windriver/window.c
@@ -0,0 +1,69 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "windriver.h"
+
+#define windowClass L"shiny_Window"
+
+static LRESULT CALLBACK windowWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+ // TODO(andlabs): this is only for testing that the package works; delete when done
+ if (uMsg == WM_CLOSE) {
+ PostQuitMessage(0);
+ }
+ return DefWindowProcW(hwnd, uMsg, wParam, lParam);
+}
+
+HRESULT initWindowClass(void) {
+ WNDCLASSW wc;
+
+ ZeroMemory(&wc, sizeof (WNDCLASSW));
+ wc.lpszClassName = windowClass;
+ wc.lpfnWndProc = windowWndProc;
+ wc.hInstance = thishInstance;
+ wc.hIcon = LoadIconW(NULL, IDI_APPLICATION);
+ if (wc.hIcon == NULL) {
+ return lastErrorToHRESULT();
+ }
+ wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
+ if (wc.hCursor == NULL) {
+ return lastErrorToHRESULT();
+ }
+ // TODO(andlabs): change this to something else? NULL? the hollow brush?
+ wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
+ if (RegisterClassW(&wc) == 0) {
+ return lastErrorToHRESULT();
+ }
+ return S_OK;
+}
+
+HRESULT createWindow(HWND *phwnd) {
+ return (HRESULT) SendMessageW(utilityWindow, msgCreateWindow, 0, (LPARAM) phwnd);
+}
+
+LRESULT utilCreateWindow(HWND *phwnd) {
+ *phwnd = CreateWindowExW(0,
+ windowClass, L"Shiny Window",
+ WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT, CW_USEDEFAULT,
+ CW_USEDEFAULT, CW_USEDEFAULT,
+ NULL, NULL, thishInstance, NULL);
+ if (*phwnd == NULL) {
+ return lastErrorToLRESULT();
+ }
+ // TODO(andlabs): use proper nCmdShow
+ ShowWindow(*phwnd, SW_SHOWDEFAULT);
+ // TODO(andlabs): UpdateWindow()?
+ return lS_OK;
+}
+
+HRESULT destroyWindow(HWND hwnd) {
+ return (HRESULT) SendMessageW(utilityWindow, msgDestroyWindow, (WPARAM) hwnd, 0);
+}
+
+LRESULT utilDestroyWindow(HWND hwnd) {
+ if (DestroyWindow(hwnd) == 0) {
+ return lastErrorToLRESULT();
+ }
+ return lS_OK;
+}
diff --git a/shiny/driver/windriver/window.go b/shiny/driver/windriver/window.go
new file mode 100644
index 0000000..e2eaa54
--- /dev/null
+++ b/shiny/driver/windriver/window.go
@@ -0,0 +1,66 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package windriver
+
+// #include "windriver.h"
+import "C"
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+
+ "golang.org/x/exp/shiny/driver/internal/pump"
+ "golang.org/x/exp/shiny/screen"
+ "golang.org/x/image/math/f64"
+ "golang.org/x/mobile/event/paint"
+)
+
+type window struct {
+ hwnd C.HWND
+ pump pump.Pump
+}
+
+func newWindow(opts *screen.NewWindowOptions) (screen.Window, error) {
+ var hwnd C.HWND
+
+ hr := C.createWindow(&hwnd)
+ if hr != C.S_OK {
+ return nil, winerror("error creating window", hr)
+ }
+ return &window{
+ hwnd: hwnd,
+ pump: pump.Make(),
+ }, nil
+}
+
+func (w *window) Release() {
+ if w.hwnd == nil { // already released?
+ return
+ }
+ // TODO(andlabs): check for errors from this?
+ C.destroyWindow(w.hwnd)
+ w.hwnd = nil
+ w.pump.Release()
+}
+
+func (w *window) Events() <-chan interface{} { return w.pump.Events() }
+func (w *window) Send(event interface{}) { w.pump.Send(event) }
+
+func (w *window) Upload(dp image.Point, src screen.Buffer, sr image.Rectangle, sender screen.Sender) {
+ // TODO
+}
+
+func (w *window) Fill(dr image.Rectangle, src color.Color, op draw.Op) {
+ // TODO
+}
+
+func (w *window) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+ // TODO
+}
+
+func (w *window) EndPaint(p paint.Event) {
+ // TODO
+}
diff --git a/shiny/driver/windriver/windriver.c b/shiny/driver/windriver/windriver.c
new file mode 100644
index 0000000..be6183d
--- /dev/null
+++ b/shiny/driver/windriver/windriver.c
@@ -0,0 +1,25 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "windriver.h"
+
+void mainMessagePump(void) {
+ MSG msg;
+
+ // This GetMessage cannot fail: http://blogs.msdn.com/b/oldnewthing/archive/2013/03/22/10404367.aspx
+ // TODO(andlabs): besides, what should we do if a future Windows change makes it fail for some other reason? we can't return an error because it's too late to stop the main function
+ while (GetMessage(&msg, NULL, 0, 0)) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+}
+
+HRESULT lastErrorToHRESULT(void) {
+ DWORD le;
+
+ le = GetLastError();
+ if (le == 0)
+ return E_FAIL;
+ return HRESULT_FROM_WIN32(le);
+}
diff --git a/shiny/driver/windriver/windriver.go b/shiny/driver/windriver/windriver.go
new file mode 100644
index 0000000..5964882
--- /dev/null
+++ b/shiny/driver/windriver/windriver.go
@@ -0,0 +1,76 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package windriver
+
+// #include "windriver.h"
+import "C"
+
+import (
+ "image"
+ "runtime"
+
+ "golang.org/x/exp/shiny/screen"
+)
+
+// TODO(andlabs): Should the Windows API code be split into a
+// separate package internal/winbackend so gldriver can use it too?
+
+// Main is called by the program's main function to run the graphical
+// application.
+//
+// It calls f on the Screen, possibly in a separate goroutine, as some OS-
+// specific libraries require being on 'the main thread'. It returns when f
+// returns.
+func Main(f func(screen.Screen)) {
+ if err := main(f); err != nil {
+ f(errScreen{err})
+ }
+}
+
+func main(f func(screen.Screen)) (retErr error) {
+ // It does not matter which OS thread we are on.
+ // All that matters is that we confine all UI operations
+ // to the thread that created the respective window.
+ runtime.LockOSThread()
+
+ hr := C.initUtilityWindow()
+ if hr != C.S_OK {
+ return winerror("failed to create utility window", hr)
+ }
+ defer func() {
+ // TODO(andlabs): log an error if this fails?
+ C.DestroyWindow(C.utilityWindow)
+ // TODO(andlabs): unregister window class
+ }()
+
+ hr = C.initWindowClass()
+ if hr != C.S_OK {
+ return winerror("failed to create Window window class", hr)
+ }
+ // TODO(andlabs): uninit
+
+ s := newScreenImpl()
+ go f(s)
+
+ C.mainMessagePump()
+ return nil
+}
+
+// errScreen is a screen.Screen.
+type errScreen struct {
+ err error
+}
+
+func (e errScreen) NewBuffer(size image.Point) (screen.Buffer, error) {
+ return nil, e.err
+}
+
+func (e errScreen) NewTexture(size image.Point) (screen.Texture, error) {
+ return nil, e.err
+}
+
+func (e errScreen) NewWindow(opts *screen.NewWindowOptions) (screen.Window, error) {
+ return nil, e.err
+}
diff --git a/shiny/driver/windriver/windriver.h b/shiny/driver/windriver/windriver.h
new file mode 100644
index 0000000..89acace
--- /dev/null
+++ b/shiny/driver/windriver/windriver.h
@@ -0,0 +1,56 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+#define UNICODE
+#define _UNICODE
+#define STRICT
+#define STRICT_TYPED_ITEMIDS
+#define CINTERFACE
+#define COBJMACROS
+// see https://github.com/golang/go/issues/9916#issuecomment-74812211
+#define INITGUID
+// get Windows version right; right now Windows XP
+#define WINVER 0x0501
+#define _WIN32_WINNT 0x0501
+#define _WIN32_WINDOWS 0x0501 /* according to Microsoft's winperf.h */
+#define _WIN32_IE 0x0600 /* according to Microsoft's sdkddkver.h */
+#define NTDDI_VERSION 0x05010000 /* according to Microsoft's sdkddkver.h */
+#include <windows.h>
+
+// see http://blogs.msdn.com/b/oldnewthing/archive/2004/10/25/247180.aspx
+// this will work on MinGW too
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+#define thishInstance ((HINSTANCE) (&__ImageBase))
+
+// messages sent to the utility window to do the various functions of the package on the UI thread
+// we start at WM_USER + 0x40 to make room for the DM_* messages
+enum {
+ // wParam - 0
+ // lParam - pointer to store HWND in
+ // return - error LRESULT
+ msgCreateWindow = WM_USER + 0x40,
+ // wParam - hwnd
+ // lParam - 0
+ // return - error LRESULT
+ msgDestroyWindow,
+};
+
+// windriver.c
+extern void mainMessagePump(void);
+extern HRESULT lastErrorToHRESULT(void);
+#define lS_OK ((LRESULT) S_OK)
+#define lastErrorToLRESULT() ((LRESULT) lastErrorToHRESULT())
+
+// utilwindow.c
+extern HWND utilityWindow;
+extern HRESULT initUtilityWindow(void);
+
+// window.c
+extern HRESULT initWindowClass(void);
+extern HRESULT createWindow(HWND *);
+extern LRESULT utilCreateWindow(HWND *);
+extern HRESULT destroyWindow(HWND);
+extern LRESULT utilDestroyWindow(HWND);