Skip to content

Commit 39c8d2b

Browse files
committed
os: parse command line without shell32.dll
Go uses CommandLineToArgV from shell32.dll to parse command line parameters. But shell32.dll is slow to load. Implement Windows command line parsing in Go. This should make starting Go programs faster. I can see these speed ups for runtime.BenchmarkRunningGoProgram on my Windows 7 amd64: name old time/op new time/op delta RunningGoProgram-2 11.2ms ± 1% 10.4ms ± 2% -6.63% (p=0.000 n=9+10) on my Windows XP 386: name old time/op new time/op delta RunningGoProgram-2 19.0ms ± 3% 12.1ms ± 1% -36.20% (p=0.000 n=10+10) on @egonelbre Windows 10 amd64: name old time/op new time/op delta RunningGoProgram-8 17.0ms ± 1% 15.3ms ± 2% -9.71% (p=0.000 n=10+10) This CL is based on CL 22932 by John Starks. Fixes #15588. Change-Id: Ib14be0206544d0d4492ca1f0d91fac968be52241 Reviewed-on: https://go-review.googlesource.com/37915 Reviewed-by: Brad Fitzpatrick <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent cc48b01 commit 39c8d2b

File tree

3 files changed

+208
-11
lines changed

3 files changed

+208
-11
lines changed

src/os/exec_windows.go

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,17 +97,79 @@ func findProcess(pid int) (p *Process, err error) {
9797
}
9898

9999
func init() {
100-
var argc int32
101-
cmd := syscall.GetCommandLine()
102-
argv, e := syscall.CommandLineToArgv(cmd, &argc)
103-
if e != nil {
104-
return
100+
p := syscall.GetCommandLine()
101+
cmd := syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(p))[:])
102+
if len(cmd) == 0 {
103+
arg0, _ := Executable()
104+
Args = []string{arg0}
105+
} else {
106+
Args = commandLineToArgv(cmd)
105107
}
106-
defer syscall.LocalFree(syscall.Handle(uintptr(unsafe.Pointer(argv))))
107-
Args = make([]string, argc)
108-
for i, v := range (*argv)[:argc] {
109-
Args[i] = syscall.UTF16ToString((*v)[:])
108+
}
109+
110+
// appendBSBytes appends n '\\' bytes to b and returns the resulting slice.
111+
func appendBSBytes(b []byte, n int) []byte {
112+
for ; n > 0; n-- {
113+
b = append(b, '\\')
114+
}
115+
return b
116+
}
117+
118+
// readNextArg splits command line string cmd into next
119+
// argument and command line remainder.
120+
func readNextArg(cmd string) (arg []byte, rest string) {
121+
var b []byte
122+
var inquote bool
123+
var nslash int
124+
for ; len(cmd) > 0; cmd = cmd[1:] {
125+
c := cmd[0]
126+
switch c {
127+
case ' ', '\t':
128+
if !inquote {
129+
return appendBSBytes(b, nslash), cmd[1:]
130+
}
131+
case '"':
132+
b = appendBSBytes(b, nslash/2)
133+
if nslash%2 == 0 {
134+
// use "Prior to 2008" rule from
135+
// http://daviddeley.com/autohotkey/parameters/parameters.htm
136+
// section 5.2 to deal with double double quotes
137+
if inquote && len(cmd) > 1 && cmd[1] == '"' {
138+
b = append(b, c)
139+
cmd = cmd[1:]
140+
}
141+
inquote = !inquote
142+
} else {
143+
b = append(b, c)
144+
}
145+
nslash = 0
146+
continue
147+
case '\\':
148+
nslash++
149+
continue
150+
}
151+
b = appendBSBytes(b, nslash)
152+
nslash = 0
153+
b = append(b, c)
154+
}
155+
return appendBSBytes(b, nslash), ""
156+
}
157+
158+
// commandLineToArgv splits a command line into individual argument
159+
// strings, following the Windows conventions documented
160+
// at http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV
161+
func commandLineToArgv(cmd string) []string {
162+
var args []string
163+
for len(cmd) > 0 {
164+
if cmd[0] == ' ' || cmd[0] == '\t' {
165+
cmd = cmd[1:]
166+
continue
167+
}
168+
var arg []byte
169+
arg, cmd = readNextArg(cmd)
170+
args = append(args, string(arg))
110171
}
172+
return args
111173
}
112174

