Replace form store with stream-from-disk and dynamic form IDs

Form IDs were baked into .form files at conversion time, preventing a
form from being displayed more than once.  dfm2form now writes a
placeholder ID (0), and formServerSendForm streams the file directly
from disk, assigning a unique ID on the fly via rewriteFormId.  This
eliminates formServerLoadFile, the in-memory form store, and the -i
flag from dfm2form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-03-04 19:14:36 -06:00
parent 38125a51a1
commit 17394e4f5d
4 changed files with 164 additions and 256 deletions

View file

@ -46,39 +46,37 @@ part of a Delphi 1.0 project on Windows.
## DFM Converter
`dfm2form` reads Delphi 1.0 binary DFM files (TPF0 format) and outputs
`.form` files containing protocol commands.
`.form` files containing protocol commands. The form ID in the output
is a placeholder (`0`); the server assigns a dynamic ID when it streams
the file to the client.
```
dfm2form [-i <formId>] <input.dfm> [output.form]
dfm2form <input.dfm> [output.form]
```
**Options:**
- `-i <formId>` — Set the form ID (default: 1). Each form needs a
unique ID when serving multiple forms.
**Examples:**
```
dfm2form login.dfm # output to stdout
dfm2form login.dfm login.form # output to file
dfm2form -i 2 settings.dfm settings.form
```
**Output** is a sequence of protocol commands:
```
FORM.CREATE 1 400 300 "Login"
CTRL.CREATE 1 1 Label 20 20 100 17 Caption="Username:"
CTRL.CREATE 1 2 Edit 120 18 200 21 Text="" MaxLength=32 TabOrder=0
CTRL.CREATE 1 3 Label 20 52 100 17 Caption="Password:"
CTRL.CREATE 1 4 Edit 120 50 200 21 Text="" MaxLength=32 TabOrder=1
CTRL.CREATE 1 5 Button 245 90 75 25 Caption="OK" TabOrder=2
CTRL.CREATE 1 6 Button 160 90 75 25 Caption="Cancel" TabOrder=3
EVENT.BIND 1 5 Enter
FORM.SHOW 1
FORM.CREATE 0 400 300 "Login"
CTRL.CREATE 0 1 Label 20 20 100 17 Caption="Username:"
CTRL.CREATE 0 2 Edit 120 18 200 21 Text="" MaxLength=32 TabOrder=0
CTRL.CREATE 0 3 Label 20 52 100 17 Caption="Password:"
CTRL.CREATE 0 4 Edit 120 50 200 21 Text="" MaxLength=32 TabOrder=1
CTRL.CREATE 0 5 Button 245 90 75 25 Caption="OK" TabOrder=2
CTRL.CREATE 0 6 Button 160 90 75 25 Caption="Cancel" TabOrder=3
EVENT.BIND 0 5 Enter
FORM.SHOW 0
```
The placeholder `0` IDs are replaced at runtime by `formServerSendForm`.
The converter maps Delphi class names to protocol control types,
extracts geometry and properties, and emits `EVENT.BIND` for any
event handler assignments in the DFM that are not auto-wired (see
@ -86,9 +84,9 @@ Events below). Unknown control classes are skipped with a warning.
## Server Library (C)
The server loads `.form` files and sends their commands to a remote
client through a pluggable transport interface. It also receives
events from the client and dispatches them to a callback.
The server streams `.form` files from disk to a remote client through
a pluggable transport interface, assigning dynamic form IDs. It also
receives events from the client and dispatches them to a callback.
### Transport Interface
@ -117,17 +115,15 @@ typedef struct {
FormServerT *formServerCreate(FormTransportT *transport);
void formServerDestroy(FormServerT *server);
// Load a .form file. Returns the form ID, or -1 on error.
int32_t formServerLoadFile(FormServerT *server, const char *path);
// Send all commands for a loaded form to the client.
void formServerSendForm(FormServerT *server, int32_t formId);
// Stream a .form file to the client, assigning a dynamic form ID.
// Returns the assigned form ID, or -1 on error.
int32_t formServerSendForm(FormServerT *server, const char *path);
// Form visibility
void formServerShowForm(FormServerT *server, int32_t formId);
void formServerHideForm(FormServerT *server, int32_t formId);
// Destroy a form (sends FORM.DESTROY and removes from store)
// Destroy a form on the client.
void formServerDestroyForm(FormServerT *server, int32_t formId);
// Update a control property
@ -188,8 +184,7 @@ int main(void)
formServerSetEventCallback(server, onEvent, server);
int32_t formId = formServerLoadFile(server, "login.form");
formServerSendForm(server, formId);
int32_t formId = formServerSendForm(server, "login.form");
// Main loop
while (running) {
@ -540,8 +535,6 @@ any protocol or application code.
| Limit | Value |
|---------------------------|-------|
| Max message length | 4096 bytes |
| Max forms (server) | 64 |
| Max lines per .form file | 1024 |
| Max controls per form | 256 |
| Form ID range | 1-65535 (stored in high word of Tag) |
| Control ID range | 1-65535 (stored in low word of Tag) |

View file

@ -1,6 +1,6 @@
// dfm2form.c - Convert Delphi 1.0 binary DFM (TPF0) to .form protocol text
//
// Usage: dfm2form [-i <formId>] <input.dfm> [output.form]
// Usage: dfm2form <input.dfm> [output.form]
//
// Reads a binary DFM file, extracts the form and control definitions,
// and outputs protocol commands (FORM.CREATE, CTRL.CREATE, EVENT.BIND,
@ -864,8 +864,7 @@ static void emitForm(FILE *out, int32_t formId, DfmFormT *form)
static void usage(const char *progName)
{
fprintf(stderr, "Usage: %s [-i <formId>] <input.dfm> [output.form]\n", progName);
fprintf(stderr, " -i <formId> Set form ID (default: 1)\n");
fprintf(stderr, "Usage: %s <input.dfm> [output.form]\n", progName);
exit(1);
}
@ -876,23 +875,13 @@ static void usage(const char *progName)
int main(int argc, char *argv[])
{
int32_t formId = 1;
const char *inputPath = NULL;
const char *outputPath = NULL;
// Parse arguments
int32_t i = 1;
while (i < argc) {
if (strcmp(argv[i], "-i") == 0) {
if (i + 1 >= argc) {
usage(argv[0]);
}
formId = atoi(argv[++i]);
if (formId <= 0) {
fprintf(stderr, "Error: form ID must be positive\n");
exit(1);
}
} else if (argv[i][0] == '-') {
if (argv[i][0] == '-') {
usage(argv[0]);
} else if (inputPath == NULL) {
inputPath = argv[i];
@ -965,7 +954,7 @@ int main(int argc, char *argv[])
fout = stdout;
}
emitForm(fout, formId, &form);
emitForm(fout, 0, &form);
if (fout != stdout) {
fclose(fout);

View file

@ -4,6 +4,7 @@
#include "formsrv.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -13,26 +14,17 @@
// Constants
// ---------------------------------------------------------------------------
#define MAX_FORMS 64
#define MAX_LINES 1024
#define MAX_MSG_LEN 4096
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
typedef struct {
int32_t formId;
char *lines[MAX_LINES];
int32_t lineCount;
} FormDataT;
struct FormServerS {
FormTransportT *transport;
EventCallbackT eventCallback;
void *eventUserData;
FormDataT forms[MAX_FORMS];
int32_t formCount;
int32_t nextFormId;
char msgBuf[MAX_MSG_LEN];
};
@ -40,66 +32,16 @@ struct FormServerS {
// Prototypes
// ---------------------------------------------------------------------------
static FormDataT *findForm(FormServerT *server, int32_t formId);
static void freeFormData(FormDataT *fd);
static int32_t parseFormId(const char *line);
static bool parseToken(const char **p, char *buf, int32_t bufSize);
static void rewriteFormId(char *line, int32_t lineSize, int32_t formId);
static void sendCommand(FormServerT *server, const char *fmt, ...);
static bool skipSpaces(const char **p);
static bool parseToken(const char **p, char *buf, int32_t bufSize);
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
static FormDataT *findForm(FormServerT *server, int32_t formId)
{
for (int32_t i = 0; i < server->formCount; i++) {
if (server->forms[i].formId == formId) {
return &server->forms[i];
}
}
return NULL;
}
static void freeFormData(FormDataT *fd)
{
for (int32_t i = 0; i < fd->lineCount; i++) {
free(fd->lines[i]);
fd->lines[i] = NULL;
}
fd->lineCount = 0;
fd->formId = 0;
}
static int32_t parseFormId(const char *line)
{
// Skip command prefix (e.g., "FORM.CREATE "), then read first integer
const char *p = line;
// Skip non-space command name
while (*p && *p != ' ') {
p++;
}
// Skip space
while (*p == ' ') {
p++;
}
// Parse integer
return (int32_t)atoi(p);
}
static bool skipSpaces(const char **p)
{
while (**p == ' ' || **p == '\t') {
(*p)++;
}
return **p != '\0';
}
static bool parseToken(const char **p, char *buf, int32_t bufSize)
{
skipSpaces(p);
@ -152,7 +94,49 @@ static bool parseToken(const char **p, char *buf, int32_t bufSize)
}
#include <stdarg.h>
static void rewriteFormId(char *line, int32_t lineSize, int32_t formId)
{
// Protocol lines have the form: COMMAND <formId> <rest...>
// Skip command prefix (non-space chars)
char *p = line;
while (*p && *p != ' ') {
p++;
}
// Skip spaces
while (*p == ' ') {
p++;
}
// p now points at the placeholder ID token ("0")
// Find the end of this token
char *idStart = p;
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
p++;
}
char *idEnd = p;
// Format the new ID
char idBuf[16];
int32_t idLen = snprintf(idBuf, sizeof(idBuf), "%d", formId);
int32_t oldLen = (int32_t)(idEnd - idStart);
int32_t shift = idLen - oldLen;
// Check that the rewritten line still fits
int32_t curLen = (int32_t)strlen(line);
if (curLen + shift >= lineSize - 1) {
return;
}
// Shift the remainder of the line
if (shift != 0) {
memmove(idStart + idLen, idEnd, strlen(idEnd) + 1);
}
// Write the new ID (no NUL — the memmove already placed the rest)
memcpy(idStart, idBuf, idLen);
}
static void sendCommand(FormServerT *server, const char *fmt, ...)
{
@ -167,10 +151,26 @@ static void sendCommand(FormServerT *server, const char *fmt, ...)
}
static bool skipSpaces(const char **p)
{
while (**p == ' ' || **p == '\t') {
(*p)++;
}
return **p != '\0';
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void formServerBindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
const char *eventName)
{
sendCommand(server, "EVENT.BIND %d %d %s", formId, ctrlId, eventName);
}
FormServerT *formServerCreate(FormTransportT *transport)
{
FormServerT *server = (FormServerT *)calloc(1, sizeof(FormServerT));
@ -178,6 +178,7 @@ FormServerT *formServerCreate(FormTransportT *transport)
return NULL;
}
server->transport = transport;
server->nextFormId = 1;
return server;
}
@ -187,151 +188,19 @@ void formServerDestroy(FormServerT *server)
if (server == NULL) {
return;
}
for (int32_t i = 0; i < server->formCount; i++) {
freeFormData(&server->forms[i]);
}
free(server);
}
int32_t formServerLoadFile(FormServerT *server, const char *path)
{
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "formsrv: cannot open '%s': %s\n", path, strerror(errno));
return -1;
}
if (server->formCount >= MAX_FORMS) {
fprintf(stderr, "formsrv: too many forms (max %d)\n", MAX_FORMS);
fclose(f);
return -1;
}
FormDataT *fd = &server->forms[server->formCount];
memset(fd, 0, sizeof(FormDataT));
char lineBuf[MAX_MSG_LEN];
int32_t formId = -1;
while (fgets(lineBuf, sizeof(lineBuf), f) != NULL) {
// Strip trailing newline
int32_t len = (int32_t)strlen(lineBuf);
while (len > 0 && (lineBuf[len - 1] == '\n' || lineBuf[len - 1] == '\r')) {
lineBuf[--len] = '\0';
}
// Skip empty lines
if (len == 0) {
continue;
}
if (fd->lineCount >= MAX_LINES) {
fprintf(stderr, "formsrv: too many lines in '%s' (max %d)\n", path, MAX_LINES);
freeFormData(fd);
fclose(f);
return -1;
}
fd->lines[fd->lineCount] = strdup(lineBuf);
if (fd->lines[fd->lineCount] == NULL) {
fprintf(stderr, "formsrv: out of memory\n");
freeFormData(fd);
fclose(f);
return -1;
}
fd->lineCount++;
// Extract form ID from first FORM.CREATE line
if (formId == -1 && strncmp(lineBuf, "FORM.CREATE ", 12) == 0) {
formId = parseFormId(lineBuf);
}
}
fclose(f);
if (formId <= 0) {
fprintf(stderr, "formsrv: no FORM.CREATE found in '%s'\n", path);
freeFormData(fd);
return -1;
}
fd->formId = formId;
server->formCount++;
return formId;
}
void formServerSendForm(FormServerT *server, int32_t formId)
{
FormDataT *fd = findForm(server, formId);
if (fd == NULL) {
return;
}
for (int32_t i = 0; i < fd->lineCount; i++) {
server->transport->writeMessage(fd->lines[i], server->transport->ctx);
}
}
void formServerShowForm(FormServerT *server, int32_t formId)
{
sendCommand(server, "FORM.SHOW %d", formId);
}
void formServerHideForm(FormServerT *server, int32_t formId)
{
sendCommand(server, "FORM.HIDE %d", formId);
}
void formServerDestroyForm(FormServerT *server, int32_t formId)
{
sendCommand(server, "FORM.DESTROY %d", formId);
// Remove from form store
for (int32_t i = 0; i < server->formCount; i++) {
if (server->forms[i].formId == formId) {
freeFormData(&server->forms[i]);
// Shift remaining forms down
for (int32_t j = i; j < server->formCount - 1; j++) {
server->forms[j] = server->forms[j + 1];
}
server->formCount--;
break;
}
}
}
void formServerSetProp(FormServerT *server, int32_t formId, int32_t ctrlId,
const char *prop, const char *value)
void formServerHideForm(FormServerT *server, int32_t formId)
{
sendCommand(server, "CTRL.SET %d %d %s=%s", formId, ctrlId, prop, value);
}
void formServerBindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
const char *eventName)
{
sendCommand(server, "EVENT.BIND %d %d %s", formId, ctrlId, eventName);
}
void formServerUnbindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
const char *eventName)
{
sendCommand(server, "EVENT.UNBIND %d %d %s", formId, ctrlId, eventName);
}
void formServerSetEventCallback(FormServerT *server, EventCallbackT cb,
void *userData)
{
server->eventCallback = cb;
server->eventUserData = userData;
sendCommand(server, "FORM.HIDE %d", formId);
}
@ -386,3 +255,63 @@ bool formServerPollEvent(FormServerT *server)
return true;
}
int32_t formServerSendForm(FormServerT *server, const char *path)
{
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "formsrv: cannot open '%s': %s\n", path, strerror(errno));
return -1;
}
int32_t formId = server->nextFormId++;
char lineBuf[MAX_MSG_LEN];
while (fgets(lineBuf, sizeof(lineBuf), f) != NULL) {
// Strip trailing newline
int32_t len = (int32_t)strlen(lineBuf);
while (len > 0 && (lineBuf[len - 1] == '\n' || lineBuf[len - 1] == '\r')) {
lineBuf[--len] = '\0';
}
// Skip empty lines
if (len == 0) {
continue;
}
rewriteFormId(lineBuf, sizeof(lineBuf), formId);
server->transport->writeMessage(lineBuf, server->transport->ctx);
}
fclose(f);
return formId;
}
void formServerSetEventCallback(FormServerT *server, EventCallbackT cb,
void *userData)
{
server->eventCallback = cb;
server->eventUserData = userData;
}
void formServerSetProp(FormServerT *server, int32_t formId, int32_t ctrlId,
const char *prop, const char *value)
{
sendCommand(server, "CTRL.SET %d %d %s=%s", formId, ctrlId, prop, value);
}
void formServerShowForm(FormServerT *server, int32_t formId)
{
sendCommand(server, "FORM.SHOW %d", formId);
}
void formServerUnbindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
const char *eventName)
{
sendCommand(server, "EVENT.UNBIND %d %d %s", formId, ctrlId, eventName);
}

View file

@ -1,8 +1,8 @@
// formsrv.h - Remote forms server library
//
// Loads .form files (protocol command sequences) and sends them to a
// remote client via a transport interface. Receives EVENT messages
// from the client and dispatches them to a callback.
// Streams .form files from disk to a remote client via a transport
// interface, assigning dynamic form IDs. Receives EVENT messages from
// the client and dispatches them to a callback.
#ifndef FORMSRV_H
#define FORMSRV_H
@ -48,12 +48,9 @@ typedef struct FormServerS FormServerT;
FormServerT *formServerCreate(FormTransportT *transport);
void formServerDestroy(FormServerT *server);
// Load a .form file into the server's form store. Returns the form ID
// parsed from the first FORM.CREATE line, or -1 on error.
int32_t formServerLoadFile(FormServerT *server, const char *path);
// Send all commands for a loaded form to the client.
void formServerSendForm(FormServerT *server, int32_t formId);
// Stream a .form file to the client, assigning a dynamic form ID.
// Returns the assigned form ID, or -1 on error.
int32_t formServerSendForm(FormServerT *server, const char *path);
// Send FORM.SHOW / FORM.HIDE / FORM.DESTROY commands.
void formServerShowForm(FormServerT *server, int32_t formId);