WinComm/forms/formsrv.c
Scott Duensing ae2aef0119 Add remote forms system: DFM converter, server library, and client engine
Text-based protocol for serving Delphi-designed forms over serial.
dfm2form converts binary DFM (TPF0) to protocol commands on Linux.
formsrv loads .form files and sends/receives via pluggable transport.
formcli creates native Win 3.1 controls and routes events back to server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:35:54 -06:00

388 lines
9.5 KiB
C

// formsrv.c - Remote forms server library implementation
#define _POSIX_C_SOURCE 200809L
#include "formsrv.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
// ---------------------------------------------------------------------------
// 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;
char msgBuf[MAX_MSG_LEN];
};
// ---------------------------------------------------------------------------
// Prototypes
// ---------------------------------------------------------------------------
static FormDataT *findForm(FormServerT *server, int32_t formId);
static void freeFormData(FormDataT *fd);
static int32_t parseFormId(const char *line);
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);
if (**p == '\0') {
return false;
}
int32_t i = 0;
if (**p == '"') {
// Quoted string — read until closing quote
(*p)++;
while (**p != '\0' && **p != '"') {
if (**p == '\\' && *(*p + 1) != '\0') {
(*p)++;
char c = **p;
switch (c) {
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case '"': c = '"'; break;
case '\\': c = '\\'; break;
default: break;
}
if (i < bufSize - 1) {
buf[i++] = c;
}
} else {
if (i < bufSize - 1) {
buf[i++] = **p;
}
}
(*p)++;
}
if (**p == '"') {
(*p)++;
}
} else {
// Bare token — read until whitespace
while (**p != '\0' && **p != ' ' && **p != '\t') {
if (i < bufSize - 1) {
buf[i++] = **p;
}
(*p)++;
}
}
buf[i] = '\0';
return i > 0;
}
#include <stdarg.h>
static void sendCommand(FormServerT *server, const char *fmt, ...)
{
char buf[MAX_MSG_LEN];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
server->transport->writeMessage(buf, server->transport->ctx);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
FormServerT *formServerCreate(FormTransportT *transport)
{
FormServerT *server = (FormServerT *)calloc(1, sizeof(FormServerT));
if (server == NULL) {
return NULL;
}
server->transport = transport;
return server;
}
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)
{
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;
}
bool formServerPollEvent(FormServerT *server)
{
int bytesRead = server->transport->readMessage(
server->msgBuf, MAX_MSG_LEN - 1, server->transport->ctx);
if (bytesRead <= 0) {
return false;
}
server->msgBuf[bytesRead] = '\0';
// Parse: EVENT <formId> <ctrlId> <eventName> [<data>]
const char *p = server->msgBuf;
char token[256];
if (!parseToken(&p, token, sizeof(token))) {
return false;
}
if (strcmp(token, "EVENT") != 0) {
return false;
}
// formId
if (!parseToken(&p, token, sizeof(token))) {
return false;
}
int32_t formId = (int32_t)atoi(token);
// ctrlId
if (!parseToken(&p, token, sizeof(token))) {
return false;
}
int32_t ctrlId = (int32_t)atoi(token);
// eventName
char eventName[64];
if (!parseToken(&p, eventName, sizeof(eventName))) {
return false;
}
// Remaining data (rest of line after skipping spaces)
skipSpaces(&p);
const char *data = p;
if (server->eventCallback != NULL) {
server->eventCallback(formId, ctrlId, eventName, data,
server->eventUserData);
}
return true;
}