Building Windows executables on Linux WITH CGo

4,717 views
Skip to first unread message

Michael Tiller

unread,
Jan 28, 2015, 2:14:43 PM1/28/15
to golan...@googlegroups.com
All,

  I spent a fair amount of time researching this topic and couldn't really find a comprehensive answer on the topic.  The closest I came was this thread on StackOverflow (so big kudos to OneOfOne there!):


  Using the information from there, I managed to put together a relatively simple process of building Windows binaries under Linux.  Not only does my project require CGo but it also has to load DLLs behind the scenes and call functions in those DLLs.  So it is really exercising the toolchain.

  I actually have a Dockerfile for my build environment that I could clean up if anybody is interested in an image like that.

  I just wanted to post a summary of what I found here in case anybody else comes along and is interested in doing what I did.

  The first step was to download Go from source so I could build it myself, i.e.,

$ cd go
$ git checkout go1.4.1

  I also needed to make sure I had several different gcc packages installed on my Ubuntu machine to support this process.  I installed them with:

$ sudo apt-get install gcc-multilib
$ sudo apt-get install gcc-mingw-w64

  The trickiest part though was building the various bits.  I used make.bash.  Perhaps there is a way to do this with all.bash.  But I wasn't sure what that actually did and whether it would pick up the particular environment variables I wanted to specify.  So I ran make.bash multiple times.  I start by building the Windows stuff (I'll explain why in a sec):

$ cd src
$ GOOS=windows GOARCH=386 CGO_ENABLED=1 CXX_FOR_TARGET=i686-w64-mingw32-g++ CC_FOR_TARGET=i686-w64-mingw32-gcc ./make.bash

  This enabled CGO and specifies special C and C++ compilers to use in order to build the native exes.  This is for the win32 platform.  Then, I run the command again for win64 but add the --no-clean option:

$ GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CXX_FOR_TARGET=x86_64-w64-mingw32-g++ CC_FOR_TARGET=x86_64-w64-mingw32-gcc ./make.bash --no-clean

  Finally, I do a build for native Linux (again with --no-clean)

$ GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CXX_FOR_TARGET=g++ CC_FOR_TARGET=gcc ./make.bash --no-clean

  Now you might wonder why I built them in this order.  I played around with this quite a bit actually.  The issue is that these builds are somehow "stateful".  The last build seems to set some defaults somewhere or something.  You'd think that it would be sufficient to just specify GOOS and GOARCH whenever you ran the go tool, but it isn't.  If I build Linux first and Windows last, the Linux version wouldn't work (because the last build was Windows and that messed up Linux).  And yes, building Linux last messes up the Windows builds.  BUT, that can be overcome by setting some environment variables.

  The way I dealt with this was to create some special scripts:

$ echo 'export GOOS=windows; export GOARCH=386; export CGO_ENABLED=1; export CXX=i686-w64-mingw32-g++; export CC=i686-w64-mingw32-gcc' > go-win32

$ echo 'export GOOS=windows; export GOARCH=amd64; export CGO_ENABLED=1; export CXX=x86_64-w64-mingw32-g++; export CC=x86_64-w64-mingw32-gcc' > go-win64

  Notice the fact that I explicitly set the values of CXX and CC.  What I normally do is just source these, i.e.,  (NOTE THE '.')

$ . go-win32

  This sets the environment variables needed to perform a win32 build.  After that, I can just use the go tool as usual, e.g.

$ go build

  Sourcing these files is a minor annoyance.  I wish I could just get away with:

$ GOOS=windows GOARCH=386 go build

  But it doesn't work.  You even see a hint of this in the StackOverflow question when the OP talks about the error:

