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>
388 lines
9.5 KiB
C
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;
|
|
}
|