summaryrefslogtreecommitdiff
path: root/redo/zwinconstgen.go
blob: 28ff8431e15bfbf10df4dfc0c468ab5ff597c442 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
// +build ignore

// 24 may 2014
package main

import (
	"fmt"
	"os"
	"strings"
	"go/token"
	"go/ast"
	"go/parser"
	"sort"
	"io/ioutil"
	"path/filepath"
	"text/template"
	"os/exec"
)

func getPackage(path string) (pkg *ast.Package) {
	fileset := token.NewFileSet()		// parser.ParseDir() actually writes to this; not sure why it doesn't return one instead
	filter := func(i os.FileInfo) bool {
		return strings.HasSuffix(i.Name(), "_windows.go")
	}
	pkgs, err := parser.ParseDir(fileset, path, filter, parser.AllErrors | parser.ParseComments)
	if err != nil {
		panic(err)
	}
	if len(pkgs) != 1 {
		panic("more than one package found")
	}
	for k, _ := range pkgs {		// get the sole key
		pkg = pkgs[k]
	}
	return pkg
}

type walker struct {
	desired	func(string) bool
}

var known = map[string]string{}
var unknown = map[string]struct{}{}

func (w *walker) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.Ident:			// constant or structure?
		if w.desired(n.Name) {
			if n.Obj != nil {
				delete(unknown, n.Name)
				kind := n.Obj.Kind.String()
				if known[n.Name] != "" && known[n.Name] != kind {
					panic(n.Name + "(" + kind + ") already known to be a " + known[n.Name])
				}
				known[n.Name] = kind
			} else if _, ok := known[n.Name]; !ok {		// only if not known
				unknown[n.Name] = struct{}{}
			}
		}
	}
	return w
}

func gatherNames(pkg *ast.Package) {
	desired := func(name string) bool {
		return strings.HasPrefix(name, "c_") ||		// constants
			strings.HasPrefix(name, "s_")			// structs
	}
	for _, f := range pkg.Files {
		for _, d := range f.Decls {
			ast.Walk(&walker{desired}, d)
		}
		for _, c := range f.Comments {
			for _, cc := range c.List {
				readComment(cc)
			}
		}
	}
}

var funcs []string
var dlls = map[string]struct{}{}

// parse comments of the form "// wfunc dll Name argtypes {ret[,noerr]|void}"
// TODO clean this up
func readComment(c *ast.Comment) {
	words := strings.Split(c.Text, " ")[1:]		// strip leading //
	if len(words) <= 0 || words[0] != "wfunc" {
		return
	}
	dll := words[1]
	dlls[dll] = struct{}{}
	name := words[2]
	args := make([]string, 0, len(words))
	for i := 3; i < len(words) - 1; i++ {
		args = append(args, words[i])
	}
	ret := words[len(words) - 1]

	funcs = append(funcs, fmt.Sprintf("var fv_%s = %s.NewProc(%q)", name, dll, name))

	r1 := "r1"
	err := "err"
	assign := ":="

	r := rune('a')
	argspec := ""
	for _, t := range args {
		argspec += fmt.Sprintf("%c %s, ", r, t)
		r++
	}
	retspec := ""
	if ret != "void" {
		r := strings.Split(ret, ",")
		retspec = "(" + r[0]
		if len(r) > 1 && r[1] == "noerr" {
			err = "_"
		} else {
			retspec += ", error"
		}
		retspec += ")"
	} else {
		r1 = "_"
		err = "_"
		assign = "="
	}
	funcs = append(funcs, fmt.Sprintf("func f_%s(%s) %s {", name, argspec, retspec))

	call := fmt.Sprintf("\t%s, _, %s %s fv_%s.Call(", r1, err, assign, name)
	r = rune('a')
	for _, t := range args {
		call += "uintptr("
		if t[0] == '*' {
			call += "unsafe.Pointer("
		}
		call += fmt.Sprintf("%c", r)
		if t[0] == '*' {
			call += ")"
		}
		call += "), "
		r++
	}
	call += ")"
	funcs = append(funcs, call)

	if ret != "void" {
		r := strings.Split(ret, ",")
		retspec = "return (" + r[0] + ")("
		if r[0][0] == '*' {
			retspec += "unsafe.Pointer("
		}
		retspec += "r1"
		if r[0][0] == '*' {
			retspec += ")"
		}
		retspec += ")"
		if len(r) > 1 && r[1] == "noerr" {
			// do nothing
		} else {
			retspec += ", err"
		}
		funcs = append(funcs, retspec)
	}

	funcs = append(funcs, "}")
}

