Skip to content

Windows programming

OpenDoors supports native Windows doors built with current 32-bit or 64-bit MSVC and MinGW toolchains. A Windows door can use a socket inherited from a BBS, a caller-supplied communications handle, or local mode. The public API is the same API used on DOS and Unix-like systems, so most door logic should not need Windows-specific source code.

See Building and linking for the supported CMake options and commands. This page explains the decisions a Windows application must make at the source and process-integration level.

Choosing an application entry point

A console program normally begins in main() and receives an argc/argv array. A graphical Windows program may instead begin in WinMain() and receive the command line as one string. OpenDoors supports both arrangements.

For a console entry point, pass the argument vector to the explicit od_parse_cmd_line_cons() interface:

#include <OpenDoor.h>

int
main(int argc, char **argv)
{
    od_control.od_prog_name = "Example Door";
    od_parse_cmd_line_cons(argc, argv);
    od_init();

    od_printf("Welcome, %s!\n\r", od_control.user_name);
    od_get_key(TRUE);
    od_exit(0, FALSE);
    return 0;
}

As an equally supported alternative, define OD_WINDOWS_CONSOLE before including OpenDoor.h; the header then aliases od_parse_cmd_line() to od_parse_cmd_line_cons(), allowing the usual portable spelling:

#define OD_WINDOWS_CONSOLE
#include <OpenDoor.h>

int main(int argc, char **argv)
{
    od_parse_cmd_line(argc, argv);
    /* ... */
}

The installed OpenDoors::SharedConsole, OpenDoors::StaticConsole, and OpenDoors::StaticMTConsole CMake targets propagate that definition. They link the same library binaries as their non-Console counterparts; the target only selects the source-level parser spelling. Explicitly calling od_parse_cmd_line_cons() works with either target.

If the application uses WinMain(), pass the raw command-line string to the single-argument form of od_parse_cmd_line(). Also copy nCmdShow into od_control.od_cmd_show before initialization so the local window honors the launcher's requested show state.

#include <OpenDoor.h>

int WINAPI
WinMain(HINSTANCE instance, HINSTANCE previous,
        LPSTR command_line, int show_command)
{
    (void)instance;
    (void)previous;

    od_control.od_cmd_show = show_command;
    od_control.od_prog_name = "Example Door";
    od_parse_cmd_line(command_line);
    od_init();

    od_disp_str("Press a key to return to the BBS.\n\r");
    od_get_key(TRUE);
    od_exit(0, FALSE);
    return 0;
}

Do not call both command-line forms for the same launch. The parser applies standard OpenDoors switches and then invokes any application callbacks, so parsing the same input twice can process an application option twice.

OpenDoors checks the executable subsystem at run time. Calling the GUI parser from a console executable, or the console parser from a GUI executable, writes a diagnostic to standard error and the debugger and terminates with failure before processing any arguments. Calling od_init() directly needs no compile definition: it detects the subsystem and selects the matching local interface.

Portable sources can use main() on every current target. Code which has a specific need for WinMain() can guard only the entry-point wrapper with ODPLAT_WIN32 and put the door itself in an ordinary shared function.

Static and shared libraries

The CMake build produces independent static and shared targets when their corresponding build options are enabled. Installed CMake packages expose them as OpenDoors::Static and OpenDoors::Shared.

When linking OpenDoors::Shared, Windows applications link against the import library and distribute the matching OpenDoors DLL beside the executable or in another directory on the DLL search path. The executable and DLL must have compatible architectures: a 64-bit process cannot load a 32-bit DLL, and a 32-bit process cannot load a 64-bit DLL.

When linking OpenDoors::Static, the OpenDoors code is incorporated into the door executable. CMake propagates the OD_WIN32_STATIC definition through the target. If a non-CMake build links the static library directly, it must define OD_WIN32_STATIC while compiling every translation unit which includes OpenDoor.h; otherwise the header declares imported DLL symbols and the final link will fail or use the wrong linkage attributes.

Use one C runtime model consistently across all static objects in an application. In particular, do not pass allocations across modules built with incompatible MSVC runtime settings. The OpenDoors allocation APIs pair their operations explicitly—for example, memory returned by od_split_cmd_line() is released with od_free_split_cmd_line()—which also keeps ownership correct across a DLL boundary.

32-bit and 64-bit data

Windows uses a flat address space in both supported architectures. There are no near, far, or huge application pointers, and ordinary allocations are not limited to 64 KiB. Nevertheless, 64-bit compilation changes pointer-sized types and the layout of structures containing pointers. Never serialize an arbitrary in-memory C structure and assume that a different build will read it correctly.

OpenDoors defines fixed-width types such as INT16, INT32, WORD, and DWORD for values whose width is part of an external format. DWORD_PTR is wide enough to hold a native handle or pointer-sized integer. Use fixed-width fields in files and network protocols, write a defined byte order, and version records whose representation may change.

The public tODControl layout is produced by OpenDoor.h for the selected compiler; MSVC receives explicit byte packing, while the header does not apply a universal packed attribute to every Windows compiler. Include the header rather than reproducing the layout. An application built with a different ABI or architecture must use the matching OpenDoors library, and no packing rule makes pointers or Windows handles interchangeable between 32-bit and 64-bit processes.

Communication handles and Door32 sessions

Most present-day Windows BBS software launches a door with a connected socket described by door32.sys or with equivalent command-line information. OpenDoors recognizes that session during initialization and uses the supplied socket instead of opening a physical serial port. The numeric value is retained in od_control.od_open_handle, and od_control.od_use_socket selects socket semantics.