gcc: error: unrecognized command line option ‘-mthreads
  That is a symptom of the "statefulness" I referred to earlier.  I suspect someone who understand the Go build process and the way the environment is managed for cross-compilation could probably figure out a way to get rid of the statefulness and make the cross-compilation easier (or maybe I'm an idiot and there is already a way to do this...entirely possible).

  One final point worth making is that if you want to test stuff, you won't be able to...at least not in the way you expect.  This process is for building native executables under Windows.  It doesn't let you run them.  Fortunately, you can install Wine and then run them.  What I do is first build the test exe with:

$ go test -c

  ...and then I run them with just:

$ wine pkgname.test.exe

  You might ask...why not just use Wine to do the build.  Well, there are a couple of reasons.  First, you have to create a development toolchain under Windows which means installing some Windows stuff under Wine (git, mercurial, TDM...and, yes, I know about Winstrap).  That part, while tedious, is doable.  But the main reason is that it doesn't actually work.  What I mean by that is that it may very well work in general.  But it didn't work in my case.  I mentioned that I'm using CGo and DLLs.  For whatever reason, this combination is quite problematic.  Plus, I really find this approach much simpler.  As I said, I can easily make a Dockerfile that builds to complete environment and then just fire it up for builds.

  Whew!  The main point here is that it took a fair amount of time for me to figure this out even after a lot of Googling so I'm going to assume that this isn't obvious (at least to everyone) so I wanted to make a note of what I figured out in case somebody finds themselves in the same boat as me.  I would, of course, be thrilled to find that this can be made even more streamlined.  So if you have experience with this, please comment.

  Thanks!

--
Mike

Tamás Gulácsi

unread,
Jan 29, 2015, 12:56:14 AM1/29/15
to golan...@googlegroups.com
Thanks!

I'm sure this is a great help for a big bunch of gophers!

Naveen

unread,
Mar 27, 2015, 2:13:15 AM3/27/15
to golan...@googlegroups.com
Thanks Mike, 


sunrai...@gmail.com

unread,
Feb 18, 2016, 11:15:52 AM2/18/16
to golang-nuts
Resolved my problem.  Go1.5.3 + Ubuntu, build Win32 and Win64 binaries.

Jackman

unread,
Jun 29, 2018, 11:58:15 AM6/29/18
to golang-nuts
Hello, Mike!

Has any of this changed since you last wrote this (three years ago)?

I'm currently on Go 1.10.3, and I'm trying to replicate your results. Unfortunately, I'm getting stuck, and I'm hoping you're still cross-compiling and willing to help.

Here is my Makefile:

export CC_FOR_windows_386=i686-w64-mingw32-gcc
export CC_FOR_windows_amd64=x86_64-w64-mingw32-gcc
export CGO_ENABLED=1
export CXX_FOR_windows_386=i686-w64-mingw32-g++
export CXX_FOR_windows_amd64=x86_64-w64-mingw32-g++
export GOOS=windows

hello
-w32.exe: export GOARCH=386
hello
-w32.exe:
 go build
-o hello-w32.exe

hello
-w64.exe: export GOARCH=amd64
hello
-w64.exe:
 go build
-o hello-w64.exe

clean
:
 rm
-rf *.h *.a *.exe

.PHONY: clean

This is the code I'm trying to compile:

package main

// typedef int (*intFunc) ();
//
// int
// bridge_int_func(intFunc f)
// {
// return f();
// }
//
// int fortytwo()
// {
// return 42;
// }
import "C"
import "fmt"

func main
() {
 f
:= C.intFunc(C.fortytwo)
 fmt
.Println(int(C.bridge_int_func(f)))
 
// Output: 42
}

The challenge is compiling for 64-bit Windows. With 32-bit, it does just fine, and I can run it in my VM.

This is the output of 'make hello-w64.exe':

go build -o hello-w64.exe
# runtime/cgo
cc1
: sorry, unimplemented: 64-bit mode not compiled in
Makefile:14: recipe for target 'hello-w64.exe' failed
make
: *** [hello-w64.exe] Error 2

I tried the installation process you described in your original email, but it doesn't seem to help. The reference to 'cc1' makes me think that it may be referencing the wrong compiler. I've specified the compiler in the environment in the same way for 386, which works, making me question that assumption. I just don't know enough to draw serious conclusions.

As I write this, I'm thinking that I'm gonna try fooling around with invoking cgo directly to examine the intermediate files and try to compile them.

Let me know if you have any suggestions. Thank you.

Andrew Jackman

Robert Johnstone

unread,
Jun 29, 2018, 4:34:42 PM6/29/18
to golang-nuts
Hello,

Are you sure that you need CGO?  On windows, the syscall package has tools for loading DLLs, getting the address of procedures, and calling them from Go.  CGO is not required.  Take a look at the package github.com/lxn/win as an example.  I will double-check later today, but I believe you can cross compile without difficulty.

Robert

Lars Seipel

unread,
Jun 29, 2018, 5:15:00 PM6/29/18
to Jackman, golang-nuts
On Fri, Jun 29, 2018 at 08:58:14AM -0700, Jackman wrote:
> go build -o hello-w64.exe
> # runtime/cgo
> cc1: sorry, unimplemented: 64-bit mode not compiled in
> Makefile:14: recipe for target 'hello-w64.exe' failed
> make: *** [hello-w64.exe] Error 2

Where does your x86_64-w64-mingw32-gcc program come from and is it
really able to output AMD64 code?

Try compiling a trivial C program into a w64 executable with it and see
if you get the same message. You may also want to use the '-x' flag to
'go build' to see the command line it is passing to the C compiler.

Jackman

unread,
Jun 29, 2018, 6:34:14 PM6/29/18
to Robert Johnstone, golang-nuts
The end goal is to generate a DLL which can be used by an office application as a plug-in. It just needs the DLL to comply with a basic contract. I thought my only option was to use cgo to generate that DLL? I'm happy to take an easier path if that's the option. Thanks! 

--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/AlQvxD7wv30/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jackman

unread,
Jun 29, 2018, 6:36:37 PM6/29/18
to Lars Seipel, golang-nuts
Thanks for the reply! In my experiments this morning, I was able to use that compiler to build a C program for Windows. I guess I'll need to verify that it actually was a 64-bit program. Once I get home, I'll check and provide you (and myself) with a definitive answer.

Have a great day! 

Jackman

unread,
Jun 30, 2018, 1:20:23 AM6/30/18
to Lars Seipel, golang-nuts
Here's my quick update.

I compiled the 32-bit and 64-bit versions of the mingw compiler, and I ran them through GNU File. This is what I got:

C:\Program Files (x86)\GnuWin32\bin>file.exe e:\hello-w64.exe
e:\hello-w64.exe; PE32+ executable for MS Windows (console) Mono/.Net assembly
C:\Program Files (x86)\GnuWin32\bin>file.exe e:\hello-w32.exe
e:\hello-w32.exe; PE32 executable for MS Windows (console) Intel 80386 32-bit

What I understand is that PE32+ is indeed a 64-bit executable. To reiterate, this was created with x86_64-w64-mingw32-gcc.

Here is the Debian description:

jackman@dorito:~$ sudo apt show mingw-w64
Package: mingw-w64
Version: 5.0.1-1
Priority: extra
Section: devel
Maintainer: Stephen Kitt <sk...@debian.org>
Installed-Size: 47.1 kB
Depends: gcc-mingw-w64, g++-mingw-w64
Homepage: http://mingw-w64.sf.net
Tag: role::metapackage
Download-Size: 12.3 kB
APT-Manual-Installed: yes
APT-Sources: http://debian.usu.edu/debian stretch/main amd64 Packages
Description: Development environment targeting 32- and 64-bit Windows
 MinGW-w64 provides a development and runtime environment for 32- and
 64-bit (x86 and x64) Windows applications using the Windows API and
 the GNU Compiler Collection (gcc).
 .
 This metapackage provides the MinGW-w64 development environment,
 including C and C++ compilers. Ada, Fortran, Objective-C and
 Objective-C++ compilers are available respectively in the
 gnat-mingw-w64, gfortran-mingw-w64, gobjc-mingw-w64 and
 gojbc++-mingw-w64 packages.

If you're interested, here is the Makefile:

export CC_FOR_windows_386=i686-w64-mingw32-gcc
export CC_FOR_windows_amd64=x86_64-w64-mingw32-gcc
export CGO_ENABLED=1
export CXX_FOR_windows_386=i686-w64-mingw32-g++
export CXX_FOR_windows_amd64=x86_64-w64-mingw32-g++
hello-w32.exe:
   $(CC_FOR_windows_386) hello.c -o hello-w32.exe
hello-w64.exe:
   $(CC_FOR_windows_amd64) hello.c -o hello-w64.exe

clean:
   rm -rf *.h *.a *.exe
.PHONY: clean

Here is the C code:

#include <stdio.h>
int main(){
    printf("Hello World!\n");
    return 0;
}

This is the output of the previously described cgo code with '-x':

$ go build -x -o hello-w64.exe
WORK=/tmp/go-build134704648
mkdir -p $WORK/b027/
cd /home/jackman/opt/go/src/runtime/cgo
CGO_LDFLAGS='"-g" "-O2"' /home/jackman/opt/go/pkg/tool/linux_amd64/cgo -objdir $WORK/b027/ -importpath runtime/cgo -import_runtime_cgo=false -import_syscall=false -- -I $WORK/b027/ -g -O2 -Wall -Werror ./cgo.go
# runtime/cgo
cc1: sorry, unimplemented: 64-bit mode not compiled in

All of the environment variables are set per the Makefile in my previous post. I don't know how to interpret this, however. Any help would be greatly appreciated. Thank you.

Andrew Jackman
kd7...@gmail.com

Lars Seipel

unread,
Jun 30, 2018, 6:33:28 PM6/30/18
to Jackman, golang-nuts
On Fri, Jun 29, 2018 at 11:19:46PM -0600, Jackman wrote:
> Here's my quick update.
>
> I compiled the 32-bit and 64-bit versions of the mingw compiler, and I ran
> them through GNU File. This is what I got:
>
> C:\Program Files (x86)\GnuWin32\bin>file.exe e:\hello-w64.exe
> > e:\hello-w64.exe; PE32+ executable for MS Windows (console) Mono/.Net
> > assembly
> > C:\Program Files (x86)\GnuWin32\bin>file.exe e:\hello-w32.exe
> > e:\hello-w32.exe; PE32 executable for MS Windows (console) Intel 80386
> > 32-bit
>
>
> What I understand is that PE32+ is indeed a 64-bit executable. To
> reiterate, this was created with x86_64-w64-mingw32-gcc.
>
> Here is the Debian description:

Using the mingw-w64 Debian package and the source file from your previous
mail, I end up with a seemingly ok win64 executable:

src/a# cat a.go
> package main
>
> // typedef int (*intFunc) ();
> //
> // int
> // bridge_int_func(intFunc f)
> // {
> // return f();
> // }
> //
> // int fortytwo()
> // {
> // return 42;
> // }
> import "C"
> import "fmt"
>
> func main() {
> f := C.intFunc(C.fortytwo)
> fmt.Println(int(C.bridge_int_func(f)))
> // Output: 42
> }

src/a# GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build
src/a# file a.exe
> a.exe: PE32+ executable (console) x86-64, for MS Windows

When calling the file program from Debian on the exe resulting from the
C hello world program, does it also say "Mono/.Net assembly" or does
this only happen with the GnuWin32 file program?
Reply all
Reply to author
Forward
0 new messages