// for backwards compatibiilty reasons, Windows defines GetWindowLongPtr()/SetWindowLongPtr() as a macro which expands to GetWindowLong()/SetWindowLong() on 32-bit systems
// we'll just simulate that here
var gwlpNames = map[string]string{
	"386":		"etWindowLongW",
	"amd64":		"etWindowLongPtrW",
}

// in reality these use LONG_PTR for the actual values; LONG_PTR is a signed value, but for our use case it doesn't really matter
func genGetSetWindowLongPtr(targetarch string) {
	name := gwlpNames[targetarch]

	funcs = append(funcs, fmt.Sprintf("var fv_GetWindowLongPtrW = user32.NewProc(%q)", "G" + name))
	funcs = append(funcs, "func f_GetWindowLongPtrW(hwnd uintptr, which uintptr) uintptr {")
	funcs = append(funcs, "\tres, _, _ := fv_GetWindowLongPtrW.Call(hwnd, which)")
	funcs = append(funcs, "\treturn res")
	funcs = append(funcs, "}")

	funcs = append(funcs, fmt.Sprintf("var fv_SetWindowLongPtrW = user32.NewProc(%q)", "S" + name))
	funcs = append(funcs, "func f_SetWindowLongPtrW(hwnd uintptr, which uintptr, value uintptr) {")
	funcs = append(funcs, "\tfv_SetWindowLongPtrW.Call(hwnd, which, value)")
	funcs = append(funcs, "}")
}

const outTemplate = `package main
import (
	"fmt"
	"bytes"
	"reflect"
	"go/format"
	"strings"
)
// #define UNICODE
// #define _UNICODE
// #define STRICT
// #define STRICT_TYPED_ITEMIDS
// /* 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>
// #include <commctrl.h>
// #include <stdint.h>
{{range .Consts}}// uintptr_t {{.}} = (uintptr_t) ({{noprefix .}});
{{end}}import "C"		// notice the lack of newline in the template
// MinGW will generate handle pointers as pointers to some structure type under some conditions I don't fully understand; here's full overrides
var handleOverrides = []string{
	"HWND",
	"HINSTANCE",
	"HICON",
	"HCURSOR",
	"HBRUSH",
	"HMENU",
	// These are all pointers to functions; handle them identically to handles.
	"WNDPROC",
}
func winName(t reflect.Type) string {
	for _, s := range handleOverrides {
		if strings.Contains(t.Name(), s) {
			return "uintptr"
		}
	}
	switch t.Kind() {
	case reflect.UnsafePointer:
		return "uintptr"
	case reflect.Ptr:
		return "*" + winName(t.Elem())
	case reflect.Struct:
		// the t.Name() will be the cgo-mangled name; get the original name out
		parts := strings.Split(t.Name(), "_")
		part := parts[len(parts) - 1]
		// many Windows API types have struct tagXXX as their declarator
		// if you wonder why, see http://blogs.msdn.com/b/oldnewthing/archive/2008/03/26/8336829.aspx?Redirected=true
		if strings.HasPrefix(part, "tag") {
			part = part[3:]
		}
		return "s_" + part
	case reflect.Array:
		return fmt.Sprintf("[%d]%s", t.Len(), winName(t.Elem()))
	}
	return t.Kind().String()
}
func main() {
	buf := new(bytes.Buffer)
	fmt.Fprintln(buf, "package ui")
	fmt.Fprintln(buf, "import (")
	fmt.Fprintln(buf, "\t\"syscall\"")
	fmt.Fprintln(buf, "\t\"unsafe\"")
	fmt.Fprintln(buf, ")")

	// constants
{{range .Consts}}	fmt.Fprintf(buf, "const %s = %d\n", {{printf "%q" .}}, C.{{.}})
{{end}}

	// structures
	var t reflect.Type
{{range .Structs}}	t = reflect.TypeOf(C.{{noprefix .}}{})
	fmt.Fprintf(buf, "type %s struct {\n", {{printf "%q" .}})
	for i := 0; i < t.NumField(); i++ {
		fmt.Fprintf(buf, "\t%s %s\n", t.Field(i).Name, winName(t.Field(i).Type))
	}
	fmt.Fprintf(buf, "}\n")
{{end}}

	// let's generate names for window procedure types
	fmt.Fprintf(buf, "\n")
	fmt.Fprintf(buf, "type t_UINT %s\n", winName(reflect.TypeOf(C.UINT(0))))
	fmt.Fprintf(buf, "type t_WPARAM %s\n", winName(reflect.TypeOf(C.WPARAM(0))))
	fmt.Fprintf(buf, "type t_LPARAM %s\n", winName(reflect.TypeOf(C.LPARAM(0))))
	fmt.Fprintf(buf, "type t_LRESULT %s\n", winName(reflect.TypeOf(C.LRESULT(0))))
	// and one for GetMessageW()
	fmt.Fprintf(buf, "type t_BOOL %s\n", winName(reflect.TypeOf(C.BOOL(0))))

	// functions
{{range .Funcs}}	fmt.Fprintf(buf, "%s\n", {{printf "%q" .}})
{{end}}

	// DLLs
{{range .DLLs}}		fmt.Fprintf(buf, "var %s = syscall.NewLazyDLL(%q)\n", {{printf "%q" .}}, {{printf "%s.dll" . | printf "%q"}})
{{end}}

	// and finally done
	res, err := format.Source(buf.Bytes())
	if err != nil { panic(err.Error() + "\n" + string(buf.Bytes())) }
	fmt.Printf("%s", res)
}
`

