The Velo CE Primer

Chapter 5Strings, Memory, Files and the Registry

Strings

Text inside your program is WCHAR: 16-bit UTF-16 code units, with L"literals". velo-toolchain builds with -fshort-wchar, so wchar_t is the same type and GDB prints WCHAR strings as text.

Formatting

wsprintfW(output, format, ...) is your formatter. It knows %d, %u, %x, %c, %s (a WCHAR string) and %hs (an 8-bit char string, widened as it's copied), with l for long, widths and zero padding like %08x. It doesn't know floating point, and it writes at most 1023 characters but otherwise doesn't know how big output is, so make the buffer big enough. To show a double, scale it to an integer and format the parts, as examples/maths does.

String functions

CE's coredll.dll exports the C library's wide string functions on both versions, and windows.h declares them: wcslen, wcscpy, wcsncpy, wcscat, wcscmp, wcsncmp, _wcsicmp, _wcsnicmp, wcschr, wcsrchr, wcsstr, wcstok, _wcsdup, _wcsupr, _wcslwr and _wtol for turning text into a number, among others. towlower and towupper are CE 2.0 only.

There's nothing for 8-bit char strings except memchr: no strlen, strcmp or atoi. Write the few you need. They're short, and a dozen of them cover a whole network client: velo-bluesky's src/text.c has its set.

UTF-8

The web, JSON and most files are UTF-8. CE 1.0 has no conversion functions at all, and CE 2.0's MultiByteToWideChar rejects CP_UTF8 with error 87. These two work on both, and let you decide what happens to bad input:

static int utf16_to_utf8(const WCHAR *text, char *output, int size) {
    static const unsigned char leads[] = {0, 0, 0xC0, 0xE0, 0xF0};
    int written = 0;
    for (; *text; text++) {
        DWORD code = *text;
        if (code >= 0xD800 && code < 0xDC00 && text[1] >= 0xDC00 && text[1] < 0xE000) {
            code = 0x10000 + ((code - 0xD800) << 10) + (text[1] - 0xDC00);
            text++;
        }
        int length = code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
        if (written + length >= size) {
            break;
        }
        for (int index = length - 1; index > 0; index--) {
            output[written + index] = (char)(0x80 | (code & 0x3F));
            code >>= 6;
        }
        output[written] = (char)(leads[length] | code);
        written += length;
    }
    output[written] = 0;
    return written;
}

The other direction is in the fetch example, which also turns \n into \r\n, as edit controls need:

static int utf8_to_edit_text(const char *text, int length, WCHAR *output, int size) {
    int written = 0;
    for (int index = 0; index < length && written < size - 2;) {
        unsigned char byte = (unsigned char)text[index];
        DWORD code = '?';
        int extra = byte >= 0xF0 ? 3 : byte >= 0xE0 ? 2 : byte >= 0xC0 ? 1 : 0;
        if (byte < 0x80) {
            code = byte;
        } else if (extra && index + extra < length) {
            code = byte & (0x3F >> extra);
            for (int follow = 1; follow <= extra; follow++) {
                code = (code << 6) | (text[index + follow] & 0x3F);
            }
        }
        index += extra + 1;
        if (code == '\n' && (written == 0 || output[written - 1] != '\r')) {
            output[written++] = '\r';
        }
        output[written++] = code > 0xFFFF ? '?' : (WCHAR)code;
    }
    output[written] = 0;
    return written;
}

Memory

LocalAlloc is your malloc. LPTR gives fixed, zeroed memory, which is what you want almost every time:

post_t *post = LocalAlloc(LPTR, sizeof(post_t));
if (!post) {
    return FALSE;
}
LocalFree(post);

LocalReAlloc(memory, size, LMEM_MOVEABLE) grows a block, possibly moving it, and LocalSize says how big one is. VirtualAlloc reserves big regions directly, for buffers in the hundreds of kilobytes. GlobalMemoryStatus reports what's free.

Check every allocation. On a desktop, running out is a theoretical problem. On a Velo it happens: a user with a full object store may have well under a megabyte for programs. When memory runs low CE sends WM_HIBERNATE (0x03FF) to running programs, asking them to free what they can, such as caches and pictures.

Patterns that help

Files and Folders

Files work as on desktop Windows, through handles:

static BOOL save_text(LPCWSTR path, const char *data, DWORD length) {
    DWORD written;
    HANDLE file = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (file == INVALID_HANDLE_VALUE) {
        return FALSE;
    }
    BOOL saved = WriteFile(file, data, length, &written, NULL) && written == length;
    CloseHandle(file);
    return saved;
}

Read with OPEN_EXISTING, GetFileSize and ReadFile. FindFirstFileW, FindNextFileW and FindClose list a folder. CreateDirectoryW, DeleteFileW, MoveFileW, CopyFileW and GetFileAttributesW do the rest. GetLastError says why something failed.

Where things go

These folders exist on both ROMs:

FolderFor
\My DocumentsThe user's documents.
\Program FilesInstalled programs, one folder each.
\WindowsSystem files (mostly in ROM), shared DLLs, help files, fonts in \Windows\Fonts.
\Windows\ProgramsStart > Programs shortcuts.
\Windows\DesktopDesktop shortcuts.
\Windows\StartUpShortcuts here run at startup.
\PC Card (CE 1.0), \Storage Card (CE 2.0)The PC Card, when one is in.
CE 2.0 also has \Temp. CE 1.0 doesn't.

Card folders have FILE_ATTRIBUTE_TEMPORARY (0x100) set as well as FILE_ATTRIBUTE_DIRECTORY. Look for that rather than a name, because the name depends on the version and the card:

static BOOL find_card(WCHAR *folder, int size) {
    WIN32_FIND_DATAW found;
    BOOL found_card = FALSE;
    HANDLE search = FindFirstFileW(L"\\*", &found);
    if (search == INVALID_HANDLE_VALUE) {
        return FALSE;
    }
    do {
        DWORD wanted = FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_TEMPORARY;
        if ((found.dwFileAttributes & wanted) == wanted && (int)wcslen(found.cFileName) + 2 < size) {
            folder[0] = '\\';
            for (int index = 0; index <= (int)wcslen(found.cFileName); index++) {
                folder[index + 1] = found.cFileName[index];
            }
            found_card = TRUE;
        }
    } while (!found_card && FindNextFileW(search, &found));
    FindClose(search);
    return found_card;
}

Where am I?

There's no current directory, so a program can't open "the file next to me" by name. On CE 2.0, GetModuleFileNameW(NULL, path, MAX_PATH) gives the program's full path: cut it at the last backslash. CE 1.0 doesn't have it. Either install to a known folder, or have whatever starts the program pass the folder on the command line, as the screenshot runner does for greeter.exe.

The Registry

Settings go in the registry, not in files. The convention is a key per program under HKEY_CURRENT_USER\Software. The form example reads and writes its settings like this:

static void read_number(HKEY key, LPCWSTR name, DWORD *value) {
    DWORD type;
    DWORD size = sizeof *value;
    DWORD stored;
    if (RegQueryValueExW(key, name, NULL, &type, (BYTE *)&stored, &size) == ERROR_SUCCESS && type == REG_DWORD) {
        *value = stored;
    }
}

static void load_account(account_t *account) {
    HKEY key;
    DWORD type;
    DWORD size = sizeof account->handle;
    if (RegOpenKeyExW(HKEY_CURRENT_USER, SETTINGS_KEY, 0, KEY_READ, &key) != ERROR_SUCCESS) {
        return;
    }
    if (RegQueryValueExW(key, L"Handle", NULL, &type, (BYTE *)account->handle, &size) != ERROR_SUCCESS || type != REG_SZ) {
        account->handle[0] = 0;
    }
    account->handle[HANDLE_MAX - 1] = 0;
    read_number(key, L"Server", &account->server);
    read_number(key, L"Refresh", &account->refresh);
    read_number(key, L"Pictures", &account->pictures);
    RegCloseKey(key);
}

static void save_account(const account_t *account) {
    HKEY key;
    DWORD disposition;
    if (RegCreateKeyExW(HKEY_CURRENT_USER, SETTINGS_KEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &key, &disposition) != ERROR_SUCCESS) {
        return;
    }
    RegSetValueExW(key, L"Handle", 0, REG_SZ, (const BYTE *)account->handle, (wcslen(account->handle) + 1) * sizeof(WCHAR));
    RegSetValueExW(key, L"Server", 0, REG_DWORD, (const BYTE *)&account->server, sizeof(DWORD));
    RegSetValueExW(key, L"Refresh", 0, REG_DWORD, (const BYTE *)&account->refresh, sizeof(DWORD));
    RegSetValueExW(key, L"Pictures", 0, REG_DWORD, (const BYTE *)&account->pictures, sizeof(DWORD));
    RegCloseKey(key);
}

Three things to remember. Sizes are in bytes, and a REG_SZ string's size includes its terminating zero. Reads can fail on the first run, so start from defaults and only overwrite what was found. And always check the type you got back, because another version of your program may have written something different. RegEnumKeyExW and RegEnumValueW list keys and values, and RegDeleteKeyW removes a key.

Time

GetLocalTime and GetSystemTime fill a SYSTEMTIME with the local time and UTC. GetTickCount counts milliseconds since the Velo started, for measuring how long things take. There's no time(): to get Unix time, go through a FILETIME, which counts 100-nanosecond steps since 1601:

static DWORD unix_time(void) {
    SYSTEMTIME now;
    FILETIME file_time;
    GetSystemTime(&now);
    SystemTimeToFileTime(&now, &file_time);
    unsigned long long ticks = ((unsigned long long)file_time.dwHighDateTime << 32) | file_time.dwLowDateTime;
    return (DWORD)(ticks / 10000000 - 11644473600ULL);
}

Internet timestamps are usually ISO 8601 in UTC, like 2026-10-04T12:00:00.000Z, which is a wsprintfW of the SYSTEMTIME fields. Remember that the Velo's clock may simply be wrong: a new Velo starts in 1996 or 1997 until someone sets it.

Processes and DLLs

Starting a program

static BOOL run(LPCWSTR program, LPWSTR arguments) {
    PROCESS_INFORMATION started;
    if (!CreateProcessW(program, arguments, NULL, NULL, FALSE, 0, NULL, NULL, NULL, &started)) {
        return FALSE;
    }
    CloseHandle(started.hThread);
    CloseHandle(started.hProcess);
    return TRUE;
}

Given a bare name, CE looks on the PC Card, then in \Windows, then in the root. For anything else, give the full path. Unlike desktop Windows, the arguments don't include the program's name, and the other parameters must be NULL, FALSE or 0. WaitForSingleObject(started.hProcess, INFINITE) waits for it to finish, on CE 2.0. On CE 1.0 a new process's handle reports finished straight away, so that doesn't work there.

Loading a DLL

#include <windows.h>

typedef int (*add_function)(int, int);
typedef int (*greeting_function)(LPWSTR, int);

int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPWSTR command_line, int show) {
    WCHAR greeting[64], text[128];
    LPCWSTR library_path = command_line[0] ? command_line : L"greet.dll";
    HMODULE library = LoadLibraryW(library_path);
    if (!library) {
        wsprintfW(text, L"Couldn't load %s", library_path);
        MessageBoxW(0, text, L"Greeter", MB_OK | MB_ICONERROR);
        return 1;
    }
    add_function add = (add_function)GetProcAddressW(library, L"Add");
    greeting_function get_greeting = (greeting_function)GetProcAddressW(library, L"Greeting");
    int sum = add(2, 3);
    get_greeting(greeting, 64);
    wsprintfW(text, L"%s\n2 + 3 = %d", greeting, sum);
    MessageBoxW(0, text, L"Greeter", MB_OK | MB_ICONINFORMATION);
    FreeLibrary(library);
    return 0;
}

Note GetProcAddressW: on CE the function name is a wide string. A DLL's functions are exported by name with EXPORTS in velo_add_library. LoadLibraryW with a bare name looks on the PC Card, in \Windows and in the root, but never in the program's folder, on either version. Put DLLs in \Windows or load them by full path.

CE also matches loaded DLLs by name alone. Microsoft's documentation warns that once \Windows\sample.dll is loaded, asking for \My Folder\sample.dll gives you the copy already loaded. Give your DLLs names nobody else will use.