Restoring BCLOCK
First up is BCLOCK. A little background.
I was learning C and C++ from books in high school in the late nineties. I’d just gotten to try out Windows 3.1 and was eager to write a Windows program, but didn’t have access to the compiler. I had Turbo C++ at home but of course no Windows SDK.
Once I reached university and had access to a computer lab with Windows for Workgroups 3.11 and Borland C++ 4.0, I could finally achieve my dream. Armed with a copy of Programming Windows by Charles Petzold, I got to work. My idea was a clock application but it could display the time in base 2, 8, 10, or 16. I don’t remember a lot about how long it took to build but I did eventually get it working.
The bare minimum
I found the directory with the code on my drive, and after stuffing it into a new git repository on my NAS, it looks like this.
c:\Users\gener\code\BCLOCK>dir
Volume in drive C has no label.
Volume Serial Number is C006-2D0A
Directory of c:\Users\gener\code\BCLOCK
2022-07-30 03:15 PM <DIR> .
2022-07-30 03:15 PM <DIR> ..
2022-07-30 03:15 PM <DIR> .git
2022-07-16 12:38 PM <DIR> .vs
2022-07-30 03:14 PM 647 Base_cvt.c
2022-07-30 03:14 PM 7,105 Bclock.c
2022-07-14 06:07 PM 214 BCLOCK.DEF
2022-07-30 03:14 PM 333 BCLOCK.H
2022-07-30 03:15 PM 486 BCLOCK.RC
2022-07-17 11:06 AM 336 Bclock.res
6 File(s) 9,121 bytes
4 Dir(s) 146,388,676,608 bytes free
I think all I have to do is compile the .c files into .obj files and link it with the .rc or .res or something. I’ve installed Visual Studio 2022 Community Edition which is incredible to me that it’s free. Compare that with the 90’s when you’d pay hundreds of dollars as a student for a product with 1/10th of the functionality.
Anyway, let’s start a Developer Command Prompt.
c:\Users\gener\code\BCLOCK>cl *.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.32.31332 for x86
Copyright (C) Microsoft Corporation. All rights reserved.
Base_cvt.c
Base_cvt.c(20): error C2065: 'new': undeclared identifier
Base_cvt.c(20): warning C4047: '=': 'char *' differs in levels of indirection from 'int'
Base_cvt.c(20): error C2143: syntax error: missing ';' before 'type'
Base_cvt.c(20): error C2143: syntax error: missing ';' before '['
Bclock.c
Bclock.c(7): warning C4229: anachronism used: modifiers on data are ignored
Bclock.c(7): error C2061: syntax error: identifier 'WndProc'
Bclock.c(7): error C2059: syntax error: ';'
Bclock.c(7): error C2059: syntax error: '<parameter-list>'
Bclock.c(9): warning C4028: formal parameter 1 different from declaration
Bclock.c(9): warning C4028: formal parameter 2 different from declaration
Bclock.c(21): error C2065: 'WndProc': undeclared identifier
Bclock.c(21): warning C4047: '=': 'WNDPROC' differs in levels of indirection from 'int'
Bclock.c(64): warning C4229: anachronism used: modifiers on data are ignored
Bclock.c(64): error C2061: syntax error: identifier 'WndProc'
Bclock.c(64): error C2059: syntax error: ';'
Bclock.c(64): error C2059: syntax error: '<parameter-list>'
Generating Code...
… and nothing was produced. Let’s look at the first error.
Base_cvt.c(20): error C2065: 'new': undeclared identifier
That’s odd. new
is a C++ feature, and this is a .c file. Should be able to fix it. The code is:
temp = new char[max_places + 1];
We’ll just replace that with malloc
:
temp = (char *) malloc(sizeof(char) * (max_places + 1));
Wait, I have a bad feeling. I am allocating some char
s in this function and returning a pointer. Is the call-site using free()
after it’s done? Let’s check… nope. I fixed that.
Let’s build it again!
c:\Users\gener\code\BCLOCK>cl *.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.32.31332 for x86
Copyright (C) Microsoft Corporation. All rights reserved.
Base_cvt.c
Bclock.c
Bclock.c(7): warning C4229: anachronism used: modifiers on data are ignored
Bclock.c(7): error C2061: syntax error: identifier 'WndProc'
Bclock.c(7): error C2059: syntax error: ';'
Bclock.c(7): error C2059: syntax error: '<parameter-list>'
Bclock.c(9): warning C4028: formal parameter 1 different from declaration
Bclock.c(9): warning C4028: formal parameter 2 different from declaration
Bclock.c(21): error C2065: 'WndProc': undeclared identifier
Bclock.c(21): warning C4047: '=': 'WNDPROC' differs in levels of indirection from 'int'
Bclock.c(64): warning C4229: anachronism used: modifiers on data are ignored
Bclock.c(64): error C2061: syntax error: identifier 'WndProc'
Bclock.c(64): error C2059: syntax error: ';'
Bclock.c(64): error C2059: syntax error: '<parameter-list>'
Generating Code...
One error down! I do have a Base_cvt.obj
so this is building binaries. The next error is the issue with “anachronisms” and WndProc
. My definition, which I would have gotten from Programming Windows, is:
long FAR PASCAL _export WndProc(HWND hwnd, UINT message, UINT wParam, LONG lParam)
Let’s see how Microsoft says it’s supposed to work now. Just a quick Google for “writing a WndProc” and I get Writing the Window Procedure. It looks like I want this now:
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, UINT wParam, LONG lParam)
Fixed. Let’s build it again!
c:\Users\gener\code\BCLOCK>cl *.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.32.31332 for x86
Copyright (C) Microsoft Corporation. All rights reserved.
Base_cvt.c
Bclock.c
Bclock.c(9): warning C4028: formal parameter 1 different from declaration
Bclock.c(9): warning C4028: formal parameter 2 different from declaration
Bclock.c(94): warning C4047: '=': 'ULONG_PTR' differs in levels of indirection from 'void *'
Generating Code...
Microsoft (R) Incremental Linker Version 14.32.31332.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:Base_cvt.exe
Base_cvt.obj
Bclock.obj
Bclock.obj : error LNK2019: unresolved external symbol __imp__CreateBrushIndirect@4 referenced in function _WndProc@16
Bclock.obj : error LNK2019: unresolved external symbol __imp__CreateFontIndirectA@4 referenced in function _WndProc@16
Bclock.obj : error LNK2019: unresolved external symbol __imp__DeleteObject@4 referenced in function _WndProc@16
[etc]
Base_cvt.exe : fatal error LNK1120: 35 unresolved externals
Okay, so it compiled, but cl.exe
also tried to link, and I didn’t provide it with the correct libraries to link with. Using the functions it couldn’t find, I can just figure out what to add. But first I need to tell cl.exe
that I want it to compile and not link. From looking at the help (cl /?
) it looks like I want cl /c
.
c:\Users\gener\code\BCLOCK>cl /c *.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.32.31332 for x86
Copyright (C) Microsoft Corporation. All rights reserved.
Base_cvt.c
Bclock.c
Bclock.c(9): warning C4028: formal parameter 1 different from declaration
Bclock.c(9): warning C4028: formal parameter 2 different from declaration
Bclock.c(93): warning C4047: '=': 'ULONG_PTR' differs in levels of indirection from 'void *'
Generating Code...
c:\Users\gener\code\BCLOCK>link *.obj user32.lib gdi32.lib comdlg32.lib /out:Bclock.exe
Microsoft (R) Incremental Linker Version 14.32.31332.0
Copyright (C) Microsoft Corporation. All rights reserved.
c:\Users\gener\code\BCLOCK>
It worked! Note that we’re statically linking some libraries here. There’s probably a way to dynamically link against a DLL but I’m not too worried here. Let’s run it and see.
It’s there, though it’s missing its menu. That makes sense because I never linked in the resources. Let’s try:
c:\Users\gener\code\BCLOCK>link *.obj Bclock.res user32.lib gdi32.lib comdlg32.lib /out:Bclock.exe
Microsoft (R) Incremental Linker Version 14.32.31332.0
Copyright (C) Microsoft Corporation. All rights reserved.
It works! With a menu and everything.
Building properly
Now, instead of remembering all this stuff next time I want to build it, we want a Makefile
. I also noticed that the Bclock.res
file is actually a compiled artifact of BCLOCK.RC
. You can generate it with rc BCLOCK.rc
. Time to write a Makefile. Interesting note: Microsoft’s tools don’t include make
but they do include nmake
which appears to be mostly compatible.
build: Base_cvt.obj Bclock.obj Bclock.res
link $? user32.lib gdi32.lib comdlg32.lib /out:Bclock.exe
Bclock.res: Bclock.rc
rc $?
%.obj: %.c
cl /c $?
clean:
del *.obj Bclock.res Bclock.exe
This is a simple one. Let’s also fix the memory leak from the char *
’s being returned, and the other casting warning.
Before:
wndclass.lpfnWndProc = WndProc ;
After:
wndclass.lpfnWndProc = (WNDPROC) WndProc ;
Before:
wsprintf(time_string,"%s:%s:%s%c",(LPSTR) base_cvt(Base,datetime->tm_hour),(LPSTR) base_cvt(Base,datetime->tm_min),(LPSTR) base_cvt(Base,datetime->tm_sec),dayhalf);
After:
LPSTR converted_hour = base_cvt(Base, datetime->tm_hour);
LPSTR converted_minute = base_cvt(Base, datetime->tm_min);
LPSTR converted_second = base_cvt(Base, datetime->tm_sec);
wsprintf(time_string,"%s:%s:%s%c", converted_hour, converted_minute, converted_second, dayhalf);
free(converted_hour);
free(converted_minute);
free(converted_second);
This is a good time to commit, and we can also make a .gitignore
:
*.obj
*.res
*.exe
Even better than before
Things are nice and tidy! Now, we can look at increasing the warning level by adding /W3
to the compiler arguments. That gives us:
Bclock.c(158): warning C4996: 'localtime': This function or variable may be unsafe. Consider using localtime_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
Looking at Microsoft’s documentation for localtime_s
, we see that it’s been cleaned up for safety. That means instead of:
struct tm* datetime;
time_t lTime;
datetime = localtime(&lTime);
We want:
struct tm datetime;
__time64_t lTime;
localtime_s(&datetime, &lTime);
Fixed. And now we can use datetime.
instead of dereferencing datetime->
. It’s possible that we were leaking tm
s on every WM_PAINT, too.
Wait a minute…
Why did I write my own base_cvt
function to convert a number to a string representation in an arbitrary base… isn’t that what itoa()
(and family) does? Turns out I can delete Base_cvt.c
entirely and just use _itoa_s()
on each integer I want to convert! I love it when I get to delete code.
Finishing touches
All that’s left is to convert my C++-style comments (//
) to C-style comments (/* */
), fix the whitespaces and tabbing, and I’m done! This is pretty clean now, since I’ve been committing at every checkpoint:
ryan@DESKTOP-VGILPE2:~/gener/code/BCLOCK$ git log --oneline
ef0c41e (HEAD, origin/master, origin/HEAD, master) use standard C formatting
934bfb6 fix for nmake and resolve all W3 compiler warnings
3f7238f remove unnecessary functions, convert to _itoa_s()
81deba1 fixed to compile in the year 2022
8b517b1 initial commit
If I had unlimited time
I would probably get a proper Visual Studio project working so that I could get the built-in debugger, use integrated tools like autocomplete, etc. Right now it’s just a Makefile
and that’s a good start.
I would like to make the application work with HiDPI displays. It’s really fuzzy right now.
The menu item for the arbitrary base function doesn’t work. It could either be removed, or made to work.
My WndProc
has a lot of static variables in it. I don’t like those. I’m kind of assuming this isn’t how it’s done anymore. Perhaps I should check out a more modern copy of Programming Windows from the library and see!
Closing comments
This was a really interesting project. It involves a program written by a 17-year-old for an OS I don’t have access to anymore, using a book that’s long out of print. Even worse, nobody really writes programs like this anymore; if you were writing this in 2022 you’d probably write it in .NET or even as an MFC application.
I’m happy I spent the time getting it working. I wonder if I’ll be digging it out in another > 20 years and trying to get it to work with Microsoft Visual Studio 2045!