type templateArgs struct {
	Consts	[]string
	Structs	[]string
	Funcs	[]string
	DLLs		[]string
}

var templateFuncs = template.FuncMap{
	"noprefix":	func(s string) string {
		return s[2:]
	},
}

func main() {
	if len(os.Args) < 3 {
		panic("usage: " + os.Args[0] + " path goarch [go-command-options...]")
	}
	pkgpath := os.Args[1]
	targetarch := os.Args[2]
	if _, ok := gwlpNames[targetarch]; !ok {
		panic("unknown target windows/" + targetarch)
	}
	goopts := os.Args[3:]		// valid if len(os.Args) == 3; in that case this will just be a slice of length zero

	pkg := getPackage(pkgpath)
	gatherNames(pkg)

	// if we still have some known, I didn't clean things up completely
	if len(known) > 0 {
		knowns := ""
		for ident, kind := range known {
			if kind != "var" && kind != "const" {
				continue
			}
			knowns += "\n" + ident + " (" + kind + ")"
		}
		panic("error: the following are still known!" + knowns)		// has a newline already
	}

	// keep sorted for git
	consts := make([]string, 0, len(unknown))
	structs := make([]string, 0, len(unknown))
	sorteddlls := make([]string, 0, len(dlls))
	for ident, _ := range unknown {
		if strings.HasPrefix(ident, "s_") {
			structs = append(structs, ident)
			continue
		}
		consts = append(consts, ident)
	}
	for dll, _ := range dlls {
		sorteddlls = append(sorteddlls, dll)
	}
	sort.Strings(consts)
	sort.Strings(structs)
	sort.Strings(sorteddlls)

	// and finally
	genGetSetWindowLongPtr(targetarch)

	// thanks to james4k in irc.freenode.net/#go-nuts
	tmpdir, err := ioutil.TempDir("", "windowsconstgen")
	if err != nil {
		panic(err)
	}
	genoutname := filepath.Join(tmpdir, "gen.go")
	f, err := os.Create(genoutname)
	if err != nil {
		panic(err)
	}

	t := template.Must(template.New("winconstgenout").Funcs(templateFuncs).Parse(outTemplate))
	err = t.Execute(f, &templateArgs{
		Consts:		consts,
		Structs:		structs,
		Funcs:		funcs,
		DLLs:		sorteddlls,
	})
	if err != nil {
		panic(err)
	}

	cmd := exec.Command("go", "run")
	cmd.Args = append(cmd.Args, goopts...)		// valid if len(goopts) == 0; in that case this will just be a no-op
	cmd.Args = append(cmd.Args, genoutname)
	f, err = os.Create(filepath.Join(pkgpath, "zconstants_windows_" + targetarch + ".go"))
	if err != nil {
		panic(err)
	}
	defer f.Close()
	cmd.Stdout = f
	cmd.Stderr = os.Stderr
	// we need to preserve the environment EXCEPT FOR the variables we're overriding
	// thanks to raggi and smw in irc.freenode.net/#go-nuts
	for _, ev := range os.Environ() {
		if strings.HasPrefix(ev, "GOOS=") ||
			strings.HasPrefix(ev, "GOARCH=") ||
			strings.HasPrefix(ev, "CGO_ENABLED=") {
			continue
		}
		cmd.Env = append(cmd.Env, ev)
	}
	cmd.Env = append(cmd.Env,
		"GOOS=windows",
		"GOARCH=" + targetarch,
		"CGO_ENABLED=1")		// needed as it's not set by default in cross-compiles
	err = cmd.Run()
	if err != nil {
		// TODO find a way to get the exit code
		os.Exit(1)
	}

	// TODO remove the temporary directory
}