Over 20 years ago I used to write Delphi and VB in the MS Windows world.
A decision by MS to remove something from Windows libraries helped me switching to Linux, and I never really looked back.
But now I have a client that wish for my service to work on their Windows (instead of Linux).
I started looking at the Windows API again (hello old MSDN), and understood how much have changed since I last touched the API.
Most documentation are for the C# dev's, but I never even written an Hello world
with it, and found out how much effort does the working with IMMDevice
requires from me, and all my client requires from me here is to play a wav
file for the default Audio device.
So I started thinking how to do that. Google reminded me by accident the "good" old world of waveform
and PlaySound
function.
After several hours, I came up with a working copy of Go over Windows (my development is still on Linux, and cross compiled to Windows).
I do not own a Windows copy, only Linux(es) for so many years now, so my tests are on my Client's Windows machine, while I'm connected using RDP (the pain).
Binding ...
Windows usually provides C based ABI for API calls (unless you are using .NET apps, and then you are using CLR based virtual machine's ABI).
There are two ways for me to load it..., and I decided to bind myself on run-time
, using Go's syscall.DLL
.
First step
My first step was going over the mmsystem.h
header file, just to find out that it's a meta
header for including many headers.
Quick grep
(thanks Google for that command btw) I found that all I need is under the playsoundapi.h
header.
I copied all the integer based constants and just made it Go syntax friendly (but not Linter friendly).
...
const (
SND_SYNC uint = 0x0000 /* play synchronously (default) */
SND_ASYNC uint = 0x0001 /* play asynchronously */
SND_NODEFAULT uint = 0x0002 /* silence (!default) if sound not found */
SND_MEMORY uint = 0x0004 /* pszSound points to a memory file */
SND_LOOP uint = 0x0008 /* loop the sound until next sndPlaySound */
SND_NOSTOP uint = 0x0010 /* don't stop any currently playing sound */
...
Initialize syscall.DLL
I can implement some nice functions after having the constants.
After some thinking, I decided to create a wrapper using a more Go friendly function that calls the ugly Windows API functions.
I started by defining a call to the .dll
file:
...
var (
mmsystem = syscall.MustLoadDLL("winmm.dll")
...
syscall.MustLoadDLL
loads a .dll
file to memory, and if fails the loading will panic my process.
mmsystem
is now a pointer of struct syscall.DLL
.
Note:
Using Go, every function with Must
will panic
if there is an error.
Memory address for functions
Now that I have the .dll
file all loaded an warm up for me (thanks Go), I can get do some stuff with it.
In this case, I need some API's function love.
...
playSound = mmsystem.MustFindProc("PlaySound")
sndPlaySoundA = mmsystem.MustFindProc("sndPlaySoundA")
sndPlaySoundW = mmsystem.MustFindProc("sndPlaySoundW")
)
...
The old world of Windows (maybe also in the new?) the NT
/server
based run-time supported Unicode (UTF-16BE), while home/pro etc.. editions supported mostly extended ASCII with some code page
encoding for human languages.
The suffix of A
and W
provides support for the two types.
The A
suffix is for ASCII
and the W
suffix is for Wide char
encoding - meaning multi-byte encoding.
I decided to support both of them in order to learn how Go works with both of them.
Wrapping and gifting
I loaded the functions that I wish to use.
Now it's time to wrap the API functions in order to have a nice Go like code rather then repeating my code usage each time.
When I used MustFindProc
, I actually got a new pointer to a struct named Proc
.
Proc
have one interesting function: Call
. It actually execute the arguments we need (up to 18 arguments), using syscallXX
for us instead of us writing this ugly code.
Call
returns three arguments, but I ignored them (bad dev, bad dev) - the last argument is an error that might have happened.
...
// PlaySound play sound in Windows
func PlaySound(sound string, hmod int, flags uint) {
s16, _ := syscall.UTF16PtrFromString(sound)
playSound.Call(uintptr(unsafe.Pointer(s16)), uintptr(hmod), uintptr(flags))
}
// SndPlaySoundA play sound file in Windows
func SndPlaySoundA(sound string, flags uint) {
b := append([]byte(sound), 0)
sndPlaySoundA.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(flags))
}
...
On PlaySound
I converted the Go string (UTF-8) to syscall
's UTF16BE
(c-)string.
On SndPlaySoundA
I converted Go string into ASCII, using slice of byte, and added a null terminated at the end of the slice to let C based code when the string needs to end.
On all functions (PlaySound
and SndPlaySoundX
) there is some ugly code, so the wrapper hides it, giving us a nice Go-ish syntax instead.
When using unsafe
, it is important to note that CPU's endian, and so does memory addresses are not cross platformed as much as other code.
So it is important to understand that usage of code that touches unsafe
contain some possible pitfalls that will be discovered on run-time only, and on some machines only. FUN!
The full Code
Warning
On a real production code, there are more steps to take - Always check and validate errors, never ignore them as I did
Top comments (1)
I'd also like to point out that the golang.org/x/sys/windows package includes a command mkwinsyscall that can do most of the work for you in creating a function that calls windows API's.
I created my own PlaySound library for this by creating a file with:
Then after calling the command:
A file called zsyscall_windows.go was generated that handles all the heavy lifting of translating the go values into windows values. An added benefit is that the generated file will follow sys/windows conventions like loading the dll library with windows.NewLazySystemDLL which limits the search path to the windows system path to help prevent dll preloading.