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>
This commit is contained in:
parent
9a735c6adf
commit
ae2aef0119
6 changed files with 2888 additions and 0 deletions
976
forms/dfm2form.c
Normal file
976
forms/dfm2form.c
Normal file
|
|
@ -0,0 +1,976 @@
|
|||
// dfm2form.c - Convert Delphi 1.0 binary DFM (TPF0) to .form protocol text
|
||||
//
|
||||
// Usage: dfm2form [-i <formId>] <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,
|
||||
// FORM.SHOW) suitable for the remote forms system.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
typedef enum {
|
||||
ctUnknown = 0,
|
||||
ctLabel,
|
||||
ctEdit,
|
||||
ctButton,
|
||||
ctCheckBox,
|
||||
ctListBox,
|
||||
ctComboBox,
|
||||
ctMemo
|
||||
} CtrlTypeE;
|
||||
|
||||
typedef struct {
|
||||
char name[64];
|
||||
CtrlTypeE type;
|
||||
int32_t left;
|
||||
int32_t top;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
char caption[256];
|
||||
char text[4096];
|
||||
char items[4096];
|
||||
int32_t checked;
|
||||
int32_t enabled;
|
||||
int32_t visible;
|
||||
int32_t maxLength;
|
||||
int32_t readOnly;
|
||||
int32_t scrollBars;
|
||||
int32_t tabOrder;
|
||||
int32_t itemIndex;
|
||||
bool hasCaption;
|
||||
bool hasText;
|
||||
bool hasItems;
|
||||
bool hasChecked;
|
||||
bool hasEnabled;
|
||||
bool hasVisible;
|
||||
bool hasMaxLength;
|
||||
bool hasReadOnly;
|
||||
bool hasScrollBars;
|
||||
bool hasTabOrder;
|
||||
bool hasItemIndex;
|
||||
bool hasOnClick;
|
||||
bool hasOnChange;
|
||||
bool hasOnDblClick;
|
||||
bool hasOnEnter;
|
||||
bool hasOnExit;
|
||||
bool hasOnKeyDown;
|
||||
bool hasOnKeyUp;
|
||||
bool hasOnMouseDown;
|
||||
bool hasOnMouseUp;
|
||||
} DfmCtrlT;
|
||||
|
||||
typedef struct {
|
||||
char name[64];
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
char caption[256];
|
||||
DfmCtrlT ctrls[256];
|
||||
int32_t ctrlCount;
|
||||
} DfmFormT;
|
||||
|
||||
// DFM value type tags
|
||||
enum {
|
||||
vaNull = 0x00,
|
||||
vaList = 0x01,
|
||||
vaInt8 = 0x02,
|
||||
vaInt16 = 0x03,
|
||||
vaInt32 = 0x04,
|
||||
vaExtended = 0x05,
|
||||
vaString = 0x06,
|
||||
vaIdent = 0x07,
|
||||
vaFalse = 0x08,
|
||||
vaTrue = 0x09,
|
||||
vaBinary = 0x0A,
|
||||
vaSet = 0x0B,
|
||||
vaLString = 0x0C,
|
||||
vaNil = 0x0D,
|
||||
vaCollection = 0x0E
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prototypes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl);
|
||||
static void emitForm(FILE *out, int32_t formId, DfmFormT *form);
|
||||
static void escapeStr(const char *src, char *dst, int32_t dstSize);
|
||||
static void initCtrl(DfmCtrlT *ctrl);
|
||||
static void initForm(DfmFormT *form);
|
||||
static CtrlTypeE mapClassName(const char *className);
|
||||
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot);
|
||||
static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, DfmCtrlT *ctrl, bool isForm);
|
||||
static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos);
|
||||
static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos);
|
||||
static int32_t readInt32LE(const uint8_t *data, int32_t size, int32_t *pos);
|
||||
static int32_t readIntValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag);
|
||||
static bool readStr(const uint8_t *data, int32_t size, int32_t *pos, char *buf, int32_t bufSize);
|
||||
static void skipValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag);
|
||||
static int stricmp_local(const char *a, const char *b);
|
||||
static void usage(const char *progName);
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Low-level DFM readers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos)
|
||||
{
|
||||
if (*pos >= size) {
|
||||
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
|
||||
exit(1);
|
||||
}
|
||||
return data[(*pos)++];
|
||||
}
|
||||
|
||||
|
||||
static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos)
|
||||
{
|
||||
if (*pos + 2 > size) {
|
||||
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
|
||||
exit(1);
|
||||
}
|
||||
int16_t val = (int16_t)(data[*pos] | (data[*pos + 1] << 8));
|
||||
*pos += 2;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static int32_t readInt32LE(const uint8_t *data, int32_t size, int32_t *pos)
|
||||
{
|
||||
if (*pos + 4 > size) {
|
||||
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
|
||||
exit(1);
|
||||
}
|
||||
int32_t val = (int32_t)(data[*pos] | (data[*pos + 1] << 8) |
|
||||
(data[*pos + 2] << 16) | (data[*pos + 3] << 24));
|
||||
*pos += 4;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static bool readStr(const uint8_t *data, int32_t size, int32_t *pos, char *buf, int32_t bufSize)
|
||||
{
|
||||
int32_t len = readByte(data, size, pos);
|
||||
if (*pos + len > size) {
|
||||
fprintf(stderr, "Error: string overflows file at offset %d\n", *pos);
|
||||
exit(1);
|
||||
}
|
||||
int32_t copyLen = (len < bufSize - 1) ? len : bufSize - 1;
|
||||
memcpy(buf, data + *pos, copyLen);
|
||||
buf[copyLen] = '\0';
|
||||
*pos += len;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static int32_t readIntValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag)
|
||||
{
|
||||
switch (tag) {
|
||||
case vaInt8:
|
||||
return (int8_t)readByte(data, size, pos);
|
||||
case vaInt16:
|
||||
return readInt16LE(data, size, pos);
|
||||
case vaInt32:
|
||||
return readInt32LE(data, size, pos);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skip unknown property values
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void skipValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag)
|
||||
{
|
||||
int32_t len;
|
||||
char buf[256];
|
||||
|
||||
switch (tag) {
|
||||
case vaNull:
|
||||
break;
|
||||
case vaList:
|
||||
// skip items until vaNull
|
||||
while (*pos < size) {
|
||||
uint8_t itemTag = readByte(data, size, pos);
|
||||
if (itemTag == vaNull) {
|
||||
break;
|
||||
}
|
||||
skipValue(data, size, pos, itemTag);
|
||||
}
|
||||
break;
|
||||
case vaInt8:
|
||||
*pos += 1;
|
||||
break;
|
||||
case vaInt16:
|
||||
*pos += 2;
|
||||
break;
|
||||
case vaInt32:
|
||||
*pos += 4;
|
||||
break;
|
||||
case vaExtended:
|
||||
*pos += 10;
|
||||
break;
|
||||
case vaString:
|
||||
len = readByte(data, size, pos);
|
||||
*pos += len;
|
||||
break;
|
||||
case vaIdent:
|
||||
len = readByte(data, size, pos);
|
||||
*pos += len;
|
||||
break;
|
||||
case vaFalse:
|
||||
case vaTrue:
|
||||
case vaNil:
|
||||
break;
|
||||
case vaBinary:
|
||||
len = readInt32LE(data, size, pos);
|
||||
*pos += len;
|
||||
break;
|
||||
case vaSet:
|
||||
// repeated strings, empty string terminates
|
||||
while (*pos < size) {
|
||||
readStr(data, size, pos, buf, sizeof(buf));
|
||||
if (buf[0] == '\0') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case vaLString:
|
||||
len = readInt32LE(data, size, pos);
|
||||
*pos += len;
|
||||
break;
|
||||
case vaCollection:
|
||||
// items bracketed by vaList/vaNull
|
||||
while (*pos < size) {
|
||||
uint8_t itemTag = readByte(data, size, pos);
|
||||
if (itemTag == vaNull) {
|
||||
break;
|
||||
}
|
||||
skipValue(data, size, pos, itemTag);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Warning: unknown value tag 0x%02X at offset %d\n", tag, *pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case-insensitive string compare
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int stricmp_local(const char *a, const char *b)
|
||||
{
|
||||
while (*a && *b) {
|
||||
int ca = tolower((unsigned char)*a);
|
||||
int cb = tolower((unsigned char)*b);
|
||||
if (ca != cb) {
|
||||
return ca - cb;
|
||||
}
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return (unsigned char)*a - (unsigned char)*b;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Map Delphi class name to control type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static CtrlTypeE mapClassName(const char *className)
|
||||
{
|
||||
if (stricmp_local(className, "TLabel") == 0) {
|
||||
return ctLabel;
|
||||
}
|
||||
if (stricmp_local(className, "TEdit") == 0) {
|
||||
return ctEdit;
|
||||
}
|
||||
if (stricmp_local(className, "TButton") == 0) {
|
||||
return ctButton;
|
||||
}
|
||||
if (stricmp_local(className, "TCheckBox") == 0) {
|
||||
return ctCheckBox;
|
||||
}
|
||||
if (stricmp_local(className, "TListBox") == 0) {
|
||||
return ctListBox;
|
||||
}
|
||||
if (stricmp_local(className, "TComboBox") == 0) {
|
||||
return ctComboBox;
|
||||
}
|
||||
if (stricmp_local(className, "TMemo") == 0) {
|
||||
return ctMemo;
|
||||
}
|
||||
return ctUnknown;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Init helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void initCtrl(DfmCtrlT *ctrl)
|
||||
{
|
||||
memset(ctrl, 0, sizeof(DfmCtrlT));
|
||||
ctrl->enabled = 1;
|
||||
ctrl->visible = 1;
|
||||
ctrl->itemIndex = -1;
|
||||
ctrl->tabOrder = -1;
|
||||
}
|
||||
|
||||
|
||||
static void initForm(DfmFormT *form)
|
||||
{
|
||||
memset(form, 0, sizeof(DfmFormT));
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse properties from DFM binary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos,
|
||||
DfmFormT *form, DfmCtrlT *ctrl, bool isForm)
|
||||
{
|
||||
char propName[64];
|
||||
char strBuf[4096];
|
||||
|
||||
while (*pos < size) {
|
||||
// Property name (empty = end of properties)
|
||||
int32_t nameLen = readByte(data, size, pos);
|
||||
if (nameLen == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Read the property name
|
||||
(*pos)--; // back up, readStr reads the length byte
|
||||
readStr(data, size, pos, propName, sizeof(propName));
|
||||
|
||||
// Read value tag
|
||||
uint8_t tag = readByte(data, size, pos);
|
||||
|
||||
// Match known properties
|
||||
if (isForm && stricmp_local(propName, "Caption") == 0) {
|
||||
if (tag == vaString) {
|
||||
readStr(data, size, pos, form->caption, sizeof(form->caption));
|
||||
} else if (tag == vaLString) {
|
||||
int32_t len = readInt32LE(data, size, pos);
|
||||
int32_t copyLen = (len < (int32_t)sizeof(form->caption) - 1) ? len : (int32_t)sizeof(form->caption) - 1;
|
||||
memcpy(form->caption, data + *pos, copyLen);
|
||||
form->caption[copyLen] = '\0';
|
||||
*pos += len;
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
} else if (isForm && stricmp_local(propName, "ClientWidth") == 0) {
|
||||
form->width = readIntValue(data, size, pos, tag);
|
||||
} else if (isForm && stricmp_local(propName, "ClientHeight") == 0) {
|
||||
form->height = readIntValue(data, size, pos, tag);
|
||||
} else if (isForm && stricmp_local(propName, "Width") == 0 && form->width == 0) {
|
||||
form->width = readIntValue(data, size, pos, tag);
|
||||
} else if (isForm && stricmp_local(propName, "Height") == 0 && form->height == 0) {
|
||||
form->height = readIntValue(data, size, pos, tag);
|
||||
} else if (!isForm && stricmp_local(propName, "Left") == 0) {
|
||||
ctrl->left = readIntValue(data, size, pos, tag);
|
||||
} else if (!isForm && stricmp_local(propName, "Top") == 0) {
|
||||
ctrl->top = readIntValue(data, size, pos, tag);
|
||||
} else if (!isForm && stricmp_local(propName, "Width") == 0) {
|
||||
ctrl->width = readIntValue(data, size, pos, tag);
|
||||
} else if (!isForm && stricmp_local(propName, "Height") == 0) {
|
||||
ctrl->height = readIntValue(data, size, pos, tag);
|
||||
} else if (!isForm && stricmp_local(propName, "Caption") == 0) {
|
||||
if (tag == vaString) {
|
||||
readStr(data, size, pos, ctrl->caption, sizeof(ctrl->caption));
|
||||
} else if (tag == vaLString) {
|
||||
int32_t len = readInt32LE(data, size, pos);
|
||||
int32_t copyLen = (len < (int32_t)sizeof(ctrl->caption) - 1) ? len : (int32_t)sizeof(ctrl->caption) - 1;
|
||||
memcpy(ctrl->caption, data + *pos, copyLen);
|
||||
ctrl->caption[copyLen] = '\0';
|
||||
*pos += len;
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasCaption = true;
|
||||
} else if (!isForm && stricmp_local(propName, "Text") == 0) {
|
||||
if (tag == vaString) {
|
||||
readStr(data, size, pos, ctrl->text, sizeof(ctrl->text));
|
||||
} else if (tag == vaLString) {
|
||||
int32_t len = readInt32LE(data, size, pos);
|
||||
int32_t copyLen = (len < (int32_t)sizeof(ctrl->text) - 1) ? len : (int32_t)sizeof(ctrl->text) - 1;
|
||||
memcpy(ctrl->text, data + *pos, copyLen);
|
||||
ctrl->text[copyLen] = '\0';
|
||||
*pos += len;
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasText = true;
|
||||
} else if (!isForm && stricmp_local(propName, "Items.Strings") == 0 && tag == vaList) {
|
||||
// List of strings for ListBox/ComboBox
|
||||
ctrl->items[0] = '\0';
|
||||
int32_t itemsLen = 0;
|
||||
while (*pos < size) {
|
||||
uint8_t itemTag = readByte(data, size, pos);
|
||||
if (itemTag == vaNull) {
|
||||
break;
|
||||
}
|
||||
if (itemTag == vaString) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else if (itemTag == vaLString) {
|
||||
int32_t len = readInt32LE(data, size, pos);
|
||||
int32_t copyLen = (len < (int32_t)sizeof(strBuf) - 1) ? len : (int32_t)sizeof(strBuf) - 1;
|
||||
memcpy(strBuf, data + *pos, copyLen);
|
||||
strBuf[copyLen] = '\0';
|
||||
*pos += len;
|
||||
} else {
|
||||
skipValue(data, size, pos, itemTag);
|
||||
continue;
|
||||
}
|
||||
int32_t slen = (int32_t)strlen(strBuf);
|
||||
if (itemsLen + slen + 2 < (int32_t)sizeof(ctrl->items)) {
|
||||
if (itemsLen > 0) {
|
||||
ctrl->items[itemsLen++] = '\n';
|
||||
}
|
||||
memcpy(ctrl->items + itemsLen, strBuf, slen);
|
||||
itemsLen += slen;
|
||||
ctrl->items[itemsLen] = '\0';
|
||||
}
|
||||
}
|
||||
ctrl->hasItems = true;
|
||||
} else if (!isForm && stricmp_local(propName, "Checked") == 0) {
|
||||
if (tag == vaTrue) {
|
||||
ctrl->checked = 1;
|
||||
} else if (tag == vaFalse) {
|
||||
ctrl->checked = 0;
|
||||
} else {
|
||||
ctrl->checked = readIntValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasChecked = true;
|
||||
} else if (!isForm && stricmp_local(propName, "State") == 0) {
|
||||
// TCheckBox.State: cbUnchecked=0, cbChecked=1, cbGrayed=2
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
if (stricmp_local(strBuf, "cbChecked") == 0) {
|
||||
ctrl->checked = 1;
|
||||
} else {
|
||||
ctrl->checked = 0;
|
||||
}
|
||||
} else {
|
||||
ctrl->checked = readIntValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasChecked = true;
|
||||
} else if (!isForm && stricmp_local(propName, "Enabled") == 0) {
|
||||
if (tag == vaTrue) {
|
||||
ctrl->enabled = 1;
|
||||
} else if (tag == vaFalse) {
|
||||
ctrl->enabled = 0;
|
||||
} else {
|
||||
ctrl->enabled = readIntValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasEnabled = true;
|
||||
} else if (!isForm && stricmp_local(propName, "Visible") == 0) {
|
||||
if (tag == vaTrue) {
|
||||
ctrl->visible = 1;
|
||||
} else if (tag == vaFalse) {
|
||||
ctrl->visible = 0;
|
||||
} else {
|
||||
ctrl->visible = readIntValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasVisible = true;
|
||||
} else if (!isForm && stricmp_local(propName, "MaxLength") == 0) {
|
||||
ctrl->maxLength = readIntValue(data, size, pos, tag);
|
||||
ctrl->hasMaxLength = true;
|
||||
} else if (!isForm && stricmp_local(propName, "ReadOnly") == 0) {
|
||||
if (tag == vaTrue) {
|
||||
ctrl->readOnly = 1;
|
||||
} else if (tag == vaFalse) {
|
||||
ctrl->readOnly = 0;
|
||||
} else {
|
||||
ctrl->readOnly = readIntValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasReadOnly = true;
|
||||
} else if (!isForm && stricmp_local(propName, "ScrollBars") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
if (stricmp_local(strBuf, "ssNone") == 0) {
|
||||
ctrl->scrollBars = 0;
|
||||
} else if (stricmp_local(strBuf, "ssHorizontal") == 0) {
|
||||
ctrl->scrollBars = 1;
|
||||
} else if (stricmp_local(strBuf, "ssVertical") == 0) {
|
||||
ctrl->scrollBars = 2;
|
||||
} else if (stricmp_local(strBuf, "ssBoth") == 0) {
|
||||
ctrl->scrollBars = 3;
|
||||
} else {
|
||||
ctrl->scrollBars = 0;
|
||||
}
|
||||
} else {
|
||||
ctrl->scrollBars = readIntValue(data, size, pos, tag);
|
||||
}
|
||||
ctrl->hasScrollBars = true;
|
||||
} else if (!isForm && stricmp_local(propName, "TabOrder") == 0) {
|
||||
ctrl->tabOrder = readIntValue(data, size, pos, tag);
|
||||
ctrl->hasTabOrder = true;
|
||||
} else if (!isForm && stricmp_local(propName, "ItemIndex") == 0) {
|
||||
ctrl->itemIndex = readIntValue(data, size, pos, tag);
|
||||
ctrl->hasItemIndex = true;
|
||||
} else if (stricmp_local(propName, "OnClick") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnClick = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnChange") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnChange = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnDblClick") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnDblClick = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnEnter") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnEnter = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnExit") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnExit = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnKeyDown") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnKeyDown = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnKeyUp") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnKeyUp = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnMouseDown") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnMouseDown = true;
|
||||
}
|
||||
} else if (stricmp_local(propName, "OnMouseUp") == 0) {
|
||||
if (tag == vaIdent) {
|
||||
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
if (!isForm) {
|
||||
ctrl->hasOnMouseUp = true;
|
||||
}
|
||||
} else {
|
||||
skipValue(data, size, pos, tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse a component (form or child control) recursively
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos,
|
||||
DfmFormT *form, bool isRoot)
|
||||
{
|
||||
// Check for flags byte (Delphi 1.0 rarely uses this but handle it)
|
||||
uint8_t peek = data[*pos];
|
||||
if ((peek & 0xF0) == 0xF0) {
|
||||
uint8_t flags = readByte(data, size, pos);
|
||||
if (flags & 0x02) {
|
||||
// ffChildPos: skip tagged integer
|
||||
uint8_t childTag = readByte(data, size, pos);
|
||||
readIntValue(data, size, pos, childTag);
|
||||
}
|
||||
}
|
||||
|
||||
// Class name
|
||||
char className[64];
|
||||
readStr(data, size, pos, className, sizeof(className));
|
||||
|
||||
// Instance name
|
||||
char instName[64];
|
||||
readStr(data, size, pos, instName, sizeof(instName));
|
||||
|
||||
if (isRoot) {
|
||||
// This is the form itself
|
||||
snprintf(form->name, sizeof(form->name), "%s", instName);
|
||||
parseProperties(data, size, pos, form, NULL, true);
|
||||
|
||||
// Parse child components
|
||||
while (*pos < size) {
|
||||
peek = data[*pos];
|
||||
if (peek == 0x00) {
|
||||
(*pos)++; // consume terminator
|
||||
break;
|
||||
}
|
||||
parseComponent(data, size, pos, form, false);
|
||||
}
|
||||
} else {
|
||||
// Child control
|
||||
CtrlTypeE type = mapClassName(className);
|
||||
if (type == ctUnknown) {
|
||||
fprintf(stderr, "Warning: unknown control class '%s' (%s), skipping\n", className, instName);
|
||||
// Still need to parse properties and children to advance pos
|
||||
DfmCtrlT dummy;
|
||||
initCtrl(&dummy);
|
||||
parseProperties(data, size, pos, form, &dummy, false);
|
||||
// Skip nested children
|
||||
while (*pos < size) {
|
||||
peek = data[*pos];
|
||||
if (peek == 0x00) {
|
||||
(*pos)++;
|
||||
break;
|
||||
}
|
||||
parseComponent(data, size, pos, form, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (form->ctrlCount >= 256) {
|
||||
fprintf(stderr, "Error: too many controls (max 256)\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
DfmCtrlT *ctrl = &form->ctrls[form->ctrlCount];
|
||||
initCtrl(ctrl);
|
||||
ctrl->type = type;
|
||||
snprintf(ctrl->name, sizeof(ctrl->name), "%s", instName);
|
||||
|
||||
parseProperties(data, size, pos, form, ctrl, false);
|
||||
form->ctrlCount++;
|
||||
|
||||
// Skip nested children (controls within controls, e.g., panels)
|
||||
while (*pos < size) {
|
||||
peek = data[*pos];
|
||||
if (peek == 0x00) {
|
||||
(*pos)++;
|
||||
break;
|
||||
}
|
||||
parseComponent(data, size, pos, form, false);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Escape a string for protocol output
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void escapeStr(const char *src, char *dst, int32_t dstSize)
|
||||
{
|
||||
int32_t di = 0;
|
||||
|
||||
for (int32_t si = 0; src[si] != '\0' && di < dstSize - 2; si++) {
|
||||
char c = src[si];
|
||||
if (c == '"') {
|
||||
if (di + 2 >= dstSize - 1) { break; }
|
||||
dst[di++] = '\\';
|
||||
dst[di++] = '"';
|
||||
} else if (c == '\\') {
|
||||
if (di + 2 >= dstSize - 1) { break; }
|
||||
dst[di++] = '\\';
|
||||
dst[di++] = '\\';
|
||||
} else if (c == '\n') {
|
||||
if (di + 2 >= dstSize - 1) { break; }
|
||||
dst[di++] = '\\';
|
||||
dst[di++] = 'n';
|
||||
} else if (c == '\r') {
|
||||
if (di + 2 >= dstSize - 1) { break; }
|
||||
dst[di++] = '\\';
|
||||
dst[di++] = 'r';
|
||||
} else if (c == '\t') {
|
||||
if (di + 2 >= dstSize - 1) { break; }
|
||||
dst[di++] = '\\';
|
||||
dst[di++] = 't';
|
||||
} else {
|
||||
dst[di++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
dst[di] = '\0';
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Emit protocol commands for a single control
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
|
||||
{
|
||||
static const char *typeNames[] = {
|
||||
"Unknown", "Label", "Edit", "Button",
|
||||
"CheckBox", "ListBox", "ComboBox", "Memo"
|
||||
};
|
||||
char escaped[8192];
|
||||
|
||||
fprintf(out, "CTRL.CREATE %d %d %s %d %d %d %d",
|
||||
formId, ctrlId, typeNames[ctrl->type],
|
||||
ctrl->left, ctrl->top, ctrl->width, ctrl->height);
|
||||
|
||||
// Inline properties
|
||||
if (ctrl->hasCaption) {
|
||||
escapeStr(ctrl->caption, escaped, sizeof(escaped));
|
||||
fprintf(out, " Caption=\"%s\"", escaped);
|
||||
}
|
||||
if (ctrl->hasText) {
|
||||
escapeStr(ctrl->text, escaped, sizeof(escaped));
|
||||
fprintf(out, " Text=\"%s\"", escaped);
|
||||
}
|
||||
if (ctrl->hasItems) {
|
||||
escapeStr(ctrl->items, escaped, sizeof(escaped));
|
||||
fprintf(out, " Items=\"%s\"", escaped);
|
||||
}
|
||||
if (ctrl->hasChecked) {
|
||||
fprintf(out, " Checked=%d", ctrl->checked);
|
||||
}
|
||||
if (ctrl->hasEnabled && ctrl->enabled == 0) {
|
||||
fprintf(out, " Enabled=0");
|
||||
}
|
||||
if (ctrl->hasVisible && ctrl->visible == 0) {
|
||||
fprintf(out, " Visible=0");
|
||||
}
|
||||
if (ctrl->hasMaxLength && ctrl->maxLength > 0) {
|
||||
fprintf(out, " MaxLength=%d", ctrl->maxLength);
|
||||
}
|
||||
if (ctrl->hasReadOnly && ctrl->readOnly) {
|
||||
fprintf(out, " ReadOnly=1");
|
||||
}
|
||||
if (ctrl->hasScrollBars && ctrl->scrollBars != 0) {
|
||||
fprintf(out, " ScrollBars=%d", ctrl->scrollBars);
|
||||
}
|
||||
if (ctrl->hasTabOrder && ctrl->tabOrder >= 0) {
|
||||
fprintf(out, " TabOrder=%d", ctrl->tabOrder);
|
||||
}
|
||||
if (ctrl->hasItemIndex && ctrl->itemIndex >= 0) {
|
||||
fprintf(out, " ItemIndex=%d", ctrl->itemIndex);
|
||||
}
|
||||
|
||||
fprintf(out, "\n");
|
||||
|
||||
// Emit EVENT.BIND for non-auto-wired events
|
||||
// Auto-wired: Button/CheckBox→Click, Edit→Change, ListBox→Select,
|
||||
// ComboBox→Select+Change, Memo→Change
|
||||
|
||||
bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox);
|
||||
bool autoChange = (ctrl->type == ctEdit || ctrl->type == ctComboBox || ctrl->type == ctMemo);
|
||||
bool autoSelect = (ctrl->type == ctListBox || ctrl->type == ctComboBox);
|
||||
|
||||
if (ctrl->hasOnClick && !autoClick) {
|
||||
fprintf(out, "EVENT.BIND %d %d Click\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnChange && !autoChange) {
|
||||
fprintf(out, "EVENT.BIND %d %d Change\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnDblClick) {
|
||||
fprintf(out, "EVENT.BIND %d %d DblClick\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnEnter) {
|
||||
fprintf(out, "EVENT.BIND %d %d Enter\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnExit) {
|
||||
fprintf(out, "EVENT.BIND %d %d Exit\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnKeyDown) {
|
||||
fprintf(out, "EVENT.BIND %d %d KeyDown\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnKeyUp) {
|
||||
fprintf(out, "EVENT.BIND %d %d KeyUp\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnMouseDown) {
|
||||
fprintf(out, "EVENT.BIND %d %d MouseDown\n", formId, ctrlId);
|
||||
}
|
||||
if (ctrl->hasOnMouseUp) {
|
||||
fprintf(out, "EVENT.BIND %d %d MouseUp\n", formId, ctrlId);
|
||||
}
|
||||
|
||||
// Suppress unused variable warning for autoSelect
|
||||
(void)autoSelect;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Emit the complete form
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void emitForm(FILE *out, int32_t formId, DfmFormT *form)
|
||||
{
|
||||
char escaped[512];
|
||||
|
||||
escapeStr(form->caption, escaped, sizeof(escaped));
|
||||
fprintf(out, "FORM.CREATE %d %d %d \"%s\"\n",
|
||||
formId, form->width, form->height, escaped);
|
||||
|
||||
for (int32_t i = 0; i < form->ctrlCount; i++) {
|
||||
emitCtrl(out, formId, i + 1, &form->ctrls[i]);
|
||||
}
|
||||
|
||||
fprintf(out, "FORM.SHOW %d\n", formId);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Usage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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] == '-') {
|
||||
usage(argv[0]);
|
||||
} else if (inputPath == NULL) {
|
||||
inputPath = argv[i];
|
||||
} else if (outputPath == NULL) {
|
||||
outputPath = argv[i];
|
||||
} else {
|
||||
usage(argv[0]);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (inputPath == NULL) {
|
||||
usage(argv[0]);
|
||||
}
|
||||
|
||||
// Read input file
|
||||
FILE *fin = fopen(inputPath, "rb");
|
||||
if (fin == NULL) {
|
||||
fprintf(stderr, "Error: cannot open '%s': %s\n", inputPath, strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
fseek(fin, 0, SEEK_END);
|
||||
long fileSize = ftell(fin);
|
||||
fseek(fin, 0, SEEK_SET);
|
||||
|
||||
uint8_t *data = (uint8_t *)malloc(fileSize);
|
||||
if (data == NULL) {
|
||||
fprintf(stderr, "Error: out of memory\n");
|
||||
fclose(fin);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ((long)fread(data, 1, fileSize, fin) != fileSize) {
|
||||
fprintf(stderr, "Error: failed to read '%s'\n", inputPath);
|
||||
free(data);
|
||||
fclose(fin);
|
||||
exit(1);
|
||||
}
|
||||
fclose(fin);
|
||||
|
||||
// Verify TPF0 signature
|
||||
if (fileSize < 4 || memcmp(data, "TPF0", 4) != 0) {
|
||||
fprintf(stderr, "Error: '%s' is not a Delphi binary DFM (missing TPF0 signature)\n", inputPath);
|
||||
free(data);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Parse
|
||||
DfmFormT form;
|
||||
initForm(&form);
|
||||
int32_t pos = 4; // skip TPF0
|
||||
parseComponent(data, (int32_t)fileSize, &pos, &form, true);
|
||||
|
||||
// Default caption if empty
|
||||
if (form.caption[0] == '\0') {
|
||||
snprintf(form.caption, sizeof(form.caption), "%s", form.name);
|
||||
}
|
||||
|
||||
// Output
|
||||
FILE *fout;
|
||||
if (outputPath != NULL) {
|
||||
fout = fopen(outputPath, "w");
|
||||
if (fout == NULL) {
|
||||
fprintf(stderr, "Error: cannot create '%s': %s\n", outputPath, strerror(errno));
|
||||
free(data);
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
fout = stdout;
|
||||
}
|
||||
|
||||
emitForm(fout, formId, &form);
|
||||
|
||||
if (fout != stdout) {
|
||||
fclose(fout);
|
||||
}
|
||||
|
||||
free(data);
|
||||
return 0;
|
||||
}
|
||||
1290
forms/formcli.pas
Normal file
1290
forms/formcli.pas
Normal file
File diff suppressed because it is too large
Load diff
388
forms/formsrv.c
Normal file
388
forms/formsrv.c
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
// 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;
|
||||
}
|
||||
83
forms/formsrv.h
Normal file
83
forms/formsrv.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// 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.
|
||||
|
||||
#ifndef FORMSRV_H
|
||||
#define FORMSRV_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transport interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
typedef struct {
|
||||
// Read a complete message into buf. Returns bytes read, 0 if no
|
||||
// message is available. Must not block.
|
||||
int (*readMessage)(char *buf, int32_t maxLen, void *ctx);
|
||||
|
||||
// Write a null-terminated message string. Transport adds framing
|
||||
// (e.g., CR+LF for serial).
|
||||
void (*writeMessage)(const char *buf, void *ctx);
|
||||
|
||||
// Opaque context pointer passed to readMessage/writeMessage.
|
||||
void *ctx;
|
||||
} FormTransportT;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event callback
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
typedef void (*EventCallbackT)(int32_t formId, int32_t ctrlId,
|
||||
const char *eventName, const char *data,
|
||||
void *userData);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server handle (opaque)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
typedef struct FormServerS FormServerT;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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);
|
||||
|
||||
// Send FORM.SHOW / FORM.HIDE / FORM.DESTROY commands.
|
||||
void formServerShowForm(FormServerT *server, int32_t formId);
|
||||
void formServerHideForm(FormServerT *server, int32_t formId);
|
||||
void formServerDestroyForm(FormServerT *server, int32_t formId);
|
||||
|
||||
// Send a CTRL.SET command to update a property on a control.
|
||||
void formServerSetProp(FormServerT *server, int32_t formId,
|
||||
int32_t ctrlId, const char *prop,
|
||||
const char *value);
|
||||
|
||||
// Send an EVENT.BIND command.
|
||||
void formServerBindEvent(FormServerT *server, int32_t formId,
|
||||
int32_t ctrlId, const char *eventName);
|
||||
|
||||
// Send an EVENT.UNBIND command.
|
||||
void formServerUnbindEvent(FormServerT *server, int32_t formId,
|
||||
int32_t ctrlId, const char *eventName);
|
||||
|
||||
// Set the callback for incoming events.
|
||||
void formServerSetEventCallback(FormServerT *server,
|
||||
EventCallbackT cb, void *userData);
|
||||
|
||||
// Poll for one incoming event. Returns true if an event was processed.
|
||||
bool formServerPollEvent(FormServerT *server);
|
||||
|
||||
#endif // FORMSRV_H
|
||||
16
forms/makefile
Normal file
16
forms/makefile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -std=c99 -O2
|
||||
LDFLAGS =
|
||||
|
||||
all: dfm2form formsrv.o
|
||||
|
||||
dfm2form: dfm2form.c
|
||||
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||
|
||||
formsrv.o: formsrv.c formsrv.h
|
||||
$(CC) $(CFLAGS) -c -o $@ formsrv.c
|
||||
|
||||
clean:
|
||||
rm -f dfm2form formsrv.o
|
||||
|
||||
.PHONY: all clean
|
||||
135
forms/protocol.md
Normal file
135
forms/protocol.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# Remote Forms Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
Text-based protocol for remote GUI. A C server on Linux sends form/control
|
||||
commands over a transport layer; a Delphi 1.0 client on Windows 3.1 creates
|
||||
native controls and sends user events back.
|
||||
|
||||
## Message Format
|
||||
|
||||
- One command per message.
|
||||
- Transport delivers whole messages (today: newline-delimited over serial).
|
||||
- Strings are double-quoted with escapes: `\"` `\\` `\n` `\r` `\t`.
|
||||
- Bare tokens (IDs, numbers, type names) are whitespace-delimited.
|
||||
- IDs are positive integers assigned by the server.
|
||||
|
||||
## Server → Client Commands
|
||||
|
||||
### FORM.CREATE
|
||||
|
||||
FORM.CREATE <formId> <width> <height> "<title>"
|
||||
|
||||
Create a new form with the given dimensions and title. The form is not
|
||||
shown until FORM.SHOW is sent.
|
||||
|
||||
### FORM.SHOW
|
||||
|
||||
FORM.SHOW <formId>
|
||||
|
||||
### FORM.HIDE
|
||||
|
||||
FORM.HIDE <formId>
|
||||
|
||||
### FORM.DESTROY
|
||||
|
||||
FORM.DESTROY <formId>
|
||||
|
||||
Free the form and all its controls.
|
||||
|
||||
### CTRL.CREATE
|
||||
|
||||
CTRL.CREATE <formId> <ctrlId> <type> <left> <top> <width> <height> [Key="val" ...]
|
||||
|
||||
Create a control on the specified form. Inline key/value properties are
|
||||
applied immediately after creation. See Control Types and Properties below.
|
||||
|
||||
### CTRL.SET
|
||||
|
||||
CTRL.SET <formId> <ctrlId> Key="val" [Key="val" ...]
|
||||
|
||||
Update one or more properties on an existing control.
|
||||
|
||||
### EVENT.BIND
|
||||
|
||||
EVENT.BIND <formId> <ctrlId> <eventName>
|
||||
|
||||
Wire an opt-in event handler. Auto-wired events do not need explicit binding.
|
||||
|
||||
### EVENT.UNBIND
|
||||
|
||||
EVENT.UNBIND <formId> <ctrlId> <eventName>
|
||||
|
||||
Remove an event handler.
|
||||
|
||||
## Client → Server Events
|
||||
|
||||
EVENT <formId> <ctrlId> <eventName> [<data>]
|
||||
|
||||
Event data varies by event type:
|
||||
|
||||
| Event | Data |
|
||||
|-----------|-------------------------------|
|
||||
| Click | (none) |
|
||||
| DblClick | (none) |
|
||||
| Change | `"new text"` |
|
||||
| Select | `<index> "selected text"` |
|
||||
| KeyDown | `<vkCode>` |
|
||||
| KeyUp | `<vkCode>` |
|
||||
| MouseDown | `<x> <y> <button>` |
|
||||
| MouseUp | `<x> <y> <button>` |
|
||||
| MouseMove | `<x> <y> <button>` |
|
||||
| Enter | (none) |
|
||||
| Exit | (none) |
|
||||
| Close | (none) |
|
||||
|
||||
## Control Types
|
||||
|
||||
| Type | Delphi Class | Auto-wired Events |
|
||||
|----------|-------------|-------------------|
|
||||
| Label | TLabel | (none) |
|
||||
| Edit | TEdit | Change |
|
||||
| Button | TButton | Click |
|
||||
| CheckBox | TCheckBox | Click |
|
||||
| ListBox | TListBox | Select |
|
||||
| ComboBox | TComboBox | Select, Change |
|
||||
| Memo | TMemo | Change |
|
||||
|
||||
Opt-in events (require EVENT.BIND): DblClick, KeyDown, KeyUp, Enter, Exit,
|
||||
MouseDown, MouseUp, MouseMove.
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Applies To | Value Format |
|
||||
|------------|-------------------------------|-------------------------------------------|
|
||||
| Caption | Label, Button, CheckBox | Quoted string |
|
||||
| Text | Edit, ComboBox, Memo | Quoted string (`\n` for line breaks) |
|
||||
| Items | ListBox, ComboBox | Quoted string (`\n`-delimited) |
|
||||
| Checked | CheckBox | 0 or 1 |
|
||||
| Enabled | All | 0 or 1 |
|
||||
| Visible | All | 0 or 1 |
|
||||
| MaxLength | Edit | Integer |
|
||||
| ReadOnly | Edit, Memo | 0 or 1 |
|
||||
| ScrollBars | Memo | 0-3 (ssNone..ssBoth) |
|
||||
| ItemIndex | ListBox, ComboBox | Integer (-1 = none) |
|
||||
| TabOrder | All windowed controls | Integer |
|
||||
|
||||
## String Encoding
|
||||
|
||||
- Strings in the protocol are always double-quoted.
|
||||
- Escape sequences: `\"` (literal quote), `\\` (literal backslash),
|
||||
`\n` (newline), `\r` (carriage return), `\t` (tab).
|
||||
- Multi-line values (Memo text, ListBox items) use `\n` within a single
|
||||
quoted string.
|
||||
|
||||
## Transport Layer
|
||||
|
||||
The protocol is transport-agnostic. Messages are delivered via:
|
||||
|
||||
```
|
||||
int ReadMessage(char *buf, int maxLen); // returns bytes read, 0 = none
|
||||
void WriteMessage(const char *buf); // sends complete message
|
||||
```
|
||||
|
||||
Current transport: newline-delimited serial (messages terminated by CR+LF).
|
||||
The transport handles framing; protocol layer never sees delimiters.
|
||||
Loading…
Add table
Reference in a new issue