The handle belongs to the process according to the launch protocol. A parent which supplies a live handle must make it inheritable or duplicate it into the door process. A decimal value copied from another process is not useful by itself. The BBS and door must also agree on whether the value denotes a Winsock socket or a serial handle; the operations and shutdown rules are different.

Set a handle manually only when the launcher contract requires it. Do so before calling od_init() or any API function which can trigger automatic initialization. Once initialization has selected and opened the transport, changing the field does not migrate the active connection.

For a socket session, outgoing bytes are sent to the connected peer and remote input is read into OpenDoors' common input queue. Carrier state means that the socket remains connected, not that a physical modem's carrier-detect pin is high. od_set_dtr() has no useful modem line to manipulate on such a connection.

Physical serial operation remains available for compatible configurations, but current applications should prefer a live handle supplied by the BBS. It avoids two processes competing to open the port and preserves the BBS's chosen line settings. Historic Windows 95 SYSTEM.INI serial-port arbitration and external DTR restoration utilities are not requirements of current Windows systems and are not part of the supported setup.

Local and remote displays

The remote terminal and the local Windows display are distinct endpoints. OpenDoors sends door output through its terminal model, updates the virtual screen, and presents that state locally. The local console or window size does not define the remote caller's screen size. This distinction is important when the caller uses a larger terminal than the local display.

Block operations such as od_gettext(), od_puttext(), and od_scroll() operate on the OpenDoors screen model. They must not be replaced with direct Win32 console reads or writes: doing that bypasses remote output, changes behavior according to local window dimensions, and can expose local-only screen contents to the caller.

The established whole-screen snapshot functions preserve their original fixed buffer contract. New code which needs arbitrary terminal dimensions should use od_save_screen_size(), od_save_screen_ex(), and od_restore_screen_ex(). The size query lets the caller allocate exactly the storage required by the current virtual screen and lets restoration validate the snapshot before using it.

od_control.od_silent_mode suppresses the normal local user interface. It does not make the remote session local, and it does not suppress output to the caller. Conversely, od_control.od_force_local selects a local session and ignores a remote drop-file connection. These settings solve different problems.

A console-subsystem application which requests the local interface first uses its inherited Windows console. If it has none, OpenDoors allocates a console and releases that console during shutdown. A non-silent local-mode session terminates with the configured error level if no usable console can be inherited or allocated; it is never allowed to continue with a missing local display. A remote session may instead fall back to silent operation when console creation fails. od_control.od_silent_mode continues to bypass console creation and console state management.

Threads and serialized API access

The GUI-subsystem interface uses one Windows frame/UI worker. A console-subsystem application starts no OpenDoors worker: console keyboard input, status handling, communications, and timers all run in the same cooperative application flow as DOS. Internal synchronization remains compiled into both modes because the same library binary and input-queue implementation also serve GUI producer/consumer operation. The public API and ABI are process-global and are not internally serialized.

An application may hand the session between its threads, but only one thread may use OpenDoors at a time. Protect every API call, direct access to od_control or another public global, and the complete useful lifetime of a returned OpenDoors pointer with the same application lock. On Windows, a CRITICAL_SECTION is a convenient choice because it permits recursive entry by the same thread. The lock must remain held for the complete public call, including while that call waits internally.

In GUI mode, OpenDoors publishes the fields needed by its UI at API boundaries. The private UI worker does not make concurrent application access safe.

Callbacks configured through od_control normally run synchronously on the application thread making the active API call and may call the API recursively. A wrapper which acquires the application lock must therefore use recursive synchronization. The Windows help and configuration callbacks are the exceptions: they retain their frame-thread context and must not access any OpenDoors function, global, or returned pointer. They should queue application-owned work and return promptly.

Long application computations should periodically call od_kernel() from the serialized application flow. Most API calls invoke the kernel themselves, so ordinary input/output loops require no special pumping. A loop which spends several seconds doing only application work must allow OpenDoors to observe disconnects, time limits, and local operator commands.

Application metadata and native resources

Set od_control.od_prog_name, od_control.od_prog_version, and od_control.od_prog_copyright before initialization. The Windows interface uses these strings when it labels the application and constructs its About information.

od_control.od_app_icon accepts an application-provided HICON. The application owns the icon and must keep it valid while OpenDoors may display it. A resource icon can be loaded before initialization with the normal Windows resource APIs. The field exists only when ODPLAT_WIN32 is selected, so portable source must guard direct access to it.

od_control.od_help_callback allows the local interface to invoke application help. The callback should display or launch help without changing the active transport. It is a local operator facility; remote users do not receive a Windows help window.

The library contains its own Windows version resources. An application should also provide version information for its executable so Explorer and deployment tools report the door's identity independently of the OpenDoors DLL.

Shutdown and error reporting

Use od_exit() for normal termination. It writes back supported drop-file state, runs the configured before-exit callback, shuts down OpenDoors components, and optionally disconnects the caller. Calling the C runtime exit() directly skips those responsibilities.

If startup fails, inspect od_control.od_error and the ERR_* reference. Preserve the drop file and exact command line when reproducing launch failures. For handle problems, also record the process architecture, whether the handle is a socket or serial object, and how the parent made it available to the child.

Windows loader failures happen before main() or WinMain() and therefore cannot be reported by OpenDoors. A missing DLL, architecture mismatch, or missing dependent runtime must be diagnosed with the Windows event log, loader diagnostic tools, or a terminal launch which keeps the error visible. Static linking removes the OpenDoors DLL deployment step but does not remove the need to deploy the runtime selected by the application's toolchain.