113175
func ftToDuration(ft *syscall.Filetime) time.Duration {

src/os/export_windows_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package os
77
// Export for testing.
88

99
var (
10-
FixLongPath = fixLongPath
11-
NewConsoleFile = newConsoleFile
10+
FixLongPath = fixLongPath
11+
NewConsoleFile = newConsoleFile
12+
CommandLineToArgv = commandLineToArgv
1213
)

src/os/os_windows_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,3 +723,137 @@ func TestStatPagefile(t *testing.T) {
723723
}
724724
t.Fatal(err)
725725
}
726+
727+
// syscallCommandLineToArgv calls syscall.CommandLineToArgv
728+
// and converts returned result into []string.
729+
func syscallCommandLineToArgv(cmd string) ([]string, error) {
730+
var argc int32
731+
argv, err := syscall.CommandLineToArgv(&syscall.StringToUTF16(cmd)[0], &argc)
732+
if err != nil {
733+
return nil, err
734+
}
735+
defer syscall.LocalFree(syscall.Handle(uintptr(unsafe.Pointer(argv))))
736+
737+
var args []string
738+
for _, v := range (*argv)[:argc] {
739+
args = append(args, syscall.UTF16ToString((*v)[:]))
740+
}
741+
return args, nil
742+
}
743+
744+
// compareCommandLineToArgvWithSyscall ensures that
745+
// os.CommandLineToArgv(cmd) and syscall.CommandLineToArgv(cmd)
746+
// return the same result.
747+
func compareCommandLineToArgvWithSyscall(t *testing.T, cmd string) {
748+
syscallArgs, err := syscallCommandLineToArgv(cmd)
749+
if err != nil {
750+
t.Fatal(err)
751+
}
752+
args := os.CommandLineToArgv(cmd)
753+
if want, have := fmt.Sprintf("%q", syscallArgs), fmt.Sprintf("%q", args); want != have {
754+
t.Errorf("testing os.commandLineToArgv(%q) failed: have %q want %q", cmd, args, syscallArgs)
755+
return
756+
}
757+
}
758+
759+
func TestCmdArgs(t *testing.T) {
760+
tmpdir, err := ioutil.TempDir("", "TestCmdArgs")
761+
if err != nil {
762+
t.Fatal(err)
763+
}
764+
defer os.RemoveAll(tmpdir)
765+
766+
const prog = `
767+
package main
768+
769+
import (
770+
"fmt"
771+
"os"
772+
)
773+
774+
func main() {
775+
fmt.Printf("%q", os.Args)
776+
}
777+
`
778+
src := filepath.Join(tmpdir, "main.go")
779+
err = ioutil.WriteFile(src, []byte(prog), 0666)
780+
if err != nil {
781+
t.Fatal(err)
782+
}
783+
784+
exe := filepath.Join(tmpdir, "main.exe")
785+
cmd := osexec.Command("go", "build", "-o", exe, src)
786+
cmd.Dir = tmpdir
787+
out, err := cmd.CombinedOutput()
788+
if err != nil {
789+
t.Fatalf("building main.exe failed: %v\n%s", err, out)
790+
}
791+
792+
var cmds = []string{
793+
``,
794+
` a b c`,
795+
` "`,
796+
` ""`,
797+
` """`,
798+
` "" a`,
799+
` "123"`,
800+
` \"123\"`,
801+
` \"123 456\"`,
802+
` \\"`,
803+
` \\\"`,
804+
` \\\\\"`,
805+
` \\\"x`,
806+
` """"\""\\\"`,
807+
` abc`,
808+
` \\\\\""x"""y z`,
809+
"\tb\t\"x\ty\"",
810+
` "Брад" d e`,
811+
// examples from https://msdn.microsoft.com/en-us/library/17w5ykft.aspx
812+
` "abc" d e`,
813+
` a\\b d"e f"g h`,
814+
` a\\\"b c d`,
815+
` a\\\\"b c" d e`,
816+
// http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV
817+
// from 5.4 Examples
818+
` CallMeIshmael`,
819+
` "Call Me Ishmael"`,
820+
` Cal"l Me I"shmael`,
821+
` CallMe\"Ishmael`,
822+
` "CallMe\"Ishmael"`,
823+
` "Call Me Ishmael\\"`,
824+
` "CallMe\\\"Ishmael"`,
825+
` a\\\b`,
826+
` "a\\\b"`,
827+
// from 5.5 Some Common Tasks
828+
` "\"Call Me Ishmael\""`,
829+
` "C:\TEST A\\"`,
830+
` "\"C:\TEST A\\\""`,
831+
// from 5.6 The Microsoft Examples Explained
832+
` "a b c" d e`,
833+
` "ab\"c" "\\" d`,
834+
` a\\\b d"e f"g h`,
835+
` a\\\"b c d`,
836+
` a\\\\"b c" d e`,
837+
// from 5.7 Double Double Quote Examples (pre 2008)
838+
` "a b c""`,
839+
` """CallMeIshmael""" b c`,
840+
` """Call Me Ishmael"""`,
841+
` """"Call Me Ishmael"" b c`,
842+
}
843+
for _, cmd := range cmds {
844+
compareCommandLineToArgvWithSyscall(t, "test"+cmd)
845+
compareCommandLineToArgvWithSyscall(t, `"cmd line"`+cmd)
846+
compareCommandLineToArgvWithSyscall(t, exe+cmd)
847+
848+
// test both syscall.EscapeArg and os.commandLineToArgv
849+
args := os.CommandLineToArgv(exe + cmd)
850+
out, err := osexec.Command(args[0], args[1:]...).CombinedOutput()
851+
if err != nil {
852+
t.Fatalf("runing %q failed: %v\n%v", args, err, string(out))
853+
}
854+
if want, have := fmt.Sprintf("%q", args), string(out); want != have {
855+
t.Errorf("wrong output of executing %q: have %q want %q", args, have, want)
856+
continue
857+
}
858+
}
859+
}

0 commit comments

Comments
 (0)