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 ## DFM Converter
`dfm2form` reads Delphi 1.0 binary DFM files (TPF0 format) and outputs `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:** **Examples:**
``` ```
dfm2form login.dfm # output to stdout dfm2form login.dfm # output to stdout
dfm2form login.dfm login.form # output to file dfm2form login.dfm login.form # output to file
dfm2form -i 2 settings.dfm settings.form
``` ```
**Output** is a sequence of protocol commands: **Output** is a sequence of protocol commands:
``` ```
FORM.CREATE 1 400 300 "Login" FORM.CREATE 0 400 300 "Login"
CTRL.CREATE 1 1 Label 20 20 100 17 Caption="Username:" CTRL.CREATE 0 1 Label 20 20 100 17 Caption="Username:"
CTRL.CREATE 1 2 Edit 120 18 200 21 Text="" MaxLength=32 TabOrder=0 CTRL.CREATE 0 2 Edit 120 18 200 21 Text="" MaxLength=32 TabOrder=0
CTRL.CREATE 1 3 Label 20 52 100 17 Caption="Password:" CTRL.CREATE 0 3 Label 20 52 100 17 Caption="Password:"
CTRL.CREATE 1 4 Edit 120 50 200 21 Text="" MaxLength=32 TabOrder=1 CTRL.CREATE 0 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 0 5 Button 245 90 75 25 Caption="OK" TabOrder=2
CTRL.CREATE 1 6 Button 160 90 75 25 Caption="Cancel" TabOrder=3 CTRL.CREATE 0 6 Button 160 90 75 25 Caption="Cancel" TabOrder=3
EVENT.BIND 1 5 Enter EVENT.BIND 0 5 Enter
FORM.SHOW 1 FORM.SHOW 0
``` ```
The placeholder `0` IDs are replaced at runtime by `formServerSendForm`.
The converter maps Delphi class names to protocol control types, The converter maps Delphi class names to protocol control types,
extracts geometry and properties, and emits `EVENT.BIND` for any extracts geometry and properties, and emits `EVENT.BIND` for any
event handler assignments in the DFM that are not auto-wired (see 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) ## Server Library (C)
The server loads `.form` files and sends their commands to a remote The server streams `.form` files from disk to a remote client through
client through a pluggable transport interface. It also receives a pluggable transport interface, assigning dynamic form IDs. It also
events from the client and dispatches them to a callback. receives events from the client and dispatches them to a callback.
### Transport Interface ### Transport Interface
@ -117,17 +115,15 @@ typedef struct {
FormServerT *formServerCreate(FormTransportT *transport); FormServerT *formServerCreate(FormTransportT *transport);
void formServerDestroy(FormServerT *server); void formServerDestroy(FormServerT *server);
// Load a .form file. Returns the form ID, or -1 on error. // Stream a .form file to the client, assigning a dynamic form ID.
int32_t formServerLoadFile(FormServerT *server, const char *path); // Returns the assigned form ID, or -1 on error.
int32_t formServerSendForm(FormServerT *server, const char *path);
// Send all commands for a loaded form to the client.
void formServerSendForm(FormServerT *server, int32_t formId);
// Form visibility // Form visibility
void formServerShowForm(FormServerT *server, int32_t formId); void formServerShowForm(FormServerT *server, int32_t formId);
void formServerHideForm(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); void formServerDestroyForm(FormServerT *server, int32_t formId);
// Update a control property // Update a control property
@ -188,8 +184,7 @@ int main(void)
formServerSetEventCallback(server, onEvent, server); formServerSetEventCallback(server, onEvent, server);
int32_t formId = formServerLoadFile(server, "login.form"); int32_t formId = formServerSendForm(server, "login.form");
formServerSendForm(server, formId);
// Main loop // Main loop
while (running) { while (running) {
@ -540,8 +535,6 @@ any protocol or application code.
| Limit | Value | | Limit | Value |
|---------------------------|-------| |---------------------------|-------|
| Max message length | 4096 bytes | | Max message length | 4096 bytes |
| Max forms (server) | 64 |
| Max lines per .form file | 1024 |
| Max controls per form | 256 | | Max controls per form | 256 |
| Form ID range | 1-65535 (stored in high word of Tag) | | Form ID range | 1-65535 (stored in high word of Tag) |
| Control ID range | 1-65535 (stored in low 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 // 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, // Reads a binary DFM file, extracts the form and control definitions,
// and outputs protocol commands (FORM.CREATE, CTRL.CREATE, EVENT.BIND, // 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) static void usage(const char *progName)
{ {
fprintf(stderr, "Usage: %s [-i <formId>] <input.dfm> [output.form]\n", progName); fprintf(stderr, "Usage: %s <input.dfm> [output.form]\n", progName);
fprintf(stderr, " -i <formId> Set form ID (default: 1)\n");
exit(1); exit(1);
} }
@ -876,23 +875,13 @@ static void usage(const char *progName)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
int32_t formId = 1; const char *inputPath = NULL;
const char *inputPath = NULL;
const char *outputPath = NULL; const char *outputPath = NULL;
// Parse arguments // Parse arguments
int32_t i = 1; int32_t i = 1;
while (i < argc) { while (i < argc) {
if (strcmp(argv[i], "-i") == 0) { if (argv[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] == '-') {
usage(argv[0]); usage(argv[0]);
} else if (inputPath == NULL) { } else if (inputPath == NULL) {
inputPath = argv[i]; inputPath = argv[i];
@ -965,7 +954,7 @@ int main(int argc, char *argv[])
fout = stdout; fout = stdout;
} }
emitForm(fout, formId, &form); emitForm(fout, 0, &form);
if (fout != stdout) { if (fout != stdout) {
fclose(fout); fclose(fout);

View file

@ -4,6 +4,7 @@
#include "formsrv.h" #include "formsrv.h"
#include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -13,26 +14,17 @@
// Constants // Constants
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#define MAX_FORMS 64
#define MAX_LINES 1024
#define MAX_MSG_LEN 4096 #define MAX_MSG_LEN 4096
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
typedef struct {
int32_t formId;
char *lines[MAX_LINES];
int32_t lineCount;
} FormDataT;
struct FormServerS { struct FormServerS {
FormTransportT *transport; FormTransportT *transport;
EventCallbackT eventCallback; EventCallbackT eventCallback;
void *eventUserData; void *eventUserData;
FormDataT forms[MAX_FORMS]; int32_t nextFormId;
int32_t formCount;
char msgBuf[MAX_MSG_LEN]; char msgBuf[MAX_MSG_LEN];
}; };
@ -40,66 +32,16 @@ struct FormServerS {
// Prototypes // Prototypes
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static FormDataT *findForm(FormServerT *server, int32_t formId); static bool parseToken(const char **p, char *buf, int32_t bufSize);
static void freeFormData(FormDataT *fd); static void rewriteFormId(char *line, int32_t lineSize, int32_t formId);
static int32_t parseFormId(const char *line); static void sendCommand(FormServerT *server, const char *fmt, ...);
static void sendCommand(FormServerT *server, const char *fmt, ...); static bool skipSpaces(const char **p);
static bool skipSpaces(const char **p);
static bool parseToken(const char **p, char *buf, int32_t bufSize);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Internal helpers // 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) static bool parseToken(const char **p, char *buf, int32_t bufSize)
{ {
skipSpaces(p); 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, ...) static void sendCommand(FormServerT *server, const char *fmt, ...)
{ {
@ -167,17 +151,34 @@ static void sendCommand(FormServerT *server, const char *fmt, ...)
} }
static bool skipSpaces(const char **p)
{
while (**p == ' ' || **p == '\t') {
(*p)++;
}
return **p != '\0';
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Public API // 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 *formServerCreate(FormTransportT *transport)
{ {
FormServerT *server = (FormServerT *)calloc(1, sizeof(FormServerT)); FormServerT *server = (FormServerT *)calloc(1, sizeof(FormServerT));
if (server == NULL) { if (server == NULL) {
return NULL; return NULL;
} }
server->transport = transport; server->transport = transport;
server->nextFormId = 1;
return server; return server;
} }
@ -187,151 +188,19 @@ void formServerDestroy(FormServerT *server)
if (server == NULL) { if (server == NULL) {
return; return;
} }
for (int32_t i = 0; i < server->formCount; i++) {
freeFormData(&server->forms[i]);
}
free(server); 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) void formServerDestroyForm(FormServerT *server, int32_t formId)
{ {
sendCommand(server, "FORM.DESTROY %d", 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, void formServerHideForm(FormServerT *server, int32_t formId)
const char *prop, const char *value)
{ {
sendCommand(server, "CTRL.SET %d %d %s=%s", formId, ctrlId, prop, value); sendCommand(server, "FORM.HIDE %d", formId);
}
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;
} }
@ -348,7 +217,7 @@ bool formServerPollEvent(FormServerT *server)
// Parse: EVENT <formId> <ctrlId> <eventName> [<data>] // Parse: EVENT <formId> <ctrlId> <eventName> [<data>]
const char *p = server->msgBuf; const char *p = server->msgBuf;
char token[256]; char token[256];
if (!parseToken(&p, token, sizeof(token))) { if (!parseToken(&p, token, sizeof(token))) {
return false; return false;
@ -386,3 +255,63 @@ bool formServerPollEvent(FormServerT *server)
return true; 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 // formsrv.h - Remote forms server library
// //
// Loads .form files (protocol command sequences) and sends them to a // Streams .form files from disk to a remote client via a transport
// remote client via a transport interface. Receives EVENT messages // interface, assigning dynamic form IDs. Receives EVENT messages from
// from the client and dispatches them to a callback. // the client and dispatches them to a callback.
#ifndef FORMSRV_H #ifndef FORMSRV_H
#define FORMSRV_H #define FORMSRV_H
@ -48,12 +48,9 @@ typedef struct FormServerS FormServerT;
FormServerT *formServerCreate(FormTransportT *transport); FormServerT *formServerCreate(FormTransportT *transport);
void formServerDestroy(FormServerT *server); void formServerDestroy(FormServerT *server);
// Load a .form file into the server's form store. Returns the form ID // Stream a .form file to the client, assigning a dynamic form ID.
// parsed from the first FORM.CREATE line, or -1 on error. // Returns the assigned form ID, or -1 on error.
int32_t formServerLoadFile(FormServerT *server, const char *path); int32_t formServerSendForm(FormServerT *server, const char *path);
// Send all commands for a loaded form to the client.
void formServerSendForm(FormServerT *server, int32_t formId);
// Send FORM.SHOW / FORM.HIDE / FORM.DESTROY commands. // Send FORM.SHOW / FORM.HIDE / FORM.DESTROY commands.
void formServerShowForm(FormServerT *server, int32_t formId); void formServerShowForm(FormServerT *server, int32_t formId);