Compare commits
68 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36c6eeaf81 | |||
| d9889b2fbb | |||
| 60d24c8c33 | |||
| 4cdcfe6b8c | |||
| 4600f3e631 | |||
| 1affec7e8c | |||
| a8c38267bc | |||
| 05e69f60a7 | |||
| 217f138d07 | |||
| 48fb1c30ae | |||
| e6305db3b5 | |||
| dcd120d769 | |||
| de60200f23 | |||
| 7157f97c28 | |||
| 66952306df | |||
| 8abe947b8b | |||
| 3db721fdc0 | |||
| 7c1eb495e1 | |||
| 094b263c36 | |||
| 5f305dd14c | |||
| 454a3620f7 | |||
| 3c886a97f6 | |||
| fe68899020 | |||
| 5ece172ad0 | |||
| c43c586f2f | |||
| 10ba408465 | |||
| 2a641f42c3 | |||
| 0f5da09d1f | |||
| 7ec2aead8f | |||
| 2a2a386592 | |||
| 1923886d42 | |||
| af0ad3091f | |||
| 5f2358fcf2 | |||
| 35f877d3e1 | |||
| 85010d17dc | |||
| de7027c44e | |||
| 626befa664 | |||
| eb5e4e567e | |||
| 827d73fbd1 | |||
| d3898707f9 | |||
| 93b912d932 | |||
| 7cd7388607 | |||
| 7bc92549f7 | |||
| e36d4b9cec | |||
| bf5bf9bb1d | |||
| dd68a19b5b | |||
| 657b44eb25 | |||
| 0ef46ff6a0 | |||
| 289adb8c47 | |||
| 6d75e4996a | |||
| 88746ec2ba | |||
| 17fe1840e3 | |||
| 4d4aedbc43 | |||
| 1fb8e2a387 | |||
| 82939c3f27 | |||
| 5dd632a862 | |||
| 344ab4794d | |||
| d094205ed0 | |||
| 59bc2b5ed3 | |||
| 7d74d76985 | |||
| 5171753250 | |||
| 51ade9f119 | |||
| 0043e06c82 | |||
| 65d7a252ca | |||
| b3cc66be4b | |||
| f62b89fc02 | |||
| aa961425c9 | |||
| 89690ca97c |
850 changed files with 328650 additions and 20816 deletions
4
.gitattributes
vendored
4
.gitattributes
vendored
|
|
@ -2,5 +2,9 @@
|
|||
*.BMP filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.JPG filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.PNG filter=lfs diff=lfs merge=lfs -text
|
||||
*.xcf filter=lfs diff=lfs merge=lfs -text
|
||||
*.XCF filter=lfs diff=lfs merge=lfs -text
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.ZIP filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,8 +2,11 @@ dosbench/
|
|||
bin/
|
||||
obj/
|
||||
lib/
|
||||
*~
|
||||
*.~
|
||||
.gitignore~
|
||||
.gitattributes~
|
||||
*.SWP
|
||||
.claude/
|
||||
capture/
|
||||
just-stuff/
|
||||
|
|
|
|||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2026 Scott Duensing
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
212
Makefile
212
Makefile
|
|
@ -1,57 +1,197 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
# DVX GUI -- Top-level Makefile
|
||||
#
|
||||
# Builds the full DVX stack: core library, task switcher,
|
||||
# bootstrap loader, text help library, widgets, shell, and apps.
|
||||
#
|
||||
# Source tree mirrors bin/ layout:
|
||||
# src/libs/kpunch/<libname>/ -> bin/libs/kpunch/<libname>/<libname>.lib
|
||||
# src/widgets/kpunch/<widget>/ -> bin/widgets/kpunch/<widget>/<widget>.wgt
|
||||
# src/apps/kpunch/<app>/ -> bin/apps/kpunch/<app>/<app>.app
|
||||
# src/loader/ -> bin/DVX.EXE
|
||||
# src/tools/ -> bin/host/<tool>
|
||||
|
||||
.PHONY: all clean core tasks loader texthelp listhelp widgets shell taskmgr serial apps tools
|
||||
.PHONY: all clean libdvx libtasks loader texthelp listhelp widgets dvxshell taskmgr serial sql apps tools deploy-helpsrc compile-help deploy-sdk
|
||||
|
||||
all: core tasks loader texthelp listhelp widgets shell taskmgr serial apps tools
|
||||
all: libdvx libtasks loader texthelp listhelp tools widgets dvxshell taskmgr serial sql apps deploy-helpsrc compile-help deploy-sdk
|
||||
|
||||
core:
|
||||
$(MAKE) -C core
|
||||
libdvx:
|
||||
$(MAKE) -C src/libs/kpunch/libdvx
|
||||
|
||||
tasks:
|
||||
$(MAKE) -C tasks
|
||||
libtasks:
|
||||
$(MAKE) -C src/libs/kpunch/libtasks
|
||||
|
||||
loader: core tasks
|
||||
$(MAKE) -C loader
|
||||
loader: libdvx libtasks
|
||||
$(MAKE) -C src/loader
|
||||
|
||||
texthelp: core tasks
|
||||
$(MAKE) -C texthelp
|
||||
texthelp: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/texthelp
|
||||
|
||||
listhelp: core tasks
|
||||
$(MAKE) -C listhelp
|
||||
listhelp: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/listhelp
|
||||
|
||||
widgets: core tasks texthelp listhelp
|
||||
$(MAKE) -C widgets
|
||||
widgets: libdvx libtasks texthelp listhelp
|
||||
$(MAKE) -C src/widgets/kpunch
|
||||
|
||||
shell: core tasks
|
||||
$(MAKE) -C shell
|
||||
dvxshell: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/dvxshell
|
||||
|
||||
taskmgr: shell
|
||||
$(MAKE) -C taskmgr
|
||||
taskmgr: dvxshell
|
||||
$(MAKE) -C src/libs/kpunch/taskmgr
|
||||
|
||||
serial: core tasks
|
||||
$(MAKE) -C serial
|
||||
serial: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/serial
|
||||
|
||||
sql: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/sql
|
||||
|
||||
tools:
|
||||
$(MAKE) -C tools
|
||||
$(MAKE) -C src/tools
|
||||
|
||||
apps: core tasks shell tools
|
||||
$(MAKE) -C apps
|
||||
apps: libdvx libtasks dvxshell tools
|
||||
$(MAKE) -C src/apps/kpunch
|
||||
|
||||
deploy-helpsrc:
|
||||
$(MAKE) -C src/tools deploy-helpsrc
|
||||
|
||||
HLPC = bin/host/dvxhlpc
|
||||
|
||||
SYSTEM_DHS = src/libs/kpunch/libdvx/sysdoc.dhs \
|
||||
src/libs/kpunch/libdvx/arch.dhs src/libs/kpunch/libdvx/apiref.dhs \
|
||||
src/libs/kpunch/libtasks/libtasks.dhs src/libs/kpunch/dvxshell/dvxshell.dhs src/libs/kpunch/sql/dvxsql.dhs \
|
||||
src/libs/kpunch/texthelp/texthelp.dhs src/libs/kpunch/listhelp/listhelp.dhs \
|
||||
src/libs/kpunch/taskmgr/taskmgr.dhs src/libs/kpunch/serial/serial.dhs \
|
||||
src/apps/kpunch/dvxbasic/basrt.dhs \
|
||||
src/widgets/kpunch/wgtsys.dhs
|
||||
|
||||
BASIC_DHS = src/apps/kpunch/dvxbasic/ideguide.dhs src/apps/kpunch/dvxbasic/langref.dhs \
|
||||
src/apps/kpunch/dvxbasic/ctrlover.dhs src/apps/kpunch/dvxbasic/form.dhs
|
||||
|
||||
compile-help:
|
||||
@mkdir -p docs
|
||||
$(HLPC) -o bin/apps/kpunch/dvxhelp/dvxhelp.hlp \
|
||||
--html docs/dvx_help_viewer.html \
|
||||
src/apps/kpunch/dvxhelp/help.dhs
|
||||
$(HLPC) -o bin/apps/kpunch/progman/dvxhelp.hlp \
|
||||
--html docs/dvx_system_reference.html \
|
||||
-i src/libs/kpunch/libdvx \
|
||||
$(SYSTEM_DHS) \
|
||||
$$(find src/widgets/kpunch -name "*.dhs" ! -path "*/wgtsys.dhs" | sort)
|
||||
$(HLPC) -o bin/apps/kpunch/dvxbasic/dvxbasic.hlp \
|
||||
--html docs/dvx_basic_reference.html \
|
||||
$(BASIC_DHS) \
|
||||
$$(find src/widgets/kpunch -name "*.bhs" | sort)
|
||||
|
||||
SDKDIR = bin/sdk
|
||||
|
||||
deploy-sdk:
|
||||
@echo "Building SDK..."
|
||||
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
|
||||
@# Core headers (libdvx public API)
|
||||
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \
|
||||
src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
|
||||
src/libs/kpunch/libdvx/dvxWm.h src/libs/kpunch/libdvx/dvxDraw.h \
|
||||
src/libs/kpunch/libdvx/dvxVideo.h src/libs/kpunch/libdvx/dvxComp.h \
|
||||
src/libs/kpunch/libdvx/dvxPrefs.h src/libs/kpunch/libdvx/dvxDlg.h \
|
||||
src/libs/kpunch/libdvx/dvxRes.h src/libs/kpunch/libdvx/dvxFont.h \
|
||||
src/libs/kpunch/libdvx/dvxCur.h src/libs/kpunch/libdvx/dvxPal.h \
|
||||
src/libs/kpunch/libdvx/dvxMem.h src/libs/kpunch/libdvx/platform/dvxPlat.h; do \
|
||||
[ -f "$$f" ] && cp "$$f" $(SDKDIR)/include/core/; \
|
||||
done
|
||||
@# Shell header
|
||||
@cp src/libs/kpunch/dvxshell/shellApp.h $(SDKDIR)/include/shell/
|
||||
@cp src/libs/kpunch/dvxshell/shellInf.h $(SDKDIR)/include/shell/
|
||||
@# Tasks header
|
||||
@cp src/libs/kpunch/libtasks/taskSwch.h $(SDKDIR)/include/tasks/
|
||||
@# SQL header
|
||||
@cp src/libs/kpunch/sql/dvxSql.h $(SDKDIR)/include/sql/
|
||||
@# Widget headers -- one subdir per widget
|
||||
@for d in src/widgets/kpunch/*/; do \
|
||||
for h in "$$d"*.h; do \
|
||||
[ -f "$$h" ] || continue; \
|
||||
wgt=$$(basename "$$d"); \
|
||||
mkdir -p $(SDKDIR)/include/widget/"$$wgt"; \
|
||||
cp "$$h" $(SDKDIR)/include/widget/"$$wgt"/; \
|
||||
done; \
|
||||
done
|
||||
@# BASIC include files
|
||||
@cp src/include/basic/*.bas $(SDKDIR)/include/basic/
|
||||
@# BASIC sample: basdemo (project file + form + icon)
|
||||
@cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
|
||||
@# BASIC sample: widshow (widget showcase)
|
||||
@cp src/apps/kpunch/widshow/widshow.dbp $(SDKDIR)/samples/basic/widshow/
|
||||
@cp src/apps/kpunch/widshow/widshow.frm $(SDKDIR)/samples/basic/widshow/
|
||||
@cp src/apps/kpunch/widshow/ICON32.BMP $(SDKDIR)/samples/basic/widshow/
|
||||
@# README
|
||||
@printf '%s\n' \
|
||||
'DVX SDK' \
|
||||
'=======' \
|
||||
'' \
|
||||
'Headers for developing applications and widgets against the DVX GUI.' \
|
||||
'' \
|
||||
'Directory Structure' \
|
||||
'-------------------' \
|
||||
'' \
|
||||
' include/' \
|
||||
' core/ DVX core headers (types, drawing, windows, widgets)' \
|
||||
' shell/ Shell/app loading API' \
|
||||
' tasks/ Cooperative task switching API' \
|
||||
' sql/ SQLite database wrapper API' \
|
||||
' widget/ Per-widget public API headers' \
|
||||
' basic/ BASIC include files (DECLARE LIBRARY modules)' \
|
||||
' samples/' \
|
||||
' basic/ Example BASIC projects (open in the DVX BASIC IDE)' \
|
||||
'' \
|
||||
'Requirements' \
|
||||
'------------' \
|
||||
'' \
|
||||
' - DJGPP cross-compiler (i586-pc-msdosdjgpp-gcc)' \
|
||||
' - dxe3gen (included with DJGPP)' \
|
||||
' - DVX resource compiler (SYSTEM/DVXRES.EXE on the target,' \
|
||||
' or bin/host/dvxres on the host)' \
|
||||
' - DVX help compiler (SYSTEM/DVXHLPC.EXE on the target,' \
|
||||
' or bin/host/dvxhlpc on the host)' \
|
||||
'' \
|
||||
'For complete working examples, see the app sources under' \
|
||||
'src/apps/kpunch/ in the DVX repository.' \
|
||||
> $(SDKDIR)/README.TXT
|
||||
|
||||
clean:
|
||||
$(MAKE) -C core clean
|
||||
$(MAKE) -C tasks clean
|
||||
$(MAKE) -C loader clean
|
||||
$(MAKE) -C texthelp clean
|
||||
$(MAKE) -C listhelp clean
|
||||
$(MAKE) -C widgets clean
|
||||
$(MAKE) -C shell clean
|
||||
$(MAKE) -C taskmgr clean
|
||||
$(MAKE) -C serial clean
|
||||
$(MAKE) -C apps clean
|
||||
$(MAKE) -C tools clean
|
||||
$(MAKE) -C src/libs/kpunch/libdvx clean
|
||||
$(MAKE) -C src/libs/kpunch/libtasks clean
|
||||
$(MAKE) -C src/loader clean
|
||||
$(MAKE) -C src/libs/kpunch/texthelp clean
|
||||
$(MAKE) -C src/libs/kpunch/listhelp clean
|
||||
$(MAKE) -C src/widgets/kpunch clean
|
||||
$(MAKE) -C src/libs/kpunch/dvxshell clean
|
||||
$(MAKE) -C src/libs/kpunch/taskmgr clean
|
||||
$(MAKE) -C src/libs/kpunch/serial clean
|
||||
$(MAKE) -C src/libs/kpunch/sql clean
|
||||
$(MAKE) -C src/apps/kpunch clean
|
||||
$(MAKE) -C src/tools clean
|
||||
-rmdir obj 2>/dev/null
|
||||
-rm -rf bin/config bin/widgets bin/libs
|
||||
-rmdir bin/apps/cpanel bin/apps/imgview bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin 2>/dev/null
|
||||
-rm -rf bin/config bin/widgets bin/libs bin/sdk
|
||||
-rm -f docs/*.html
|
||||
|
|
|
|||
309
README.md
309
README.md
|
|
@ -1,219 +1,110 @@
|
|||
# DVX -- DOS Visual eXecutive
|
||||
|
||||
A windowed GUI compositor and widget toolkit targeting DJGPP/DPMI on
|
||||
486+ hardware. VESA VBE 2.0+ LFB only. Motif-inspired visual style
|
||||
with 2px bevels, fixed bitmap fonts, and dirty-rect compositing.
|
||||
DVX is a windowed GUI environment and application platform for DOS.
|
||||
It provides a Motif-inspired desktop, a cooperative multitasking kernel,
|
||||
a widget toolkit, a compiled BASIC language with a visual form designer,
|
||||
and a small set of bundled applications.
|
||||
|
||||
The system runs on 86Box (primary target) and real DOS hardware.
|
||||
The system is a compositor built on VESA VBE 2.0+ linear framebuffer
|
||||
video modes. A dirty-rectangle compositor keeps redraw cost proportional
|
||||
to what actually changes on screen, so the desktop is responsive even at
|
||||
1024x768 on modest hardware.
|
||||
|
||||
|
||||
## Architecture
|
||||
## What's Included
|
||||
|
||||
DVX is built as a set of DXE3 dynamic modules loaded by a bootstrap
|
||||
executable. The loader resolves dependencies and loads modules in
|
||||
topological order, then hands control to the shell.
|
||||
|
||||
```
|
||||
dvx.exe (loader)
|
||||
|
|
||||
+-- libs/libtasks.lib cooperative task switcher
|
||||
+-- libs/libdvx.lib core GUI (draw, comp, wm, app, widget infra)
|
||||
+-- libs/texthelp.lib shared text editing helpers
|
||||
+-- libs/listhelp.lib shared list/dropdown helpers
|
||||
+-- libs/dvxshell.lib shell (app lifecycle, desktop)
|
||||
+-- libs/taskmgr.lib task manager (Ctrl+Esc, separate DXE)
|
||||
|
|
||||
+-- widgets/*.wgt 26 widget type plugins
|
||||
|
|
||||
+-- apps/*/*.app DXE applications
|
||||
```
|
||||
|
||||
|
||||
## Directory Structure
|
||||
|
||||
| Directory | Output | Description |
|
||||
|--------------|-------------------------|--------------------------------------------|
|
||||
| `loader/` | `bin/dvx.exe` | Bootstrap loader, platform layer, stb_ds |
|
||||
| `core/` | `bin/libs/libdvx.lib` | Core GUI library (5 layers + widget infra) |
|
||||
| `tasks/` | `bin/libs/libtasks.lib` | Cooperative task switching |
|
||||
| `texthelp/` | `bin/libs/texthelp.lib` | Shared text editing helpers |
|
||||
| `listhelp/` | `bin/libs/listhelp.lib` | Shared list/dropdown helpers |
|
||||
| `shell/` | `bin/libs/dvxshell.lib` | DVX Shell (app lifecycle, desktop) |
|
||||
| `taskmgr/` | `bin/libs/taskmgr.lib` | Task Manager (separate DXE, Ctrl+Esc) |
|
||||
| `widgets/` | `bin/widgets/*.wgt` | 26 individual widget DXE modules |
|
||||
| `apps/` | `bin/apps/*/*.app` | Application DXE modules |
|
||||
| `tools/` | `bin/dvxres` | Resource tool (host native, not DXE) |
|
||||
| `config/` | `bin/config/`, `bin/libs/`, `bin/widgets/` | INI config, themes, wallpapers, dep files |
|
||||
| `rs232/` | `lib/librs232.a` | ISR-driven UART serial driver |
|
||||
| `packet/` | `lib/libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
|
||||
| `security/` | `lib/libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |
|
||||
| `seclink/` | `lib/libseclink.a` | Secure serial link (channels, encryption) |
|
||||
| `proxy/` | `bin/secproxy` | Linux proxy: 86Box <-> telnet BBS |
|
||||
| `termdemo/` | `bin/termdemo.exe` | Standalone encrypted terminal demo |
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
Requires the DJGPP cross-compiler at `~/djgpp/djgpp`.
|
||||
|
||||
```
|
||||
make # build everything
|
||||
make clean # remove all build artifacts
|
||||
./mkcd.sh # build + create ISO for 86Box
|
||||
```
|
||||
|
||||
The top-level Makefile builds in dependency order:
|
||||
|
||||
```
|
||||
core -> tasks -> loader -> texthelp -> listhelp -> widgets -> shell -> taskmgr -> apps
|
||||
tools (host native, parallel)
|
||||
```
|
||||
|
||||
Build output goes to `bin/` (executables, DXE modules, config) and
|
||||
`obj/` (intermediate object files).
|
||||
|
||||
|
||||
## Runtime Directory Layout (bin/)
|
||||
|
||||
```
|
||||
bin/
|
||||
dvx.exe bootstrap loader (entry point)
|
||||
libs/
|
||||
libtasks.lib task switching library
|
||||
libdvx.lib core GUI library
|
||||
texthelp.lib text editing helpers
|
||||
listhelp.lib list/dropdown helpers
|
||||
dvxshell.lib DVX shell
|
||||
taskmgr.lib task manager (Ctrl+Esc)
|
||||
*.dep dependency files for load ordering
|
||||
widgets/
|
||||
box.wgt VBox/HBox/Frame containers
|
||||
button.wgt push button
|
||||
... (26 widget modules total)
|
||||
*.dep dependency files
|
||||
apps/
|
||||
progman/progman.app Program Manager (desktop)
|
||||
notepad/notepad.app text editor
|
||||
clock/clock.app clock display
|
||||
dvxdemo/dvxdemo.app widget showcase
|
||||
cpanel/cpanel.app control panel
|
||||
imgview/imgview.app image viewer
|
||||
config/
|
||||
dvx.ini system configuration
|
||||
themes/ color theme files (.thm)
|
||||
wpaper/ wallpaper images
|
||||
```
|
||||
|
||||
|
||||
## Core Architecture (5 Layers)
|
||||
|
||||
1. **dvxVideo** -- VESA VBE init, LFB mapping, backbuffer, pixel format, color packing
|
||||
2. **dvxDraw** -- Rectangle fills, bevels, text rendering, cursor/icon drawing
|
||||
3. **dvxComp** -- Dirty rectangle tracking, merge, compositor, LFB flush
|
||||
4. **dvxWm** -- Window stack, chrome, drag/resize, menus, scrollbars, hit testing
|
||||
5. **dvxApp** -- Event loop, input polling, public API, color schemes, wallpaper
|
||||
|
||||
|
||||
## Widget System
|
||||
|
||||
Widgets are isolated DXE modules. Core knows nothing about individual
|
||||
widget types -- no compile-time enum, no union, no per-widget structs in
|
||||
dvxWidget.h.
|
||||
|
||||
* **Dynamic type IDs**: `wgtRegisterClass()` assigns IDs at load time
|
||||
* **void *data**: Each widget allocates its own private data struct
|
||||
* **ABI-stable dispatch**: `WidgetClassT.handlers[]` is an array of function
|
||||
pointers indexed by `WGT_METHOD_*` constants (0-20, room for 32). Core
|
||||
dispatches via `w->wclass->handlers[WGT_METHOD_PAINT]` etc., so adding
|
||||
new methods does not break existing widget DXE binaries
|
||||
* **Generic drag**: `WGT_METHOD_ON_DRAG_UPDATE` and `WGT_METHOD_ON_DRAG_END`
|
||||
provide widget-level drag support without per-widget hacks in core
|
||||
* **Per-widget API registry**: `wgtRegisterApi("name", &api)` replaces the monolithic API
|
||||
* **Per-widget headers**: `widgets/widgetButton.h` etc. provide typed macros
|
||||
* **Shared helpers**: texthelp.lib (text editing) and listhelp.lib (dropdown/list)
|
||||
* **All limits dynamic**: widget child arrays, app slots, and desktop callbacks
|
||||
are stb_ds dynamic arrays with no fixed maximums
|
||||
|
||||
|
||||
## DXE Module System
|
||||
|
||||
All modules are DXE3 dynamic libraries loaded by dvx.exe at startup.
|
||||
The loader recursively scans `libs/` for `.lib` files and `widgets/` for `.wgt`
|
||||
files, reads `.dep` files for dependencies, and loads in topological
|
||||
order. Widget modules that export `wgtRegister()` have it called after
|
||||
loading.
|
||||
|
||||
Each `.dep` file lists base names of modules that must load first, one
|
||||
per line. Comments start with `#`.
|
||||
|
||||
|
||||
## App Types
|
||||
|
||||
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows,
|
||||
registers callbacks, and returns. The app lives through event callbacks
|
||||
in the shell's main loop. No dedicated task or stack needed.
|
||||
|
||||
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for
|
||||
the app. `appMain` runs its own loop calling `tsYield()` to share CPU.
|
||||
Used for apps that need continuous processing (clocks, terminal
|
||||
emulators, games).
|
||||
|
||||
|
||||
## Crash Recovery
|
||||
|
||||
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL
|
||||
before loading any apps, so even crashes during app initialization are
|
||||
caught. If an app crashes, the handler `longjmp`s back to the shell's
|
||||
main loop, the crashed app is force-killed, and the shell continues
|
||||
running. Diagnostic information (registers, faulting EIP) is logged to
|
||||
`dvx.log`.
|
||||
|
||||
|
||||
## Bundled Applications
|
||||
|
||||
| App | File | Type | Description |
|
||||
|-----------------|---------------|-----------|------------------------------------------------------------|
|
||||
| Program Manager | `progman.app` | Callback | App launcher grid, system info dialog |
|
||||
| Notepad | `notepad.app` | Callback | Text editor with File/Edit menus, open/save, clipboard |
|
||||
| Clock | `clock.app` | Main-loop | Digital clock display; multi-instance capable |
|
||||
| DVX Demo | `dvxdemo.app` | Callback | Widget system showcase demonstrating 31 widget types |
|
||||
| Control Panel | `cpanel.app` | Callback | Themes, wallpaper, video mode, mouse configuration |
|
||||
| Image Viewer | `imgview.app` | Callback | Displays BMP, PNG, JPEG, GIF images with aspect-ratio zoom |
|
||||
|
||||
|
||||
## Serial / Networking Stack
|
||||
|
||||
A layered encrypted serial communications stack for connecting DVX to
|
||||
remote systems (BBS, etc.) through 86Box's emulated UART:
|
||||
|
||||
| Layer | Library | Description |
|
||||
|----------|------------------|---------------------------------------------------|
|
||||
| rs232 | `librs232.a` | ISR-driven UART driver, ring buffers, flow control |
|
||||
| packet | `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
|
||||
| security | `libsecurity.a` | 1024-bit DH, XTEA-CTR cipher, DRBG RNG |
|
||||
| seclink | `libseclink.a` | Channel multiplexing, per-packet encryption |
|
||||
| proxy | `secproxy` | Linux bridge: 86Box serial <-> telnet BBS |
|
||||
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
All third-party code is vendored as single-header libraries:
|
||||
|
||||
| Library | Location | Purpose |
|
||||
|-------------------|--------------------|---------------------------------|
|
||||
| stb_image.h | `core/thirdparty/` | Image loading (BMP, PNG, JPEG, GIF) |
|
||||
| stb_image_write.h | `core/thirdparty/` | PNG export for screenshots |
|
||||
| stb_ds.h | `core/thirdparty/` | Dynamic arrays and hash maps |
|
||||
|
||||
stb_ds implementation is compiled into dvx.exe (the loader) with
|
||||
`STBDS_REALLOC`/`STBDS_FREE` overridden to use `dvxRealloc`/`dvxFree`,
|
||||
so all `arrput`/`arrfree` calls in DXE code are tracked per-app. The
|
||||
functions are exported via `dlregsym` to all DXE modules.
|
||||
* **DVX Shell** -- desktop, app lifecycle manager, Task Manager, crash
|
||||
recovery. The shell is the program the user interacts with and the
|
||||
host for all running applications.
|
||||
* **Widget toolkit** -- 31 widget types as plug-in modules: buttons,
|
||||
labels, text inputs, list boxes, tree views, tab controls, sliders,
|
||||
spinners, progress bars, canvases, scroll panes, splitters, an ANSI
|
||||
terminal, and more.
|
||||
* **DVX BASIC** -- a Visual Basic 3 compatible language with a visual
|
||||
form designer, per-procedure code editor, project management, and a
|
||||
bytecode compiler and VM. BASIC programs compile to standalone DVX
|
||||
applications.
|
||||
* **Help system** -- a full hypertext help viewer with table of contents,
|
||||
keyword index, full-text search, and inline images. Help files are
|
||||
compiled from a plain-text source format.
|
||||
* **Serial / networking stack** -- an ISR-driven UART driver, HDLC
|
||||
packet framing with CRC-16 and Go-Back-N reliable delivery, 1024-bit
|
||||
Diffie-Hellman key exchange, and XTEA-CTR encryption, plus a Linux
|
||||
bridge that connects a virtual serial port to a telnet BBS.
|
||||
* **Control Panel** -- themes, wallpapers, video mode, mouse
|
||||
configuration, all with live preview.
|
||||
* **Bundled applications** -- Program Manager (desktop launcher),
|
||||
Notepad, Clock, Image Viewer, DVX Demo, Control Panel, DVX BASIC
|
||||
IDE, Help Viewer, Resource Editor, Icon Editor, Help Editor, BASIC
|
||||
Demo.
|
||||
|
||||
|
||||
## Target Hardware
|
||||
|
||||
* CPU: 486 baseline, Pentium-optimized paths where significant
|
||||
* Video: VESA VBE 2.0+ with LFB (no bank switching)
|
||||
* Platform: 86Box emulator (trusted reference), real DOS hardware
|
||||
* Resolutions: 640x480, 800x600, 1024x768 at 8/15/16/32 bpp
|
||||
* DOS with a 386-class CPU or newer (486 recommended, Pentium for best
|
||||
performance)
|
||||
* VESA BIOS Extensions 2.0 or newer with linear framebuffer support
|
||||
* 4 MB of extended memory minimum; 8 MB or more recommended
|
||||
* Any resolution the video card reports at 8, 15, 16, 24, or 32 bits
|
||||
per pixel. 640x480, 800x600, and 1024x768 are the common cases.
|
||||
* Two-button mouse; scroll wheel supported if a compatible driver is
|
||||
loaded
|
||||
|
||||
|
||||
## Building and Deploying
|
||||
|
||||
```
|
||||
make # build everything
|
||||
make clean # remove all build artifacts
|
||||
./mkcd.sh # build and produce a CD-ROM ISO image
|
||||
```
|
||||
|
||||
`make` builds in dependency order: core, tasks, loader, helpers,
|
||||
host tools, widgets, shell, task manager, apps. Build output goes to
|
||||
`bin/` and intermediate objects to `obj/`.
|
||||
|
||||
`./mkcd.sh` builds everything, then wraps `bin/` into an ISO 9660
|
||||
image for use with an emulator or for burning to physical media.
|
||||
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
dvxgui/
|
||||
assets/ logo and splash artwork
|
||||
bin/ build output (runtime tree; mirrors what ships)
|
||||
config/ shipped INI, themes, wallpapers
|
||||
docs/ HTML exports of the built-in help content
|
||||
LICENSE.txt MIT license
|
||||
Makefile top-level build
|
||||
mkcd.sh build-and-package script
|
||||
src/
|
||||
apps/ bundled applications
|
||||
include/ shared include files (BASIC library declarations)
|
||||
libs/ core libraries (GUI, tasks, shell, helpers, serial, SQL)
|
||||
loader/ bootstrap executable
|
||||
tools/ host-native development tools and the BASIC compiler
|
||||
widgets/ widget plug-in modules
|
||||
```
|
||||
|
||||
See `src/tools/README.md` for the host-side development tools
|
||||
(resource tool, icon generators, help compiler, proxy), and
|
||||
`src/apps/kpunch/README.md` for the SDK contract applications must
|
||||
follow.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
Full reference documentation ships with the system as browsable help:
|
||||
|
||||
* `docs/dvx_system_reference.html` -- system API, widget catalog
|
||||
* `docs/dvx_basic_reference.html` -- DVX BASIC language reference
|
||||
* `docs/dvx_help_viewer.html` -- help viewer guide
|
||||
|
||||
The same content is available from inside DVX via the Help Viewer and
|
||||
Program Manager.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MIT. See `LICENSE.txt`.
|
||||
|
|
|
|||
54
analyze.sh
Executable file
54
analyze.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# analyze.sh -- run the DJGPP build under gcc's -fanalyzer.
|
||||
#
|
||||
# Usage:
|
||||
# ./analyze.sh # analyse the whole build (slow: 2-5x)
|
||||
# ./analyze.sh 2>&1 | tee analyze.log
|
||||
#
|
||||
# How it works:
|
||||
#
|
||||
# The project Makefiles hardcode -Werror in CFLAGS. gcc evaluates
|
||||
# diagnostic flags in order, so a trailing -Wno-error (appended
|
||||
# AFTER CFLAGS at compile time) demotes analyzer findings back to
|
||||
# warnings -- otherwise the first analyzer hit aborts the build.
|
||||
#
|
||||
# We do that by setting CC to a wrapper that execs the real compiler
|
||||
# with -fanalyzer prepended and -Wno-error appended. make -k keeps
|
||||
# building after individual failures so we see every hit in one run.
|
||||
#
|
||||
# Runs the existing top-level Makefile with an override CC; no
|
||||
# Makefile edits required. Output goes to stderr; redirect with
|
||||
# '2>&1 | tee analyze.log' to capture.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
REAL_CC="$HOME/djgpp/djgpp/bin/i586-pc-msdosdjgpp-gcc"
|
||||
|
||||
if [ ! -x "$REAL_CC" ]; then
|
||||
echo "analyze.sh: $REAL_CC not executable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the wrapper in a temp dir that the spawned makes can see.
|
||||
WRAP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WRAP_DIR"' EXIT
|
||||
WRAP="$WRAP_DIR/analyze-cc"
|
||||
|
||||
cat > "$WRAP" <<WRAPEOF
|
||||
#!/bin/bash
|
||||
exec "$REAL_CC" -fanalyzer "\$@" -Wno-error
|
||||
WRAPEOF
|
||||
|
||||
chmod +x "$WRAP"
|
||||
|
||||
echo "analyze.sh: running full build under gcc -fanalyzer..." >&2
|
||||
echo "analyze.sh: this will be slow (2-5x normal build time)." >&2
|
||||
|
||||
# -k = keep going after errors so we collect every analyzer hit
|
||||
# CC=... overrides the assignment in each submakefile
|
||||
make -k CC="$WRAP" 2>&1
|
||||
|
||||
echo "analyze.sh: done. Grep for 'Wanalyzer' to see findings." >&2
|
||||
107
apps/Makefile
107
apps/Makefile
|
|
@ -1,107 +0,0 @@
|
|||
# DVX Shell Applications Makefile -- builds DXE3 modules
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../core/thirdparty -I../widgets -I../tasks -I../core/thirdparty -I../shell
|
||||
|
||||
OBJDIR = ../obj/apps
|
||||
BINDIR = ../bin/apps
|
||||
DVXRES = ../bin/dvxres
|
||||
|
||||
# App definitions: each is a subdir with a single .c file
|
||||
APPS = progman notepad clock dvxdemo cpanel imgview
|
||||
|
||||
.PHONY: all clean $(APPS)
|
||||
|
||||
all: $(APPS)
|
||||
|
||||
cpanel: $(BINDIR)/cpanel/cpanel.app
|
||||
imgview: $(BINDIR)/imgview/imgview.app
|
||||
progman: $(BINDIR)/progman/progman.app
|
||||
notepad: $(BINDIR)/notepad/notepad.app
|
||||
clock: $(BINDIR)/clock/clock.app
|
||||
dvxdemo: $(BINDIR)/dvxdemo/dvxdemo.app
|
||||
|
||||
$(BINDIR)/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/cpanel
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DVXRES) build $@ cpanel/cpanel.res
|
||||
|
||||
$(BINDIR)/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/imgview
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DVXRES) build $@ imgview/imgview.res
|
||||
|
||||
$(BINDIR)/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/progman
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
|
||||
$(BINDIR)/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/notepad
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DVXRES) build $@ notepad/notepad.res
|
||||
|
||||
$(BINDIR)/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/clock
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -E _appShutdown -U $<
|
||||
$(DVXRES) build $@ clock/clock.res
|
||||
|
||||
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
|
||||
|
||||
$(BINDIR)/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/dvxdemo
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DVXRES) build $@ dvxdemo/dvxdemo.res
|
||||
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/dvxdemo/
|
||||
|
||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/imgview.o: imgview/imgview.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/notepad.o: notepad/notepad.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(BINDIR)/cpanel:
|
||||
mkdir -p $(BINDIR)/cpanel
|
||||
|
||||
$(BINDIR)/imgview:
|
||||
mkdir -p $(BINDIR)/imgview
|
||||
|
||||
$(BINDIR)/progman:
|
||||
mkdir -p $(BINDIR)/progman
|
||||
|
||||
$(BINDIR)/notepad:
|
||||
mkdir -p $(BINDIR)/notepad
|
||||
|
||||
$(BINDIR)/clock:
|
||||
mkdir -p $(BINDIR)/clock
|
||||
|
||||
$(BINDIR)/dvxdemo:
|
||||
mkdir -p $(BINDIR)/dvxdemo
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/imgview.o: imgview/imgview.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../core/dvxVideo.h ../shell/shellApp.h
|
||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxPrefs.h ../core/dvxWidget.h ../core/dvxWm.h ../core/platform/dvxPlatform.h ../shell/shellApp.h
|
||||
$(OBJDIR)/progman.o: progman/progman.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../shell/shellApp.h ../shell/shellInfo.h
|
||||
$(OBJDIR)/notepad.o: notepad/notepad.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../shell/shellApp.h
|
||||
$(OBJDIR)/clock.o: clock/clock.c ../core/dvxApp.h ../core/dvxWidget.h ../core/dvxDraw.h ../core/dvxVideo.h ../shell/shellApp.h ../tasks/taskswitch.h
|
||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../core/dvxVideo.h ../shell/shellApp.h
|
||||
|
||||
clean:
|
||||
rm -f $(OBJDIR)/cpanel.o $(OBJDIR)/imgview.o $(OBJDIR)/progman.o $(OBJDIR)/notepad.o $(OBJDIR)/clock.o $(OBJDIR)/dvxdemo.o
|
||||
rm -f $(BINDIR)/cpanel/cpanel.app
|
||||
rm -f $(BINDIR)/imgview/imgview.app
|
||||
rm -f $(BINDIR)/progman/progman.app
|
||||
rm -f $(BINDIR)/notepad/notepad.app
|
||||
rm -f $(BINDIR)/clock/clock.app
|
||||
rm -f $(BINDIR)/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/dvxdemo/,$(DVXDEMO_BMPS))
|
||||
178
apps/README.md
178
apps/README.md
|
|
@ -1,178 +0,0 @@
|
|||
# DVX Shell Applications
|
||||
|
||||
DXE3 application modules for the DVX Shell. Each app is a `.app` file
|
||||
(DXE3 shared object) in a subdirectory under `apps/`. The Program
|
||||
Manager scans this directory at startup and displays all discovered
|
||||
apps as launchable icons.
|
||||
|
||||
|
||||
## DXE App Contract
|
||||
|
||||
Every app exports two symbols:
|
||||
|
||||
* `appDescriptor` (`AppDescriptorT`) -- name, hasMainLoop, multiInstance, stackSize, priority
|
||||
* `appMain` (`int appMain(DxeAppContextT *)`) -- entry point
|
||||
|
||||
Optional: `appShutdown` (`void appShutdown(void)`) -- called during
|
||||
graceful shutdown.
|
||||
|
||||
### Callback-Only Apps (hasMainLoop = false)
|
||||
|
||||
`appMain()` creates windows, registers callbacks, and returns 0. The
|
||||
shell drives everything through event callbacks. No dedicated task or
|
||||
stack is allocated. Lifecycle ends when the last window closes.
|
||||
|
||||
### Main-Loop Apps (hasMainLoop = true)
|
||||
|
||||
A cooperative task is created for the app. `appMain()` runs its own
|
||||
loop calling `tsYield()` to share CPU. Lifecycle ends when `appMain()`
|
||||
returns.
|
||||
|
||||
|
||||
## Applications
|
||||
|
||||
### Program Manager (progman)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/progman/progman.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
The desktop app. Scans `apps/` recursively for `.app` files and
|
||||
displays them in a grid. Double-click or Enter launches an app.
|
||||
Includes a Help menu with system information dialog and About box.
|
||||
|
||||
Registers with `shellRegisterDesktopUpdate()` to refresh when apps
|
||||
are loaded, crash, or terminate.
|
||||
|
||||
Widget headers used: `widgetBox.h`, `widgetButton.h`, `widgetLabel.h`,
|
||||
`widgetStatusBar.h`, `widgetTextInput.h`.
|
||||
|
||||
### Notepad (notepad)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/notepad/notepad.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | Yes |
|
||||
|
||||
Text editor with File menu (New, Open, Save, Save As) and Edit menu
|
||||
(Cut, Copy, Paste, Select All). Uses the TextArea widget for all
|
||||
editing. 32KB text buffer limit. Tracks dirty state for save prompts.
|
||||
Keyboard accelerators for all menu commands.
|
||||
|
||||
Widget headers used: `widgetTextInput.h`.
|
||||
|
||||
### Clock (clock)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/clock/clock.app` |
|
||||
| Type | Main-loop |
|
||||
| Multi-instance | Yes |
|
||||
|
||||
Digital clock display showing time and date. Demonstrates the main-loop
|
||||
app pattern -- polls the system clock every second and invalidates the
|
||||
window to trigger a repaint. Uses raw `onPaint` callbacks (no widgets)
|
||||
to draw centered text.
|
||||
|
||||
Widget headers used: none (raw paint callbacks).
|
||||
|
||||
### DVX Demo (dvxdemo)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/dvxdemo/dvxdemo.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
Comprehensive widget system showcase. Opens multiple windows
|
||||
demonstrating 31 of the 32 widget types (all except Timer):
|
||||
|
||||
* Main window: raw paint callbacks (gradients, patterns, text)
|
||||
* Widget demo: form widgets (TextInput, Checkbox, Radio, ListBox)
|
||||
* Controls window: TabControl with tabs for advanced widgets
|
||||
(Dropdown, ProgressBar, Slider, Spinner, TreeView, ListView,
|
||||
ScrollPane, Toolbar, Canvas, Splitter, Image)
|
||||
* Terminal window: AnsiTerm widget
|
||||
|
||||
Widget headers used: 25 of the 26 widget headers (all except widgetTimer.h).
|
||||
|
||||
Resources: `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`, `save.bmp`.
|
||||
|
||||
### Control Panel (cpanel)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/cpanel/cpanel.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
System configuration with four tabs:
|
||||
|
||||
* **Mouse** -- scroll direction, double-click speed, acceleration
|
||||
* **Colors** -- all 20 system colors with live preview, theme load/save
|
||||
* **Desktop** -- wallpaper image selection and display mode
|
||||
* **Video** -- resolution and color depth switching
|
||||
|
||||
Changes preview live. OK saves to `DVX.INI`, Cancel reverts to the
|
||||
state captured when the control panel was opened.
|
||||
|
||||
Widget headers used: `widgetBox.h`, `widgetButton.h`, `widgetCanvas.h`,
|
||||
`widgetDropdown.h`, `widgetLabel.h`, `widgetListBox.h`, `widgetSlider.h`,
|
||||
`widgetSpacer.h`, `widgetTabControl.h`.
|
||||
|
||||
### Image Viewer (imgview)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/imgview/imgview.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | Yes |
|
||||
|
||||
Displays BMP, PNG, JPEG, and GIF images. The image is scaled to fit
|
||||
the window while preserving aspect ratio. Resize the window to zoom.
|
||||
Open files via the File menu or by launching with Run in the Task
|
||||
Manager.
|
||||
|
||||
Widget headers used: none (raw paint callbacks with `dvxLoadImage()`).
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
make # builds all 6 app DXE modules
|
||||
make clean # removes objects and app files
|
||||
```
|
||||
|
||||
Each app compiles to a single `.o`, then is packaged via `dxe3gen`
|
||||
into a `.app` DXE exporting `appDescriptor` and `appMain` (plus
|
||||
`appShutdown` for clock).
|
||||
|
||||
Output goes to `bin/apps/<name>/<name>.app`.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
apps/
|
||||
Makefile top-level build for all apps
|
||||
progman/
|
||||
progman.c Program Manager
|
||||
notepad/
|
||||
notepad.c text editor
|
||||
clock/
|
||||
clock.c digital clock
|
||||
dvxdemo/
|
||||
dvxdemo.c widget demo
|
||||
logo.bmp DVX logo bitmap
|
||||
new.bmp toolbar icon
|
||||
open.bmp toolbar icon
|
||||
sample.bmp sample image
|
||||
save.bmp toolbar icon
|
||||
cpanel/
|
||||
cpanel.c control panel
|
||||
imgview/
|
||||
imgview.c image viewer
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# clock.res -- Resource manifest for Clock
|
||||
icon32 icon clock/icon32.bmp
|
||||
name text "Clock"
|
||||
author text "DVX Project"
|
||||
description text "Digital clock with date display"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# cpanel.res -- Resource manifest for Control Panel
|
||||
icon32 icon cpanel/icon32.bmp
|
||||
name text "Control Panel"
|
||||
author text "DVX Project"
|
||||
description text "System settings and preferences"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# dvxdemo.res -- Resource manifest for DVX Demo
|
||||
icon32 icon dvxdemo/icon32.bmp
|
||||
name text "DVX Demo"
|
||||
author text "DVX Project"
|
||||
description text "Widget toolkit demonstration"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# imgview.res -- Resource manifest for Image Viewer
|
||||
icon32 icon imgview/icon32.bmp
|
||||
name text "Image Viewer"
|
||||
author text "DVX Project"
|
||||
description text "BMP, PNG, JPEG, and GIF viewer"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# notepad.res -- Resource manifest for Notepad
|
||||
icon32 icon notepad/icon32.bmp
|
||||
name text "Notepad"
|
||||
author text "DVX Project"
|
||||
description text "Simple text editor"
|
||||
BIN
assets/DVX Help Logo.xcf
(Stored with Git LFS)
Normal file
BIN
assets/DVX Help Logo.xcf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/DVX Logo.xcf
(Stored with Git LFS)
Normal file
BIN
assets/DVX Logo.xcf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/DVX Text.xcf
(Stored with Git LFS)
Normal file
BIN
assets/DVX Text.xcf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/help.png
(Stored with Git LFS)
Normal file
BIN
assets/help.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/splash.bmp
(Stored with Git LFS)
Normal file
BIN
assets/splash.bmp
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -29,6 +29,8 @@ build. Text files are converted to DOS line endings (CR+LF) via sed.
|
|||
| `listbox.dep` | `bin/widgets/listbox.dep` | ListBox widget dep file |
|
||||
| `listview.dep` | `bin/widgets/listview.dep` | ListView widget dep file |
|
||||
| `treeview.dep` | `bin/widgets/treeview.dep` | TreeView widget dep file |
|
||||
| `basrt.dep` | `bin/libs/basrt.dep` | BASIC runtime dep file |
|
||||
| `serial.dep` | `bin/libs/serial.dep` | Serial communications dep file |
|
||||
|
||||
|
||||
## dvx.ini Format
|
||||
|
|
@ -126,6 +128,8 @@ ignored. Names are case-insensitive.
|
|||
| `texthelp.dep` | texthelp.lib | libtasks, libdvx |
|
||||
| `listhelp.dep` | listhelp.lib | libtasks, libdvx |
|
||||
| `dvxshell.dep` | dvxshell.lib | libtasks, libdvx, texthelp, listhelp |
|
||||
| `basrt.dep` | basrt.lib | libtasks, libdvx |
|
||||
| `serial.dep` | serial.lib | libtasks, libdvx |
|
||||
|
||||
### Widget Dependencies
|
||||
|
||||
|
|
|
|||
|
|
@ -5,22 +5,26 @@
|
|||
; Supported color depths: 8, 15, 16, 24, 32
|
||||
|
||||
[video]
|
||||
width = 640
|
||||
height = 480
|
||||
width = 1024
|
||||
height = 768
|
||||
bpp = 16
|
||||
|
||||
; Mouse settings.
|
||||
; wheel: normal or reversed
|
||||
; wheelspeed: lines per wheel notch (1-10, default 3)
|
||||
; doubleclick: double-click speed in milliseconds (200-900, default 500)
|
||||
; acceleration: off, low, medium, high (default medium)
|
||||
; speed: cursor speed (2-32, default 8; higher = faster)
|
||||
|
||||
[mouse]
|
||||
wheel = normal
|
||||
wheelspeed = 3
|
||||
doubleclick = 500
|
||||
acceleration = medium
|
||||
speed = 8
|
||||
|
||||
; Shell settings.
|
||||
; desktop: path to the desktop app loaded at startup
|
||||
|
||||
[shell]
|
||||
desktop = apps/progman/progman.app
|
||||
desktop = apps/kpunch/progman/progman.app
|
||||
|
|
|
|||
25
config/themes/hotdog.thm
Normal file
25
config/themes/hotdog.thm
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
; DVX Color Theme - Hot Dog Stand
|
||||
; The infamous Windows 3.1 color scheme. Yellow, red, black.
|
||||
; Included here for historical authenticity and eye strain.
|
||||
|
||||
[colors]
|
||||
desktop = 255,255,0
|
||||
windowFace = 255,255,0
|
||||
windowHighlight = 255,255,255
|
||||
windowShadow = 0,0,0
|
||||
activeTitleBg = 255,0,0
|
||||
activeTitleFg = 255,255,255
|
||||
inactiveTitleBg = 255,255,255
|
||||
inactiveTitleFg = 0,0,0
|
||||
contentBg = 255,255,255
|
||||
contentFg = 0,0,0
|
||||
menuBg = 255,255,255
|
||||
menuFg = 0,0,0
|
||||
menuHighlightBg = 255,0,0
|
||||
menuHighlightFg = 255,255,255
|
||||
buttonFace = 255,255,0
|
||||
scrollbarBg = 255,255,0
|
||||
scrollbarFg = 0,0,0
|
||||
scrollbarTrough = 255,255,255
|
||||
cursorColor = 255,255,0
|
||||
cursorOutline = 0,0,0
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# DVX Core Library Makefile for DJGPP cross-compilation
|
||||
#
|
||||
# Builds libdvx.lib -- core GUI infrastructure (draw, compositor,
|
||||
# window manager, event dispatch, layout engine, widget infrastructure).
|
||||
# Zero widget implementations -- those are in ../widgets/ as .wgt modules.
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -Iplatform -I../tasks -Ithirdparty
|
||||
|
||||
OBJDIR = ../obj/core
|
||||
LIBSDIR = ../bin/libs
|
||||
|
||||
# Core sources
|
||||
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c \
|
||||
dvxApp.c dvxDialog.c dvxPrefs.c dvxResource.c \
|
||||
widgetClass.c widgetCore.c widgetScrollbar.c \
|
||||
widgetLayout.c widgetEvent.c widgetOps.c
|
||||
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBSDIR)/libdvx.lib
|
||||
|
||||
# libdvx.lib export prefixes
|
||||
DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _text \
|
||||
-E _setClip -E _resetClip -E _stbi_ -E _stbi_write -E _dirtyList \
|
||||
-E _widget \
|
||||
-E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \
|
||||
-E _sOpen -E _sPressed -E _sDrag -E _sDrawing -E _sResize \
|
||||
-E _sListView -E _sSplitter -E _sTreeView \
|
||||
-E _accelParse -E _clipboard -E _multiClick
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET) $(LIBSDIR)/libdvx.dep
|
||||
|
||||
$(LIBSDIR)/libdvx.dep: ../config/libdvx.dep | $(LIBSDIR)
|
||||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBSDIR)
|
||||
$(DXE3GEN) -o $(LIBSDIR)/libdvx.dxe $(DVX_EXPORTS) -U $(OBJS)
|
||||
mv $(LIBSDIR)/libdvx.dxe $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBSDIR):
|
||||
mkdir -p $(LIBSDIR)
|
||||
|
||||
# Dependencies
|
||||
CORE_HDRS = dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h dvxWidget.h platform/dvxPlatform.h
|
||||
$(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h platform/dvxPlatform.h dvxTypes.h dvxPalette.h
|
||||
$(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h platform/dvxPlatform.h dvxTypes.h
|
||||
$(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h platform/dvxPlatform.h dvxTypes.h
|
||||
$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h dvxWidget.h thirdparty/stb_image.h
|
||||
$(OBJDIR)/dvxImage.o: dvxImage.c thirdparty/stb_image.h
|
||||
$(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h
|
||||
$(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h
|
||||
$(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h dvxWidgetPlugin.h dvxTypes.h dvxDraw.h
|
||||
$(OBJDIR)/dvxPrefs.o: dvxPrefs.c dvxPrefs.h
|
||||
|
||||
WIDGET_DEPS = dvxWidgetPlugin.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
|
||||
$(OBJDIR)/widgetClass.o: widgetClass.c $(WIDGET_DEPS)
|
||||
$(OBJDIR)/widgetCore.o: widgetCore.c $(WIDGET_DEPS)
|
||||
$(OBJDIR)/widgetScrollbar.o: widgetScrollbar.c $(WIDGET_DEPS)
|
||||
$(OBJDIR)/widgetLayout.o: widgetLayout.c $(WIDGET_DEPS)
|
||||
$(OBJDIR)/widgetEvent.o: widgetEvent.c $(WIDGET_DEPS)
|
||||
$(OBJDIR)/widgetOps.o: widgetOps.c $(WIDGET_DEPS)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/libdvx.dep
|
||||
449
core/README.md
449
core/README.md
|
|
@ -1,449 +0,0 @@
|
|||
# DVX Core Library (libdvx.lib)
|
||||
|
||||
The core GUI infrastructure for DVX, built as a DXE3 module. Provides
|
||||
VESA video setup, 2D drawing primitives, dirty-rectangle compositing,
|
||||
a window manager with Motif-style chrome, and the widget infrastructure
|
||||
(layout engine, event dispatch, class registration). Individual widget
|
||||
type implementations live in `../widgets/` as separate `.wgt` DXE
|
||||
modules that register themselves at runtime via `wgtRegisterClass()`.
|
||||
|
||||
Core knows nothing about individual widget types. There is no
|
||||
WidgetTypeE enum, no widget union, and no per-widget structs in
|
||||
dvxWidget.h. All widget-specific behavior is dispatched through the
|
||||
WidgetClassT dispatch table.
|
||||
|
||||
|
||||
## 5-Layer Architecture
|
||||
|
||||
| Layer | Header | Source | Description |
|
||||
|-------|--------|--------|-------------|
|
||||
| 1. Video | `dvxVideo.h` | `dvxVideo.c` | VESA VBE init, LFB mapping, backbuffer, pixel format, `packColor()` |
|
||||
| 2. Draw | `dvxDraw.h` | `dvxDraw.c` | Rect fills, bevels, text, bitmap cursors, focus rects, lines |
|
||||
| 3. Compositor | `dvxComp.h` | `dvxComp.c` | Dirty rect tracking, merge, clip, LFB flush |
|
||||
| 4. Window Manager | `dvxWm.h` | `dvxWm.c` | Window stack, chrome, drag/resize, menus, scrollbars, hit test |
|
||||
| 5. Application | `dvxApp.h` | `dvxApp.c` | Event loop, input polling, color schemes, wallpaper, public API |
|
||||
|
||||
Additional modules built into libdvx.lib:
|
||||
|
||||
| Header | Source | Description |
|
||||
|--------|--------|-------------|
|
||||
| `dvxDialog.h` | `dvxDialog.c` | Modal message box and file open/save dialogs |
|
||||
| `dvxPrefs.h` | `dvxPrefs.c` | INI-based preferences (read/write with typed accessors) |
|
||||
| `dvxResource.h` | `dvxResource.c` | Resource system -- icons, text, and binary data appended to DXE files |
|
||||
| `dvxMem.h` | (header only) | Per-app memory tracking API declarations |
|
||||
| `dvxWidget.h` | `widgetClass.c`, `widgetCore.c`, `widgetEvent.c`, `widgetLayout.c`, `widgetOps.c`, `widgetScrollbar.c` | Widget infrastructure |
|
||||
| `dvxWidgetPlugin.h` | (header only) | Plugin API for widget DXE modules |
|
||||
| -- | `dvxImage.c` | Image loading via stb_image (BMP, PNG, JPEG, GIF) |
|
||||
| -- | `dvxImageWrite.c` | PNG export via stb_image_write |
|
||||
|
||||
|
||||
## Source Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `dvxVideo.c` | VESA mode negotiation, LFB mapping via DPMI, backbuffer alloc, `packColor()` |
|
||||
| `dvxDraw.c` | `rectFill()`, `rectCopy()`, `drawBevel()`, `drawText()`, `drawTextN()`, `drawTermRow()`, cursor rendering |
|
||||
| `dvxComp.c` | `dirtyListAdd()`, `dirtyListMerge()`, `flushRect()`, `rectIntersect()` |
|
||||
| `dvxWm.c` | Window create/destroy, Z-order, chrome drawing, drag/resize, menu bar, scrollbars, minimize/maximize |
|
||||
| `dvxApp.c` | `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, screenshots |
|
||||
| `dvxDialog.c` | `dvxMessageBox()`, `dvxFileDialog()` -- modal dialogs with own event loops |
|
||||
| `dvxPrefs.c` | `prefsLoad()`, `prefsSave()`, typed get/set for string/int/bool |
|
||||
| `dvxResource.c` | `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` -- resource system |
|
||||
| `dvxImage.c` | `dvxLoadImage()`, `dvxLoadImageFromMemory()` -- stb_image loader, converts to native pixel format |
|
||||
| `dvxImageWrite.c` | `dvxSaveImage()` -- PNG writer for screenshots |
|
||||
| `widgetClass.c` | `wgtRegisterClass()`, `wgtRegisterApi()`, `wgtGetApi()`, class table |
|
||||
| `widgetCore.c` | Widget allocation, tree ops, focus management, clipboard, hit testing, cursor blink |
|
||||
| `widgetEvent.c` | `widgetOnMouse()`, `widgetOnKey()`, `widgetOnPaint()`, `widgetOnResize()`, scrollbar management |
|
||||
| `widgetLayout.c` | Two-pass flexbox layout: bottom-up `calcMinSize`, top-down space allocation with weights |
|
||||
| `widgetOps.c` | `wgtPaint()`, `wgtLayout()`, `wgtInitWindow()`, text get/set, invalidation |
|
||||
| `widgetScrollbar.c` | Scrollbar drawing (H/V), thumb calculation, hit testing, drag update |
|
||||
|
||||
|
||||
## Public Headers
|
||||
|
||||
| Header | Purpose |
|
||||
|--------|---------|
|
||||
| `dvxTypes.h` | All shared types: DisplayT, RectT, BlitOpsT, BevelStyleT, BitmapFontT, ColorSchemeT, WindowT, MenuT, ScrollbarT, CursorT, PopupStateT |
|
||||
| `dvxVideo.h` | `videoInit()`, `videoShutdown()`, `packColor()`, `setClipRect()`, `resetClipRect()` |
|
||||
| `dvxDraw.h` | All drawing functions: `rectFill()`, `drawBevel()`, `drawText()`, `drawTextN()`, `drawTermRow()`, etc. |
|
||||
| `dvxComp.h` | Dirty list operations: `dirtyListAdd()`, `dirtyListMerge()`, `flushRect()`, `rectIntersect()` |
|
||||
| `dvxWm.h` | Window management: `wmCreateWindow()`, `wmDestroyWindow()`, `wmRaiseWindow()`, menus, scrollbars, chrome |
|
||||
| `dvxApp.h` | Application API: `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, image I/O |
|
||||
| `dvxDialog.h` | Modal dialogs: `dvxMessageBox()`, `dvxFileDialog()` |
|
||||
| `dvxPrefs.h` | INI preferences: `prefsLoad()`, `prefsSave()`, typed accessors |
|
||||
| `dvxResource.h` | Resource system: `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` |
|
||||
| `dvxMem.h` | Per-app memory tracking: `dvxMalloc()`, `dvxFree()`, `dvxMemGetAppUsage()`, etc. |
|
||||
| `dvxWidget.h` | Widget system public API: WidgetT, WidgetClassT, size tags, layout, API registry, `wclsFoo()` dispatch helpers |
|
||||
| `dvxWidgetPlugin.h` | Plugin API for widget DXE authors: tree ops, focus, scrollbar helpers, shared state |
|
||||
| `dvxFont.h` | Embedded 8x14 and 8x16 bitmap font data (CP437) |
|
||||
| `dvxCursor.h` | Mouse cursor AND/XOR mask data (arrow, resize H/V/diag, busy) |
|
||||
| `dvxPalette.h` | Default 256-color VGA palette for 8-bit mode |
|
||||
|
||||
|
||||
## Platform Layer
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `platform/dvxPlatform.h` | Platform abstraction API (video, input, spans, DXE, crash recovery, memory tracking) |
|
||||
| `platform/dvxPlatformDos.c` | DJGPP/DPMI implementation (VESA VBE, INT 33h mouse, INT 16h keyboard, asm spans) |
|
||||
|
||||
The platform layer is compiled into dvx.exe (the loader), not into
|
||||
libdvx.lib. Platform functions are exported to all DXE modules via
|
||||
`platformRegisterDxeExports()`.
|
||||
|
||||
|
||||
## Third-Party Libraries
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `thirdparty/stb_image.h` | Image loading (implementation compiled into dvxImage.c) |
|
||||
| `thirdparty/stb_image_write.h` | PNG writing (implementation compiled into dvxImageWrite.c) |
|
||||
| `thirdparty/stb_ds.h` | Dynamic arrays/hash maps (implementation in loader, exported to all DXEs) |
|
||||
|
||||
|
||||
## Dynamic Limits
|
||||
|
||||
All major data structures grow dynamically via realloc. There are no
|
||||
fixed-size limits for:
|
||||
|
||||
- **Windows** -- `WindowStackT.windows` is a dynamic array
|
||||
- **Menus** -- `MenuBarT.menus` and `MenuT.items` are dynamic arrays
|
||||
- **Accelerator entries** -- `AccelTableT.entries` is a dynamic array
|
||||
- **Dirty rectangles** -- `DirtyListT.rects` is a dynamic array
|
||||
- **Submenu depth** -- `PopupStateT.parentStack` is a dynamic array
|
||||
|
||||
The only fixed-size buffers remaining are per-element string fields
|
||||
(`MAX_TITLE_LEN = 128`, `MAX_MENU_LABEL = 32`, `MAX_WIDGET_NAME = 32`)
|
||||
and the system menu (`SYS_MENU_MAX_ITEMS = 10`).
|
||||
|
||||
|
||||
## Resource System
|
||||
|
||||
Resources are appended to DXE3 files (.app, .wgt, .lib) after the
|
||||
normal DXE content. The DXE loader never reads past the DXE header,
|
||||
so appended data is invisible to dlopen.
|
||||
|
||||
File layout:
|
||||
|
||||
[DXE3 content]
|
||||
[resource data entries] -- sequential, variable length
|
||||
[resource directory] -- fixed-size entries (48 bytes each)
|
||||
[footer] -- magic + directory offset + count (16 bytes)
|
||||
|
||||
### Resource Types
|
||||
|
||||
| Define | Value | Description |
|
||||
|--------|-------|-------------|
|
||||
| `DVX_RES_ICON` | 1 | Image data (BMP icon: 16x16, 32x32, etc.) |
|
||||
| `DVX_RES_TEXT` | 2 | Null-terminated string (author, copyright, etc.) |
|
||||
| `DVX_RES_BINARY` | 3 | Arbitrary binary data (app-specific) |
|
||||
|
||||
### Resource API
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `dvxResOpen(path)` | Open a resource handle by reading the footer and directory. Returns NULL if no resources. |
|
||||
| `dvxResRead(h, name, outSize)` | Find a resource by name and read its data into a malloc'd buffer. Caller frees. |
|
||||
| `dvxResFind(h, name)` | Find a resource by name and return its directory entry pointer. |
|
||||
| `dvxResClose(h)` | Close the handle and free associated memory. |
|
||||
|
||||
### Key Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `DvxResDirEntryT` | Directory entry: name[32], type, offset, size, reserved (48 bytes) |
|
||||
| `DvxResFooterT` | Footer: magic (`0x52585644` = "DVXR"), dirOffset, entryCount, reserved (16 bytes) |
|
||||
| `DvxResHandleT` | Runtime handle: path, entries array, entry count |
|
||||
|
||||
|
||||
## Memory Tracking (dvxMem.h)
|
||||
|
||||
Per-app memory tracking wraps malloc/free/calloc/realloc/strdup with a
|
||||
small header per allocation that records the owning app ID and size.
|
||||
DXE code does not need to include dvxMem.h -- the DXE export table maps
|
||||
the standard allocator names to these wrappers transparently.
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `dvxMalloc(size)` | Tracked malloc |
|
||||
| `dvxCalloc(nmemb, size)` | Tracked calloc |
|
||||
| `dvxRealloc(ptr, size)` | Tracked realloc |
|
||||
| `dvxFree(ptr)` | Tracked free (falls through to real free on non-tracked pointers) |
|
||||
| `dvxStrdup(s)` | Tracked strdup |
|
||||
| `dvxMemSnapshotLoad(appId)` | Record baseline memory for leak detection |
|
||||
| `dvxMemGetAppUsage(appId)` | Query current memory usage for an app (bytes) |
|
||||
| `dvxMemResetApp(appId)` | Free all allocations charged to an app |
|
||||
|
||||
The global `dvxMemAppIdPtr` pointer is set by the shell to
|
||||
`&ctx->currentAppId` so the allocator knows which app to charge.
|
||||
|
||||
|
||||
## WidgetT Structure
|
||||
|
||||
The WidgetT struct is generic -- no widget-specific fields or union:
|
||||
|
||||
```c
|
||||
typedef struct WidgetT {
|
||||
int32_t type; // assigned by wgtRegisterClass()
|
||||
const struct WidgetClassT *wclass; // dispatch table pointer
|
||||
char name[MAX_WIDGET_NAME];
|
||||
|
||||
// Tree linkage
|
||||
struct WidgetT *parent, *firstChild, *lastChild, *nextSibling;
|
||||
WindowT *window;
|
||||
|
||||
// Geometry (relative to window content area)
|
||||
int32_t x, y, w, h;
|
||||
int32_t calcMinW, calcMinH; // computed minimum size
|
||||
|
||||
// Size hints (tagged: wgtPixels/wgtChars/wgtPercent, 0 = auto)
|
||||
int32_t minW, minH, maxW, maxH;
|
||||
int32_t prefW, prefH;
|
||||
int32_t weight; // extra-space distribution (0 = fixed)
|
||||
|
||||
// Container properties
|
||||
WidgetAlignE align;
|
||||
int32_t spacing, padding; // tagged sizes
|
||||
|
||||
// Colors (0 = use color scheme defaults)
|
||||
uint32_t fgColor, bgColor;
|
||||
|
||||
// State
|
||||
bool visible, enabled, readOnly, focused;
|
||||
char accelKey;
|
||||
|
||||
// User data and callbacks
|
||||
void *userData;
|
||||
void *data; // widget-private data (allocated by widget DXE)
|
||||
const char *tooltip;
|
||||
MenuT *contextMenu;
|
||||
void (*onClick)(struct WidgetT *w);
|
||||
void (*onDblClick)(struct WidgetT *w);
|
||||
void (*onChange)(struct WidgetT *w);
|
||||
void (*onFocus)(struct WidgetT *w);
|
||||
void (*onBlur)(struct WidgetT *w);
|
||||
} WidgetT;
|
||||
```
|
||||
|
||||
|
||||
## WidgetClassT Dispatch Table
|
||||
|
||||
WidgetClassT is an ABI-stable dispatch table. Method IDs are fixed
|
||||
constants that never change -- adding new methods appends new IDs
|
||||
without shifting existing ones. Widget DXEs compiled against an older
|
||||
DVX version continue to work unmodified.
|
||||
|
||||
```c
|
||||
#define WGT_CLASS_VERSION 1 // bump on breaking ABI change
|
||||
#define WGT_METHOD_MAX 32 // room for future methods
|
||||
|
||||
typedef struct WidgetClassT {
|
||||
uint32_t version;
|
||||
uint32_t flags;
|
||||
void *handlers[WGT_METHOD_MAX];
|
||||
} WidgetClassT;
|
||||
```
|
||||
|
||||
### Method ID Table
|
||||
|
||||
21 methods are currently defined (IDs 0--20). WGT_METHOD_MAX is 32,
|
||||
leaving room for 11 future methods without a version bump.
|
||||
|
||||
| ID | Method ID | Signature | Purpose |
|
||||
|----|-----------|-----------|---------|
|
||||
| 0 | `WGT_METHOD_PAINT` | `void (w, d, ops, font, colors)` | Render the widget |
|
||||
| 1 | `WGT_METHOD_PAINT_OVERLAY` | `void (w, d, ops, font, colors)` | Render overlay (dropdown popup) |
|
||||
| 2 | `WGT_METHOD_CALC_MIN_SIZE` | `void (w, font)` | Compute minimum size (bottom-up pass) |
|
||||
| 3 | `WGT_METHOD_LAYOUT` | `void (w, font)` | Position children (top-down pass) |
|
||||
| 4 | `WGT_METHOD_GET_LAYOUT_METRICS` | `void (w, font, pad, gap, extraTop, borderW)` | Return padding/gap for box layout |
|
||||
| 5 | `WGT_METHOD_ON_MOUSE` | `void (w, root, vx, vy)` | Handle mouse click |
|
||||
| 6 | `WGT_METHOD_ON_KEY` | `void (w, key, mod)` | Handle keyboard input |
|
||||
| 7 | `WGT_METHOD_ON_ACCEL_ACTIVATE` | `void (w, root)` | Handle accelerator key match |
|
||||
| 8 | `WGT_METHOD_DESTROY` | `void (w)` | Free widget-private data |
|
||||
| 9 | `WGT_METHOD_ON_CHILD_CHANGED` | `void (parent, child)` | Notification when a child changes |
|
||||
| 10 | `WGT_METHOD_GET_TEXT` | `const char *(w)` | Return widget text |
|
||||
| 11 | `WGT_METHOD_SET_TEXT` | `void (w, text)` | Set widget text |
|
||||
| 12 | `WGT_METHOD_CLEAR_SELECTION` | `bool (w)` | Clear text/item selection |
|
||||
| 13 | `WGT_METHOD_CLOSE_POPUP` | `void (w)` | Close dropdown popup |
|
||||
| 14 | `WGT_METHOD_GET_POPUP_RECT` | `void (w, font, contentH, popX, popY, popW, popH)` | Compute popup rectangle |
|
||||
| 15 | `WGT_METHOD_ON_DRAG_UPDATE` | `void (w, root, x, y)` | Mouse move during drag |
|
||||
| 16 | `WGT_METHOD_ON_DRAG_END` | `void (w, root, x, y)` | Mouse release after drag |
|
||||
| 17 | `WGT_METHOD_GET_CURSOR_SHAPE` | `int32_t (w, vx, vy)` | Return cursor ID for this position |
|
||||
| 18 | `WGT_METHOD_POLL` | `void (w, win)` | Periodic polling (AnsiTerm comms) |
|
||||
| 19 | `WGT_METHOD_QUICK_REPAINT` | `int32_t (w, outY, outH)` | Fast incremental repaint |
|
||||
| 20 | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` | `void (parent, child)` | Scroll to make a child visible |
|
||||
|
||||
### Typed Dispatch Helpers
|
||||
|
||||
Each `wclsFoo()` inline function extracts a handler by stable method
|
||||
ID, casts it to the correct function pointer type, and calls it with
|
||||
a NULL check. This gives callers type-safe dispatch with the same
|
||||
codegen as a direct struct field call.
|
||||
|
||||
| Helper | Wraps Method ID |
|
||||
|--------|-----------------|
|
||||
| `wclsHas(w, methodId)` | Check if a method is implemented (non-NULL) |
|
||||
| `wclsPaint(w, d, ops, font, colors)` | `WGT_METHOD_PAINT` |
|
||||
| `wclsPaintOverlay(w, d, ops, font, colors)` | `WGT_METHOD_PAINT_OVERLAY` |
|
||||
| `wclsCalcMinSize(w, font)` | `WGT_METHOD_CALC_MIN_SIZE` |
|
||||
| `wclsLayout(w, font)` | `WGT_METHOD_LAYOUT` |
|
||||
| `wclsGetLayoutMetrics(w, font, pad, gap, extraTop, borderW)` | `WGT_METHOD_GET_LAYOUT_METRICS` |
|
||||
| `wclsOnMouse(w, root, vx, vy)` | `WGT_METHOD_ON_MOUSE` |
|
||||
| `wclsOnKey(w, key, mod)` | `WGT_METHOD_ON_KEY` |
|
||||
| `wclsOnAccelActivate(w, root)` | `WGT_METHOD_ON_ACCEL_ACTIVATE` |
|
||||
| `wclsDestroy(w)` | `WGT_METHOD_DESTROY` |
|
||||
| `wclsOnChildChanged(parent, child)` | `WGT_METHOD_ON_CHILD_CHANGED` |
|
||||
| `wclsGetText(w)` | `WGT_METHOD_GET_TEXT` |
|
||||
| `wclsSetText(w, text)` | `WGT_METHOD_SET_TEXT` |
|
||||
| `wclsClearSelection(w)` | `WGT_METHOD_CLEAR_SELECTION` |
|
||||
| `wclsClosePopup(w)` | `WGT_METHOD_CLOSE_POPUP` |
|
||||
| `wclsGetPopupRect(w, font, contentH, popX, popY, popW, popH)` | `WGT_METHOD_GET_POPUP_RECT` |
|
||||
| `wclsOnDragUpdate(w, root, x, y)` | `WGT_METHOD_ON_DRAG_UPDATE` |
|
||||
| `wclsOnDragEnd(w, root, x, y)` | `WGT_METHOD_ON_DRAG_END` |
|
||||
| `wclsGetCursorShape(w, vx, vy)` | `WGT_METHOD_GET_CURSOR_SHAPE` |
|
||||
| `wclsPoll(w, win)` | `WGT_METHOD_POLL` |
|
||||
| `wclsQuickRepaint(w, outY, outH)` | `WGT_METHOD_QUICK_REPAINT` |
|
||||
| `wclsScrollChildIntoView(parent, child)` | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` |
|
||||
|
||||
### WidgetClassT Flags
|
||||
|
||||
| Flag | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| `WCLASS_FOCUSABLE` | 0x0001 | Can receive keyboard focus |
|
||||
| `WCLASS_BOX_CONTAINER` | 0x0002 | Uses VBox/HBox layout algorithm |
|
||||
| `WCLASS_HORIZ_CONTAINER` | 0x0004 | Lays out children horizontally |
|
||||
| `WCLASS_PAINTS_CHILDREN` | 0x0008 | Widget handles child rendering |
|
||||
| `WCLASS_NO_HIT_RECURSE` | 0x0010 | Hit testing stops here |
|
||||
| `WCLASS_FOCUS_FORWARD` | 0x0020 | Accel hit forwards focus to next focusable |
|
||||
| `WCLASS_HAS_POPUP` | 0x0040 | Has dropdown popup overlay |
|
||||
| `WCLASS_SCROLLABLE` | 0x0080 | Accepts mouse wheel events |
|
||||
| `WCLASS_SCROLL_CONTAINER` | 0x0100 | Scroll container (ScrollPane) |
|
||||
| `WCLASS_NEEDS_POLL` | 0x0200 | Needs periodic polling |
|
||||
| `WCLASS_SWALLOWS_TAB` | 0x0400 | Tab key goes to widget, not focus nav |
|
||||
| `WCLASS_RELAYOUT_ON_SCROLL` | 0x0800 | Full relayout on scrollbar drag |
|
||||
| `WCLASS_PRESS_RELEASE` | 0x1000 | Click = press+release (buttons) |
|
||||
| `WCLASS_ACCEL_WHEN_HIDDEN` | 0x2000 | Accel matching works when invisible |
|
||||
|
||||
|
||||
## Widget Registration
|
||||
|
||||
Each widget DXE exports `wgtRegister()`, called by the loader after
|
||||
`dlopen`. The WidgetClassT uses the `handlers[]` array indexed by
|
||||
method IDs:
|
||||
|
||||
```c
|
||||
static int32_t sButtonType;
|
||||
|
||||
static const WidgetClassT sButtonClass = {
|
||||
.version = WGT_CLASS_VERSION,
|
||||
.flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE,
|
||||
.handlers = {
|
||||
[WGT_METHOD_PAINT] = buttonPaint,
|
||||
[WGT_METHOD_CALC_MIN_SIZE] = buttonCalcMinSize,
|
||||
[WGT_METHOD_ON_MOUSE] = buttonOnMouse,
|
||||
[WGT_METHOD_ON_KEY] = buttonOnKey,
|
||||
[WGT_METHOD_DESTROY] = buttonDestroy,
|
||||
[WGT_METHOD_GET_TEXT] = buttonGetText,
|
||||
[WGT_METHOD_SET_TEXT] = buttonSetText,
|
||||
[WGT_METHOD_ON_ACCEL_ACTIVATE] = buttonAccelActivate,
|
||||
}
|
||||
};
|
||||
|
||||
static const ButtonApiT sApi = { .create = buttonCreate };
|
||||
|
||||
void wgtRegister(void) {
|
||||
sButtonType = wgtRegisterClass(&sButtonClass);
|
||||
wgtRegisterApi("button", &sApi);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Per-Widget API Registry
|
||||
|
||||
The monolithic WidgetApiT is gone. Each widget registers a small API
|
||||
struct under a name via `wgtRegisterApi()`. Callers retrieve it via
|
||||
`wgtGetApi()` and cast to the widget-specific type. Per-widget headers
|
||||
(e.g. `widgetButton.h`) provide typed accessors and convenience macros:
|
||||
|
||||
```c
|
||||
// widgetButton.h
|
||||
typedef struct {
|
||||
WidgetT *(*create)(WidgetT *parent, const char *text);
|
||||
} ButtonApiT;
|
||||
|
||||
static inline const ButtonApiT *dvxButtonApi(void) {
|
||||
static const ButtonApiT *sApi;
|
||||
if (!sApi) { sApi = (const ButtonApiT *)wgtGetApi("button"); }
|
||||
return sApi;
|
||||
}
|
||||
|
||||
#define wgtButton(parent, text) dvxButtonApi()->create(parent, text)
|
||||
```
|
||||
|
||||
|
||||
## Tagged Size Values
|
||||
|
||||
Size hints encode both unit type and numeric value in a single int32_t:
|
||||
|
||||
| Macro | Encoding | Example |
|
||||
|-------|----------|---------|
|
||||
| `wgtPixels(v)` | Bits 31:30 = 00 | `w->minW = wgtPixels(200);` |
|
||||
| `wgtChars(v)` | Bits 31:30 = 01 | `w->minW = wgtChars(40);` |
|
||||
| `wgtPercent(v)` | Bits 31:30 = 10 | `w->minW = wgtPercent(50);` |
|
||||
| `0` | -- | Auto (use computed minimum) |
|
||||
|
||||
|
||||
## Layout Algorithm
|
||||
|
||||
Two-pass flexbox-like layout:
|
||||
|
||||
1. **Bottom-up** (`calcMinSize`): Each widget computes its minimum size.
|
||||
Containers sum children along the main axis, max across the cross axis.
|
||||
2. **Top-down** (`layout`): Available space is allocated to children.
|
||||
Extra space beyond minimum is distributed proportionally to weights.
|
||||
`weight=0` means fixed size, `weight=100` is the default flexible weight.
|
||||
|
||||
### Cross-Axis Centering
|
||||
|
||||
When a child widget has a `maxW` (in a VBox) or `maxH` (in an HBox)
|
||||
that constrains it smaller than the available cross-axis space, the
|
||||
layout engine automatically centers the child on the cross axis. This
|
||||
means setting `maxW` or `maxH` on a child inside a container will both
|
||||
cap its size and center it within the remaining space.
|
||||
|
||||
|
||||
## Image Loading
|
||||
|
||||
Two image loading functions are available:
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `dvxLoadImage(ctx, path, outW, outH, outPitch)` | Load from a file path |
|
||||
| `dvxLoadImageFromMemory(ctx, data, dataLen, outW, outH, outPitch)` | Load from a memory buffer (e.g. resource data) |
|
||||
|
||||
Both convert to the display's native pixel format. Caller frees the
|
||||
returned buffer with `dvxFreeImage()`. Supported formats: BMP, PNG,
|
||||
JPEG, GIF (via stb_image).
|
||||
|
||||
|
||||
## Exported Symbols
|
||||
|
||||
libdvx.lib exports symbols matching these prefixes:
|
||||
|
||||
```
|
||||
dvx*, wgt*, wm*, prefs*, rect*, draw*, pack*, text*, setClip*,
|
||||
resetClip*, stbi*, stbi_write*, dirtyList*, widget*,
|
||||
accelParse*, clipboard*, multiClick*,
|
||||
sCursor*, sDbl*, sDebug*, sClosed*, sFocused*, sKey*,
|
||||
sOpen*, sDrag*, sClosed*, sKey*
|
||||
```
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
make # builds bin/libs/libdvx.lib + bin/libs/libdvx.dep
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Depends on: `libtasks.lib` (via libdvx.dep).
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
// dvxDialog.h -- Modal dialogs for DVX GUI
|
||||
//
|
||||
// Provides pre-built modal dialog boxes (message box, file dialog) that
|
||||
// block the caller and run their own event loop via dvxUpdate() until the
|
||||
// user dismisses them. Modal dialogs set ctx->modalWindow to prevent input
|
||||
// from reaching other windows during the dialog's lifetime.
|
||||
//
|
||||
// The flag encoding uses separate bit fields for button configuration
|
||||
// (low nibble) and icon type (high nibble) so they can be OR'd together
|
||||
// in a single int32_t argument, matching the Win16 MessageBox() convention.
|
||||
#ifndef DVX_DIALOG_H
|
||||
#define DVX_DIALOG_H
|
||||
|
||||
#include "dvxApp.h"
|
||||
|
||||
// ============================================================
|
||||
// Message box button flags (low nibble)
|
||||
// ============================================================
|
||||
|
||||
#define MB_OK 0x0000
|
||||
#define MB_OKCANCEL 0x0001
|
||||
#define MB_YESNO 0x0002
|
||||
#define MB_YESNOCANCEL 0x0003
|
||||
#define MB_RETRYCANCEL 0x0004
|
||||
|
||||
// ============================================================
|
||||
// Message box icon flags (high nibble, OR with button flags)
|
||||
// ============================================================
|
||||
|
||||
#define MB_ICONINFO 0x0010
|
||||
#define MB_ICONWARNING 0x0020
|
||||
#define MB_ICONERROR 0x0030
|
||||
#define MB_ICONQUESTION 0x0040
|
||||
|
||||
// ============================================================
|
||||
// Message box return values
|
||||
// ============================================================
|
||||
|
||||
#define ID_OK 1
|
||||
#define ID_CANCEL 2
|
||||
#define ID_YES 3
|
||||
#define ID_NO 4
|
||||
#define ID_RETRY 5
|
||||
|
||||
// Display a modal message box with the specified button and icon combination.
|
||||
// Blocks the caller by running dvxUpdate() in a loop until a button is
|
||||
// pressed or the dialog is closed. Returns the ID_xxx value of the button
|
||||
// that was pressed. The dialog window is automatically destroyed on return.
|
||||
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags);
|
||||
|
||||
// ============================================================
|
||||
// File dialog flags
|
||||
// ============================================================
|
||||
|
||||
#define FD_OPEN 0x0000 // Open file (default)
|
||||
#define FD_SAVE 0x0001 // Save file
|
||||
|
||||
// ============================================================
|
||||
// File dialog filter
|
||||
// ============================================================
|
||||
//
|
||||
// Filters are displayed in a dropdown at the bottom of the file dialog.
|
||||
// Pattern matching is case-insensitive and supports only single glob
|
||||
// patterns (no semicolon-separated lists). This keeps the matching code
|
||||
// trivial for a DOS filesystem where filenames are short and simple.
|
||||
|
||||
typedef struct {
|
||||
const char *label; // e.g. "Text Files (*.txt)"
|
||||
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
|
||||
} FileFilterT;
|
||||
|
||||
// Display a modal file open/save dialog. The dialog shows a directory
|
||||
// listing with navigation (parent directory, drive letters on DOS), a
|
||||
// filename text input, and an optional filter dropdown. Blocks the caller
|
||||
// via dvxUpdate() loop. Returns true if the user selected a file (path
|
||||
// written to outPath), false if cancelled or closed. initialDir may be
|
||||
// NULL to start in the current working directory.
|
||||
bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize);
|
||||
|
||||
#endif // DVX_DIALOG_H
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
// dvxImageWrite.c -- stb_image_write implementation for DVX GUI
|
||||
//
|
||||
// Companion to dvxIcon.c: instantiates stb_image_write for PNG output
|
||||
// (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as
|
||||
// dvxIcon.c for using stb -- zero external dependencies, single header,
|
||||
// public domain. Kept in a separate translation unit from the read side
|
||||
// so projects that don't need screenshot support can omit this file and
|
||||
// save the code size.
|
||||
//
|
||||
// STBI_WRITE_NO_SIMD disables SSE codepaths for the same reason as
|
||||
// STBI_NO_SIMD in dvxIcon.c: the DOS target lacks SSE support.
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
|
||||
#define STBI_WRITE_NO_SIMD
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "thirdparty/stb_image_write.h"
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "dvxMem.h"
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// dvxMem.h -- Per-app memory tracking API for DVX
|
||||
//
|
||||
// Declares the tracked allocation functions. DXE code does NOT need
|
||||
// to include this header for tracking to work -- the DXE export table
|
||||
// maps malloc/free/calloc/realloc/strdup to these wrappers transparently.
|
||||
//
|
||||
// This header is provided for code that needs to call the tracking
|
||||
// functions by name (e.g. dvxMemGetAppUsage in the Task Manager) or
|
||||
// for the dvxMemAppIdPtr declaration.
|
||||
|
||||
#ifndef DVX_MEM_H
|
||||
#define DVX_MEM_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
extern int32_t *dvxMemAppIdPtr;
|
||||
|
||||
void *dvxMalloc(size_t size);
|
||||
void *dvxCalloc(size_t nmemb, size_t size);
|
||||
void *dvxRealloc(void *ptr, size_t size);
|
||||
void dvxFree(void *ptr);
|
||||
char *dvxStrdup(const char *s);
|
||||
void dvxMemSnapshotLoad(int32_t appId);
|
||||
uint32_t dvxMemGetAppUsage(int32_t appId);
|
||||
void dvxMemResetApp(int32_t appId);
|
||||
|
||||
#endif // DVX_MEM_H
|
||||
443
core/dvxPrefs.c
443
core/dvxPrefs.c
|
|
@ -1,443 +0,0 @@
|
|||
// dvxPrefs.c -- INI-based preferences system (read/write)
|
||||
//
|
||||
// Custom INI parser and writer. Stores entries as a dynamic array of
|
||||
// section/key/value triples using stb_ds. Preserves insertion order
|
||||
// on save so the file remains human-readable.
|
||||
|
||||
#include "dvxPrefs.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "dvxMem.h"
|
||||
|
||||
// stb_ds dynamic arrays (implementation lives in libtasks.a)
|
||||
#include "thirdparty/stb_ds.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Internal types
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
char *section;
|
||||
char *key;
|
||||
char *value;
|
||||
} PrefsEntryT;
|
||||
|
||||
// Comment lines are stored to preserve them on save. A comment has
|
||||
// key=NULL and value=the full line text (including the ; prefix).
|
||||
// Section headers have key=NULL and value=NULL.
|
||||
|
||||
static PrefsEntryT *sEntries = NULL; // stb_ds dynamic array
|
||||
static char *sFilePath = NULL; // path used by prefsLoad (for prefsSave)
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
|
||||
static char *dupStr(const char *s) {
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t len = strlen(s);
|
||||
char *d = (char *)malloc(len + 1);
|
||||
|
||||
if (d) {
|
||||
memcpy(d, s, len + 1);
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
static void freeEntry(PrefsEntryT *e) {
|
||||
free(e->section);
|
||||
free(e->key);
|
||||
free(e->value);
|
||||
e->section = NULL;
|
||||
e->key = NULL;
|
||||
e->value = NULL;
|
||||
}
|
||||
|
||||
|
||||
// Case-insensitive string compare
|
||||
static int strcmpci(const char *a, const char *b) {
|
||||
for (;;) {
|
||||
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
|
||||
|
||||
if (d != 0 || !*a) {
|
||||
return d;
|
||||
}
|
||||
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Find an entry by section+key (case-insensitive). Returns index or -1.
|
||||
static int32_t findEntry(const char *section, const char *key) {
|
||||
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
||||
PrefsEntryT *e = &sEntries[i];
|
||||
|
||||
if (e->key && e->section &&
|
||||
strcmpci(e->section, section) == 0 &&
|
||||
strcmpci(e->key, key) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Find the index of a section header entry. Returns -1 if not found.
|
||||
static int32_t findSection(const char *section) {
|
||||
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
||||
PrefsEntryT *e = &sEntries[i];
|
||||
|
||||
if (!e->key && !e->value && e->section &&
|
||||
strcmpci(e->section, section) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Trim leading/trailing whitespace in place. Returns pointer into buf.
|
||||
static char *trimInPlace(char *buf) {
|
||||
while (*buf == ' ' || *buf == '\t') {
|
||||
buf++;
|
||||
}
|
||||
|
||||
char *end = buf + strlen(buf) - 1;
|
||||
|
||||
while (end >= buf && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
|
||||
*end-- = '\0';
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsFree
|
||||
// ============================================================
|
||||
|
||||
void prefsFree(void) {
|
||||
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
||||
freeEntry(&sEntries[i]);
|
||||
}
|
||||
|
||||
arrfree(sEntries);
|
||||
sEntries = NULL;
|
||||
free(sFilePath);
|
||||
sFilePath = NULL;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsGetBool
|
||||
// ============================================================
|
||||
|
||||
bool prefsGetBool(const char *section, const char *key, bool defaultVal) {
|
||||
const char *val = prefsGetString(section, key, NULL);
|
||||
|
||||
if (!val) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
char c = (char)tolower((unsigned char)val[0]);
|
||||
|
||||
if (c == 't' || c == 'y' || c == '1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == 'f' || c == 'n' || c == '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsGetInt
|
||||
// ============================================================
|
||||
|
||||
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) {
|
||||
const char *val = prefsGetString(section, key, NULL);
|
||||
|
||||
if (!val) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
char *end = NULL;
|
||||
long n = strtol(val, &end, 10);
|
||||
|
||||
if (end == val) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
return (int32_t)n;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsGetString
|
||||
// ============================================================
|
||||
|
||||
const char *prefsGetString(const char *section, const char *key, const char *defaultVal) {
|
||||
int32_t idx = findEntry(section, key);
|
||||
|
||||
if (idx < 0) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
return sEntries[idx].value;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsLoad
|
||||
// ============================================================
|
||||
|
||||
bool prefsLoad(const char *filename) {
|
||||
prefsFree();
|
||||
|
||||
// Always store the path so prefsSave can create the file
|
||||
// even if it doesn't exist yet.
|
||||
sFilePath = dupStr(filename);
|
||||
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
|
||||
if (!fp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char line[512];
|
||||
char *currentSection = dupStr("");
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
// Strip trailing whitespace/newline
|
||||
char *end = line + strlen(line) - 1;
|
||||
|
||||
while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t')) {
|
||||
*end-- = '\0';
|
||||
}
|
||||
|
||||
char *p = line;
|
||||
|
||||
// Skip leading whitespace
|
||||
while (*p == ' ' || *p == '\t') {
|
||||
p++;
|
||||
}
|
||||
|
||||
// Blank line -- store as comment to preserve formatting
|
||||
if (*p == '\0') {
|
||||
PrefsEntryT e = {0};
|
||||
e.section = dupStr(currentSection);
|
||||
e.value = dupStr("");
|
||||
arrput(sEntries, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Comment line
|
||||
if (*p == ';' || *p == '#') {
|
||||
PrefsEntryT e = {0};
|
||||
e.section = dupStr(currentSection);
|
||||
e.value = dupStr(line);
|
||||
arrput(sEntries, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Section header
|
||||
if (*p == '[') {
|
||||
char *close = strchr(p, ']');
|
||||
|
||||
if (close) {
|
||||
*close = '\0';
|
||||
free(currentSection);
|
||||
currentSection = dupStr(trimInPlace(p + 1));
|
||||
|
||||
PrefsEntryT e = {0};
|
||||
e.section = dupStr(currentSection);
|
||||
arrput(sEntries, e);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key=value
|
||||
char *eq = strchr(p, '=');
|
||||
|
||||
if (eq) {
|
||||
*eq = '\0';
|
||||
|
||||
PrefsEntryT e = {0};
|
||||
e.section = dupStr(currentSection);
|
||||
e.key = dupStr(trimInPlace(p));
|
||||
e.value = dupStr(trimInPlace(eq + 1));
|
||||
arrput(sEntries, e);
|
||||
}
|
||||
}
|
||||
|
||||
free(currentSection);
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsRemove
|
||||
// ============================================================
|
||||
|
||||
void prefsRemove(const char *section, const char *key) {
|
||||
int32_t idx = findEntry(section, key);
|
||||
|
||||
if (idx >= 0) {
|
||||
freeEntry(&sEntries[idx]);
|
||||
arrdel(sEntries, idx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsSave
|
||||
// ============================================================
|
||||
|
||||
bool prefsSave(void) {
|
||||
if (!sFilePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return prefsSaveAs(sFilePath);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsSaveAs
|
||||
// ============================================================
|
||||
|
||||
bool prefsSaveAs(const char *filename) {
|
||||
FILE *fp = fopen(filename, "wb");
|
||||
|
||||
if (!fp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *lastSection = "";
|
||||
|
||||
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
||||
PrefsEntryT *e = &sEntries[i];
|
||||
|
||||
// Comment or blank line (key=NULL, value=text or empty)
|
||||
if (!e->key && e->value) {
|
||||
fprintf(fp, "%s\r\n", e->value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Section header (key=NULL, value=NULL)
|
||||
if (!e->key && !e->value) {
|
||||
fprintf(fp, "[%s]\r\n", e->section);
|
||||
lastSection = e->section;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key=value
|
||||
if (e->key && e->value) {
|
||||
fprintf(fp, "%s = %s\r\n", e->key, e->value);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsSetBool
|
||||
// ============================================================
|
||||
|
||||
void prefsSetBool(const char *section, const char *key, bool value) {
|
||||
prefsSetString(section, key, value ? "true" : "false");
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsSetInt
|
||||
// ============================================================
|
||||
|
||||
void prefsSetInt(const char *section, const char *key, int32_t value) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%ld", (long)value);
|
||||
prefsSetString(section, key, buf);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsSetString
|
||||
// ============================================================
|
||||
|
||||
void prefsSetString(const char *section, const char *key, const char *value) {
|
||||
int32_t idx = findEntry(section, key);
|
||||
|
||||
if (idx >= 0) {
|
||||
// Update existing entry
|
||||
free(sEntries[idx].value);
|
||||
sEntries[idx].value = dupStr(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create section header
|
||||
int32_t secIdx = findSection(section);
|
||||
|
||||
if (secIdx < 0) {
|
||||
// Add blank line before new section (unless file is empty)
|
||||
if (arrlen(sEntries) > 0) {
|
||||
PrefsEntryT blank = {0};
|
||||
blank.section = dupStr(section);
|
||||
blank.value = dupStr("");
|
||||
arrput(sEntries, blank);
|
||||
}
|
||||
|
||||
// Add section header
|
||||
PrefsEntryT secEntry = {0};
|
||||
secEntry.section = dupStr(section);
|
||||
arrput(sEntries, secEntry);
|
||||
secIdx = arrlen(sEntries) - 1;
|
||||
}
|
||||
|
||||
// Find insertion point: after last entry in this section
|
||||
int32_t insertAt = secIdx + 1;
|
||||
|
||||
while (insertAt < arrlen(sEntries)) {
|
||||
PrefsEntryT *e = &sEntries[insertAt];
|
||||
|
||||
// Stop if we've hit a different section header
|
||||
if (!e->key && !e->value && e->section &&
|
||||
strcmpci(e->section, section) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Stop if we've hit an entry from a different section
|
||||
if (e->section && strcmpci(e->section, section) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
insertAt++;
|
||||
}
|
||||
|
||||
// Insert new entry
|
||||
PrefsEntryT newEntry = {0};
|
||||
newEntry.section = dupStr(section);
|
||||
newEntry.key = dupStr(key);
|
||||
newEntry.value = dupStr(value);
|
||||
arrins(sEntries, insertAt, newEntry);
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
// dvxPrefs.h -- INI-based preferences system (read/write)
|
||||
//
|
||||
// Loads a configuration file at startup and provides typed accessors
|
||||
// with caller-supplied defaults. Values can be modified at runtime
|
||||
// and saved back to disk. If the file is missing or a key is absent,
|
||||
// getters return the default silently.
|
||||
|
||||
#ifndef DVX_PREFS_H
|
||||
#define DVX_PREFS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Load an INI file into memory. Returns true on success, false if the
|
||||
// file could not be opened (all getters will return their defaults).
|
||||
// Only one file may be loaded at a time; calling again frees the previous.
|
||||
bool prefsLoad(const char *filename);
|
||||
|
||||
// Save the current in-memory state back to the file that was loaded.
|
||||
// Returns true on success.
|
||||
bool prefsSave(void);
|
||||
|
||||
// Save the current in-memory state to a specific file.
|
||||
bool prefsSaveAs(const char *filename);
|
||||
|
||||
// Release all memory held by the preferences.
|
||||
void prefsFree(void);
|
||||
|
||||
// Retrieve a string value. Returns defaultVal if the key is not present.
|
||||
// The returned pointer is valid until the key is modified or prefsFree().
|
||||
const char *prefsGetString(const char *section, const char *key, const char *defaultVal);
|
||||
|
||||
// Retrieve an integer value.
|
||||
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal);
|
||||
|
||||
// Retrieve a boolean value. Recognises "true"/"yes"/"1" and "false"/"no"/"0".
|
||||
bool prefsGetBool(const char *section, const char *key, bool defaultVal);
|
||||
|
||||
// Set a string value. Creates the section and key if they don't exist.
|
||||
void prefsSetString(const char *section, const char *key, const char *value);
|
||||
|
||||
// Set an integer value.
|
||||
void prefsSetInt(const char *section, const char *key, int32_t value);
|
||||
|
||||
// Set a boolean value (stored as "true"/"false").
|
||||
void prefsSetBool(const char *section, const char *key, bool value);
|
||||
|
||||
// Remove a key from a section. No-op if not found.
|
||||
void prefsRemove(const char *section, const char *key);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
// dvxResource.c -- DVX resource runtime API
|
||||
//
|
||||
// Reads the resource block appended to DXE3 files. The resource
|
||||
// block is located by reading the footer at the end of the file.
|
||||
|
||||
#include "dvxResource.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
DvxResHandleT *dvxResOpen(const char *path) {
|
||||
if (!path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "rb");
|
||||
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Read footer from end of file
|
||||
if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DvxResFooterT footer;
|
||||
|
||||
if (fread(&footer, sizeof(footer), 1, f) != 1) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Read directory
|
||||
if (fseek(f, footer.dirOffset, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT));
|
||||
|
||||
if (!entries) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) {
|
||||
free(entries);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
DvxResHandleT *h = (DvxResHandleT *)malloc(sizeof(DvxResHandleT));
|
||||
|
||||
if (!h) {
|
||||
free(entries);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
strncpy(h->path, path, sizeof(h->path) - 1);
|
||||
h->path[sizeof(h->path) - 1] = '\0';
|
||||
h->entries = entries;
|
||||
h->entryCount = footer.entryCount;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
|
||||
const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) {
|
||||
if (!h || !name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < h->entryCount; i++) {
|
||||
if (strcmp(h->entries[i].name, name) == 0) {
|
||||
return &h->entries[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize) {
|
||||
const DvxResDirEntryT *entry = dvxResFind(h, name);
|
||||
|
||||
if (!entry) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FILE *f = fopen(h->path, "rb");
|
||||
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fseek(f, entry->offset, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *buf = malloc(entry->size);
|
||||
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fread(buf, 1, entry->size, f) != entry->size) {
|
||||
free(buf);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
if (outSize) {
|
||||
*outSize = entry->size;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
void dvxResClose(DvxResHandleT *h) {
|
||||
if (h) {
|
||||
free(h->entries);
|
||||
free(h);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
#define DVX_WIDGET_IMPL
|
||||
// widgetClass.c -- Widget class table, API registry, and dynamic registration
|
||||
//
|
||||
// widgetClassTable is a stb_ds dynamic array. Widget DXEs call
|
||||
// wgtRegisterClass() during wgtRegister() to append their class
|
||||
// definition and receive a runtime type ID. No widget types are
|
||||
// known at compile time.
|
||||
//
|
||||
// The API registry uses an stb_ds string hashmap for O(1) lookup.
|
||||
// Each widget DXE calls wgtRegisterApi("name", &sApi) during
|
||||
// wgtRegister(). App/core code calls wgtGetApi("name") to get
|
||||
// the API pointer, then casts and calls through it.
|
||||
|
||||
#include "dvxWidgetPlugin.h"
|
||||
#include "stb_ds.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
// stb_ds dynamic array of class pointers. Grows on each
|
||||
// wgtRegisterClass() call. Index = type ID.
|
||||
const WidgetClassT **widgetClassTable = NULL;
|
||||
|
||||
// stb_ds string hashmap: key = widget name, value = API pointer
|
||||
typedef struct {
|
||||
char *key; // stb_ds string key (heap-allocated by shput)
|
||||
const void *value;
|
||||
} ApiMapEntryT;
|
||||
|
||||
static ApiMapEntryT *sApiMap = NULL;
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtGetApi
|
||||
// ============================================================
|
||||
//
|
||||
// Look up a widget API by name. O(1) via stb_ds string hashmap.
|
||||
// Returns NULL if the widget is not loaded. Callers should still
|
||||
// cache the result in a static local to skip even the hash.
|
||||
|
||||
const void *wgtGetApi(const char *name) {
|
||||
if (!name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t idx = shgeti(sApiMap, name);
|
||||
|
||||
if (idx < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sApiMap[idx].value;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtRegisterApi
|
||||
// ============================================================
|
||||
//
|
||||
// Register a widget's public API struct under a name. Called by
|
||||
// each widget DXE during wgtRegister().
|
||||
|
||||
void wgtRegisterApi(const char *name, const void *api) {
|
||||
if (!name || !api) {
|
||||
return;
|
||||
}
|
||||
|
||||
shput(sApiMap, name, api);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtRegisterClass
|
||||
// ============================================================
|
||||
//
|
||||
// Appends a class to the table and returns the assigned type ID.
|
||||
// The ID is simply the array index.
|
||||
|
||||
int32_t wgtRegisterClass(const WidgetClassT *wclass) {
|
||||
if (!wclass) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t id = arrlen(widgetClassTable);
|
||||
arrput(widgetClassTable, wclass);
|
||||
return id;
|
||||
}
|
||||
76
cppcheck.sh
Executable file
76
cppcheck.sh
Executable file
|
|
@ -0,0 +1,76 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# cppcheck.sh -- static analysis for the DVX source tree.
|
||||
#
|
||||
# Usage:
|
||||
# ./cppcheck.sh # whole tree
|
||||
# ./cppcheck.sh src/widgets # one subtree
|
||||
# ./cppcheck.sh src/libs/kpunch/texthelp/textHelp.c # one file
|
||||
#
|
||||
# Exits 0 even when warnings are found -- cppcheck's exit status is
|
||||
# not useful as a gate for this codebase (too many DJGPP-isms it can't
|
||||
# resolve). Read the output.
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
TARGETS="${@:-src}"
|
||||
|
||||
INCLUDES=(
|
||||
-Isrc/libs/kpunch/libdvx
|
||||
-Isrc/libs/kpunch/libdvx/platform
|
||||
-Isrc/libs/kpunch/libdvx/thirdparty
|
||||
-Isrc/libs/kpunch/libtasks
|
||||
-Isrc/libs/kpunch/texthelp
|
||||
-Isrc/libs/kpunch/listhelp
|
||||
-Isrc/libs/kpunch/dvxshell
|
||||
-Isrc/libs/kpunch/taskmgr
|
||||
-Isrc/libs/kpunch/serial
|
||||
-Isrc/widgets/kpunch
|
||||
-Isrc/apps/kpunch/dvxbasic
|
||||
-Isrc/apps/kpunch/dvxbasic/compiler
|
||||
-Isrc/apps/kpunch/dvxbasic/runtime
|
||||
-Isrc/apps/kpunch/dvxbasic/formrt
|
||||
)
|
||||
|
||||
# DJGPP / platform macros cppcheck can't find. Expand them to nothing
|
||||
# so the preprocessor leaves the code visible to the analyser instead
|
||||
# of eliding whole functions behind unknown macro calls.
|
||||
DEFINES=(
|
||||
-DDXE_EXPORT=
|
||||
-DDVX_WIDGET_IMPL
|
||||
-D__DJGPP__=2
|
||||
)
|
||||
|
||||
# Suppressions: things cppcheck flags that aren't actually wrong for
|
||||
# this codebase. Keep the list short so real issues stay visible.
|
||||
SUPPRESS=(
|
||||
--suppress=missingIncludeSystem # system headers aren't on host
|
||||
--suppress=unusedFunction # many public exports look "unused" to a .c-only scan
|
||||
--suppress=constParameterPointer # noisy on this style
|
||||
--suppress=constVariablePointer # ditto
|
||||
--suppress=normalCheckLevelMaxBranches
|
||||
# stb_ds arrput is a macro -- cppcheck can't see that the value
|
||||
# (including heap pointers inside it) is stored, so it reports
|
||||
# spurious null-derefs on the stb_ds dynamic-array handle.
|
||||
--suppress=nullPointerRedundantCheck
|
||||
--suppress=nullPointerArithmeticRedundantCheck
|
||||
# Thirdparty headers -- their own style isn't our problem.
|
||||
--suppress='*:src/libs/kpunch/libdvx/thirdparty/*'
|
||||
--suppress='*:src/libs/kpunch/sql/thirdparty/*'
|
||||
)
|
||||
|
||||
exec cppcheck \
|
||||
--enable=warning,style,performance,portability \
|
||||
--inline-suppr \
|
||||
--std=c99 \
|
||||
--platform=unix32 \
|
||||
--quiet \
|
||||
-j"$(nproc)" \
|
||||
-i src/libs/kpunch/sql/thirdparty \
|
||||
-i src/libs/kpunch/libdvx/thirdparty \
|
||||
"${INCLUDES[@]}" \
|
||||
"${DEFINES[@]}" \
|
||||
"${SUPPRESS[@]}" \
|
||||
$TARGETS
|
||||
3319
docs/dvx_basic_reference.html
Normal file
3319
docs/dvx_basic_reference.html
Normal file
File diff suppressed because it is too large
Load diff
171
docs/dvx_help_viewer.html
Normal file
171
docs/dvx_help_viewer.html
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DVX Help Viewer</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 0; padding: 0; display: flex; }
|
||||
nav { width: 250px; min-width: 250px; background: #f0f0f0; padding: 16px;
|
||||
border-right: 1px solid #ccc; height: 100vh; overflow-y: auto;
|
||||
position: sticky; top: 0; box-sizing: border-box; }
|
||||
nav ul { list-style: none; padding-left: 16px; margin: 4px 0; }
|
||||
nav > ul { padding-left: 0; }
|
||||
nav a { text-decoration: none; color: #0066cc; }
|
||||
nav a:hover { text-decoration: underline; }
|
||||
main { flex: 1; padding: 24px 32px; max-width: 800px; }
|
||||
h1 { border-bottom: 2px solid #333; padding-bottom: 4px; }
|
||||
h2 { border-bottom: 1px solid #999; padding-bottom: 2px; margin-top: 32px; }
|
||||
h3 { margin-top: 24px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; padding: 8px;
|
||||
overflow-x: auto; font-size: 14px; }
|
||||
blockquote { background: #fffde7; border-left: 4px solid #ffc107;
|
||||
padding: 8px 12px; margin: 12px 0; }
|
||||
hr { border: none; border-top: 1px solid #ccc; margin: 24px 0; }
|
||||
img { max-width: 100%; }
|
||||
.topic { margin-bottom: 48px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<h3>Contents</h3>
|
||||
<ul>
|
||||
<li><a href="#help.overview">DVX Help Viewer</a></li>
|
||||
<li><a href="#help.format">Help Source Format</a></li>
|
||||
<li><a href="#help.compiler">Help Compiler</a></li>
|
||||
<li><a href="#help.integration">Application Integration</a></li>
|
||||
</ul>
|
||||
<h3>Index</h3>
|
||||
<ul>
|
||||
<li><a href="#help.format">.dhs</a></li>
|
||||
<li><a href="#help.compiler">Compiler</a></li>
|
||||
<li><a href="#help.integration">Context Help</a></li>
|
||||
<li><a href="#help.format">Directives</a></li>
|
||||
<li><a href="#help.overview">DVX Help</a></li>
|
||||
<li><a href="#help.compiler">dvxhlpc</a></li>
|
||||
<li><a href="#help.integration">F1</a></li>
|
||||
<li><a href="#help.overview">Help Viewer</a></li>
|
||||
<li><a href="#help.integration">helpFile</a></li>
|
||||
<li><a href="#help.integration">helpTopic</a></li>
|
||||
<li><a href="#help.integration">shellLoadAppWithArgs</a></li>
|
||||
<li><a href="#help.format">Source Format</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="topic" id="help.overview">
|
||||
<h1>DVX Help Viewer</h1>
|
||||
<p>The DVX Help Viewer displays .hlp help files compiled from .dhs source documents. It provides a tree-based table of contents, scrollable content with word-wrapped text, clickable hyperlinks, full-text search, and a keyword index.</p>
|
||||
<h2>Opening Help</h2>
|
||||
<p>Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.</p>
|
||||
<p>You can also launch the help viewer from an application's Help menu, or by clicking the DVX Help icon in the Program Manager.</p>
|
||||
<h2>Navigation</h2>
|
||||
<ul>
|
||||
<li>Click a topic in the tree on the left to display it</li>
|
||||
<li>Click underlined links in the content to jump to other topics</li>
|
||||
<li>Use the Back and Forward buttons (or Navigate menu) to retrace your steps</li>
|
||||
<li>Use Navigate > Index to browse an alphabetical keyword list</li>
|
||||
</ul>
|
||||
<p>Use Navigate > Search to find topics by keyword</p>
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
<pre> Alt+Left Back
|
||||
Alt+Right Forward
|
||||
Ctrl+F Search
|
||||
Escape Close viewer</pre>
|
||||
</div>
|
||||
<div class="topic" id="help.format">
|
||||
<h1>Help Source Format (.dhs)</h1>
|
||||
<p>Help files are authored as plain text .dhs source files using a simple line-oriented directive format. Lines beginning with a period at column 0 are directives. All other lines are body text, which is automatically word-wrapped by the viewer at display time.</p>
|
||||
<h2>Topic Directives</h2>
|
||||
<pre> .topic <id> Start a new topic with a unique string ID
|
||||
.title <text> Set the topic's display title
|
||||
.toc <depth> <text> Add a table of contents entry (0=root, 1=child, etc.)
|
||||
.default Mark this topic as the one shown when the file opens</pre>
|
||||
<h2>Content Directives</h2>
|
||||
<pre> .h1 <text> Level 1 heading (colored bar)
|
||||
.h2 <text> Level 2 heading (underlined)
|
||||
.h3 <text> Level 3 heading (plain)
|
||||
.hr Horizontal rule
|
||||
.link <id> <text> Hyperlink to another topic
|
||||
.image <file.bmp> Inline image (BMP format)</pre>
|
||||
<h2>Block Directives</h2>
|
||||
<pre> .list Start a bulleted list
|
||||
.item <text> List item (must be inside .list)
|
||||
.endlist End the bulleted list
|
||||
.table Start a preformatted table block
|
||||
.endtable End table block
|
||||
.code Start a preformatted code block
|
||||
.endcode End code block
|
||||
.note [info|tip|warning] Start a callout box
|
||||
.endnote End callout box</pre>
|
||||
<h2>Index Directives</h2>
|
||||
<pre> .index <keyword> Add a keyword to the index pointing to this topic</pre>
|
||||
<h2>Example</h2>
|
||||
<pre><code>.topic intro
|
||||
.title Welcome
|
||||
.toc 0 Welcome
|
||||
.default
|
||||
.index Welcome
|
||||
|
||||
.h1 Welcome
|
||||
|
||||
This is a paragraph of body text. It will be
|
||||
automatically word-wrapped by the viewer.
|
||||
|
||||
.list
|
||||
.item First item
|
||||
.item Second item
|
||||
.endlist
|
||||
|
||||
.link other.topic See also: Other Topic
|
||||
|
||||
.note info
|
||||
This is an informational note.
|
||||
.endnote
|
||||
|
||||
.note tip
|
||||
This is a helpful tip.
|
||||
.endnote
|
||||
|
||||
.note warning
|
||||
This is a warning message.
|
||||
.endnote</code></pre>
|
||||
<h2>Callout Boxes</h2>
|
||||
<p>Three types of callout boxes are available, each with a distinct colored accent bar:</p>
|
||||
<blockquote><strong>Note:</strong> Use info notes for general supplementary information.</blockquote>
|
||||
<blockquote><strong>Tip:</strong> Use tip notes for helpful suggestions and best practices.</blockquote>
|
||||
<blockquote><strong>Warning:</strong> Use warning notes for important cautions the reader should be aware of.</blockquote>
|
||||
</div>
|
||||
<div class="topic" id="help.compiler">
|
||||
<h1>Help Compiler (dvxhlpc)</h1>
|
||||
<p>The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML.</p>
|
||||
<h2>Usage</h2>
|
||||
<pre><code>dvxhlpc -o output.hlp [-i imagedir] [--html out.html] input.dhs [...]</code></pre>
|
||||
<h2>Options</h2>
|
||||
<pre> -o output.hlp Output binary help file (required)
|
||||
-i imagedir Directory to find .image files (default: current dir)
|
||||
--html out.html Also emit a self-contained HTML file</pre>
|
||||
<p>Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.</p>
|
||||
<h2>Build Integration</h2>
|
||||
<p>The standard build pattern globs all fragments:</p>
|
||||
<pre><code>dvxhlpc -o dvxhelp.hlp docs/src/overview.dhs widgets/*/*.dhs</code></pre>
|
||||
<p>New widgets or features just drop a .dhs file in their source directory and it appears in the help on the next build.</p>
|
||||
<h2>HTML Output</h2>
|
||||
<p>The --html flag produces a single self-contained HTML file with a sidebar table of contents, styled headings, lists, code blocks, notes, and embedded images (base64 data URIs). This is useful for viewing documentation on the host machine without running the DOS help viewer.</p>
|
||||
</div>
|
||||
<div class="topic" id="help.integration">
|
||||
<h1>Application Integration</h1>
|
||||
<p>Any DVX application can provide context-sensitive help via the F1 key.</p>
|
||||
<h2>Setting Up Help</h2>
|
||||
<p>In your appMain, set the help file path on the app context:</p>
|
||||
<pre><code>snprintf(ctx->helpFile, sizeof(ctx->helpFile),
|
||||
"%s%cMYAPP.HLP", ctx->appDir, DVX_PATH_SEP);</code></pre>
|
||||
<h2>Context-Sensitive Topics</h2>
|
||||
<p>Update helpTopic as the user navigates your application:</p>
|
||||
<pre><code>snprintf(ctx->helpTopic, sizeof(ctx->helpTopic), "settings.video");</code></pre>
|
||||
<p>When the user presses F1, the shell launches the help viewer with your help file opened to the specified topic.</p>
|
||||
<h2>Launching Help from Menus</h2>
|
||||
<p>To add a Help menu item that opens your help file:</p>
|
||||
<pre><code>shellLoadAppWithArgs(ctx, viewerPath, helpFilePath);</code></pre>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
7189
docs/dvx_system_reference.html
Normal file
7189
docs/dvx_system_reference.html
Normal file
File diff suppressed because it is too large
Load diff
37
dosbox-staging-overrides.conf
Normal file
37
dosbox-staging-overrides.conf
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[sdl]
|
||||
windowresolution = 1024x768
|
||||
|
||||
[dosbox]
|
||||
machine = svga_s3
|
||||
memsize = 16
|
||||
|
||||
[cpu]
|
||||
core = dynamic
|
||||
#cputype = 486
|
||||
#cpu_cycles = 25000
|
||||
#cpu_cycles_protected = 25000
|
||||
|
||||
[render]
|
||||
aspect = true
|
||||
|
||||
[dos]
|
||||
umb = true
|
||||
xms = true
|
||||
ems = true
|
||||
ver = 6.22
|
||||
|
||||
[mouse]
|
||||
mouse_capture = seamless
|
||||
dos_mouse_immediate = true
|
||||
|
||||
[serial]
|
||||
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
|
||||
|
||||
[autoexec]
|
||||
mount d ~/dos
|
||||
rem d:\ctmouse\bin\ctmouse.exe /O
|
||||
mount c .
|
||||
c:
|
||||
cd bin
|
||||
dvx
|
||||
rem exit
|
||||
|
|
@ -26,12 +26,15 @@ vesa oldvbe10 = false
|
|||
umb = true
|
||||
xms = true
|
||||
ems = true
|
||||
ver = 6.22
|
||||
lfn = false
|
||||
|
||||
[serial]
|
||||
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
|
||||
|
||||
[autoexec]
|
||||
mount d ~/dos
|
||||
mount c .
|
||||
c:
|
||||
cd bin
|
||||
dir
|
||||
dvx
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# List Help Library Makefile for DJGPP cross-compilation
|
||||
#
|
||||
# Builds listhelp.lib -- shared list/dropdown helper infrastructure
|
||||
# (dropdown arrow, item length, keyboard navigation, popup list painting).
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -I../core -I../core/platform -I../tasks -I../core/thirdparty
|
||||
|
||||
OBJDIR = ../obj/listhelp
|
||||
LIBSDIR = ../bin/libs
|
||||
|
||||
SRCS = listHelp.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBSDIR)/listhelp.lib
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET) $(LIBSDIR)/listhelp.dep
|
||||
|
||||
$(LIBSDIR)/listhelp.dep: ../config/listhelp.dep | $(LIBSDIR)
|
||||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBSDIR)
|
||||
$(DXE3GEN) -o $(LIBSDIR)/listhelp.dxe -E _widgetDraw -E _widgetDropdown -E _widgetMax -E _widgetNavigate -E _widgetPaint -U $(OBJS)
|
||||
mv $(LIBSDIR)/listhelp.dxe $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBSDIR):
|
||||
mkdir -p $(LIBSDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/listHelp.o: listHelp.c listHelp.h ../core/dvxWidgetPlugin.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/listhelp.dep
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
# listhelp -- Shared List/Dropdown Helper Library
|
||||
|
||||
Shared infrastructure for list and dropdown widgets, built as
|
||||
`listhelp.lib` (DXE3 module). Provides dropdown arrow drawing, item
|
||||
measurement, keyboard navigation, popup rectangle calculation, and
|
||||
popup list painting.
|
||||
|
||||
Used by: Dropdown, ComboBox, ListBox, ListView, TreeView.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Dropdown Arrow Glyph
|
||||
|
||||
```c
|
||||
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops,
|
||||
int32_t centerX, int32_t centerY, uint32_t color);
|
||||
```
|
||||
|
||||
Draws the small downward-pointing triangle glyph used on dropdown
|
||||
buttons.
|
||||
|
||||
### Item Measurement
|
||||
|
||||
```c
|
||||
int32_t widgetMaxItemLen(const char **items, int32_t count);
|
||||
```
|
||||
|
||||
Scans an array of item strings and returns the length of the longest
|
||||
one. Used to size dropdown popups and list columns to fit their
|
||||
content.
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
```c
|
||||
int32_t widgetNavigateIndex(int32_t key, int32_t current,
|
||||
int32_t count, int32_t pageSize);
|
||||
```
|
||||
|
||||
Maps arrow key presses to index changes for list navigation. Handles
|
||||
Up, Down, Home, End, PageUp, and PageDown. Returns the new selected
|
||||
index, clamped to valid range.
|
||||
|
||||
### Popup Rectangle Calculation
|
||||
|
||||
```c
|
||||
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font,
|
||||
int32_t contentH, int32_t itemCount,
|
||||
int32_t *popX, int32_t *popY,
|
||||
int32_t *popW, int32_t *popH);
|
||||
```
|
||||
|
||||
Computes the screen rectangle for a dropdown popup overlay. Positions
|
||||
the popup below the widget (or above if there is not enough room
|
||||
below). Limits height to `DROPDOWN_MAX_VISIBLE` items.
|
||||
|
||||
### Popup List Painting
|
||||
|
||||
```c
|
||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops,
|
||||
const BitmapFontT *font, const ColorSchemeT *colors,
|
||||
int32_t popX, int32_t popY, int32_t popW, int32_t popH,
|
||||
const char **items, int32_t itemCount,
|
||||
int32_t hoverIdx, int32_t scrollPos);
|
||||
```
|
||||
|
||||
Renders the popup overlay list with items, selection highlight, scroll
|
||||
position, and beveled border. Used by Dropdown and ComboBox for their
|
||||
popup overlays.
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| `DROPDOWN_BTN_WIDTH` | 16 | Width of dropdown arrow button area |
|
||||
| `DROPDOWN_MAX_VISIBLE` | 8 | Maximum visible items in popup list |
|
||||
|
||||
|
||||
## Exported Symbols
|
||||
|
||||
Matches prefixes: `_widgetDraw*`, `_widgetDropdown*`, `_widgetMax*`,
|
||||
`_widgetNavigate*`, `_widgetPaint*`.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `listHelp.h` | Public API header |
|
||||
| `listHelp.c` | Complete implementation |
|
||||
| `Makefile` | Builds `bin/libs/listhelp.lib` + dep file |
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
make # builds bin/libs/listhelp.lib + listhelp.dep
|
||||
make clean # removes objects, library, and dep file
|
||||
```
|
||||
|
||||
Depends on: `libtasks.lib`, `libdvx.lib` (via listhelp.dep).
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
// listHelp.c -- Shared list/dropdown helper functions
|
||||
//
|
||||
// Implements dropdown arrow drawing, item length scanning, keyboard
|
||||
// navigation, and popup list painting used by ListBox, Dropdown,
|
||||
// ComboBox, ListView, and TreeView widgets.
|
||||
|
||||
#include "listHelp.h"
|
||||
#include "../texthelp/textHelp.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDrawDropdownArrow
|
||||
// ============================================================
|
||||
//
|
||||
// Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels
|
||||
// wide across 4 rows) centered at the given position. Used by both
|
||||
// Dropdown and ComboBox for the drop button arrow glyph.
|
||||
|
||||
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color) {
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawHLine(d, ops, centerX - 3 + i, centerY + i, 7 - i * 2, color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetMaxItemLen
|
||||
// ============================================================
|
||||
//
|
||||
// Scans an array of string items and returns the maximum strlen.
|
||||
// Shared by ListBox, Dropdown, and ComboBox to cache the widest
|
||||
// item length for calcMinSize without duplicating the loop.
|
||||
|
||||
int32_t widgetMaxItemLen(const char **items, int32_t count) {
|
||||
int32_t maxLen = 0;
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
int32_t slen = (int32_t)strlen(items[i]);
|
||||
|
||||
if (slen > maxLen) {
|
||||
maxLen = slen;
|
||||
}
|
||||
}
|
||||
|
||||
return maxLen;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetNavigateIndex
|
||||
// ============================================================
|
||||
//
|
||||
// Shared keyboard navigation for list-like widgets (ListBox, Dropdown,
|
||||
// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic
|
||||
// so each widget doesn't have to reimplement index clamping.
|
||||
//
|
||||
// Key values use the 0x100 flag to mark extended scan codes (arrow
|
||||
// keys, Home, End, etc.) -- this is the DVX convention for passing
|
||||
// scan codes through the same int32_t channel as ASCII values.
|
||||
//
|
||||
// Returns -1 for unrecognized keys so callers can check whether the
|
||||
// key was consumed.
|
||||
|
||||
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
|
||||
if (key == (0x50 | 0x100)) {
|
||||
// Down arrow
|
||||
if (current < count - 1) {
|
||||
return current + 1;
|
||||
}
|
||||
|
||||
return current < 0 ? 0 : current;
|
||||
}
|
||||
|
||||
if (key == (0x48 | 0x100)) {
|
||||
// Up arrow
|
||||
if (current > 0) {
|
||||
return current - 1;
|
||||
}
|
||||
|
||||
return current < 0 ? 0 : current;
|
||||
}
|
||||
|
||||
if (key == (0x47 | 0x100)) {
|
||||
// Home
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (key == (0x4F | 0x100)) {
|
||||
// End
|
||||
return count - 1;
|
||||
}
|
||||
|
||||
if (key == (0x51 | 0x100)) {
|
||||
// Page Down
|
||||
int32_t n = current + pageSize;
|
||||
return n >= count ? count - 1 : n;
|
||||
}
|
||||
|
||||
if (key == (0x49 | 0x100)) {
|
||||
// Page Up
|
||||
int32_t n = current - pageSize;
|
||||
return n < 0 ? 0 : n;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetPaintPopupList
|
||||
// ============================================================
|
||||
//
|
||||
// Shared popup list painting for Dropdown and ComboBox.
|
||||
|
||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
|
||||
// Draw popup border
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowHighlight;
|
||||
bevel.shadow = colors->windowShadow;
|
||||
bevel.face = colors->contentBg;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
|
||||
|
||||
// Draw items
|
||||
int32_t visibleItems = popH / font->charHeight;
|
||||
int32_t textX = popX + TEXT_INPUT_PAD;
|
||||
int32_t textY = popY + 2;
|
||||
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
|
||||
|
||||
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
|
||||
int32_t idx = scrollPos + i;
|
||||
int32_t iy = textY + i * font->charHeight;
|
||||
uint32_t ifg = colors->contentFg;
|
||||
uint32_t ibg = colors->contentBg;
|
||||
|
||||
if (idx == hoverIdx) {
|
||||
ifg = colors->menuHighlightFg;
|
||||
ibg = colors->menuHighlightBg;
|
||||
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
|
||||
}
|
||||
|
||||
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownPopupRect
|
||||
// ============================================================
|
||||
//
|
||||
// Calculates the screen rectangle for a dropdown/combobox popup list.
|
||||
// Shared between Dropdown and ComboBox since they have identical
|
||||
// popup positioning logic.
|
||||
|
||||
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
|
||||
int32_t visibleItems = itemCount;
|
||||
|
||||
if (visibleItems > DROPDOWN_MAX_VISIBLE) {
|
||||
visibleItems = DROPDOWN_MAX_VISIBLE;
|
||||
}
|
||||
|
||||
if (visibleItems < 1) {
|
||||
visibleItems = 1;
|
||||
}
|
||||
|
||||
*popX = w->x;
|
||||
*popW = w->w;
|
||||
*popH = visibleItems * font->charHeight + 4;
|
||||
|
||||
if (w->y + w->h + *popH <= contentH) {
|
||||
*popY = w->y + w->h;
|
||||
} else {
|
||||
*popY = w->y - *popH;
|
||||
|
||||
if (*popY < 0) {
|
||||
*popY = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
// listHelp.h -- Public API for the shared list/dropdown helper library
|
||||
//
|
||||
// Declares dropdown arrow drawing, item length scanning, keyboard
|
||||
// navigation, and popup list painting shared across widget DXEs
|
||||
// (ListBox, Dropdown, ComboBox, ListView, TreeView).
|
||||
|
||||
#ifndef LIST_HELP_H
|
||||
#define LIST_HELP_H
|
||||
|
||||
#include "../core/dvxWidgetPlugin.h"
|
||||
|
||||
#define DROPDOWN_BTN_WIDTH 16
|
||||
#define DROPDOWN_MAX_VISIBLE 8
|
||||
|
||||
// ============================================================
|
||||
// Dropdown arrow glyph
|
||||
// ============================================================
|
||||
|
||||
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color);
|
||||
|
||||
// ============================================================
|
||||
// Item measurement
|
||||
// ============================================================
|
||||
|
||||
int32_t widgetMaxItemLen(const char **items, int32_t count);
|
||||
|
||||
// ============================================================
|
||||
// Keyboard navigation (Up/Down/Home/End/PgUp/PgDn)
|
||||
// ============================================================
|
||||
|
||||
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
|
||||
|
||||
// ============================================================
|
||||
// Popup rect calculation
|
||||
// ============================================================
|
||||
|
||||
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
|
||||
|
||||
// ============================================================
|
||||
// Popup list painting
|
||||
// ============================================================
|
||||
|
||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
|
||||
|
||||
#endif // LIST_HELP_H
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# DVX Loader Makefile for DJGPP cross-compilation
|
||||
#
|
||||
# Builds the bootstrap loader (dvx.exe) that loads DXE modules.
|
||||
# Links dvxPlatformDos.c directly -- the platform layer provides
|
||||
# the DXE export table via platformRegisterDxeExports().
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
|
||||
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../core/thirdparty
|
||||
LDFLAGS = -lm
|
||||
|
||||
OBJDIR = ../obj/loader
|
||||
POBJDIR = ../obj/loader/platform
|
||||
BINDIR = ../bin
|
||||
|
||||
SRCS = loaderMain.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
POBJS = $(POBJDIR)/dvxPlatformDos.o
|
||||
TARGET = $(BINDIR)/dvx.exe
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(POBJS) | $(BINDIR)
|
||||
$(CC) $(CFLAGS) -o $@ $(OBJS) $(POBJS) $(LDFLAGS) -Wl,-Map=$(BINDIR)/dvx.map
|
||||
$(EXE2COFF) $@
|
||||
cat $(CWSDSTUB) $(BINDIR)/dvx > $@
|
||||
rm -f $(BINDIR)/dvx
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(POBJDIR)/dvxPlatformDos.o: ../core/platform/dvxPlatformDos.c | $(POBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(POBJDIR):
|
||||
mkdir -p $(POBJDIR)
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/loaderMain.o: loaderMain.c ../core/platform/dvxPlatform.h ../core/dvxTypes.h
|
||||
$(POBJDIR)/dvxPlatformDos.o: ../core/platform/dvxPlatformDos.c ../core/platform/dvxPlatform.h ../core/dvxTypes.h ../core/dvxPalette.h
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(POBJS) $(TARGET) $(BINDIR)/dvx.map
|
||||
133
loader/README.md
133
loader/README.md
|
|
@ -1,133 +0,0 @@
|
|||
# DVX Loader
|
||||
|
||||
Bootstrap loader for the DVX desktop environment. Builds as `dvx.exe`
|
||||
-- the only native executable in the system. Everything else is a
|
||||
dynamically loaded DXE3 module.
|
||||
|
||||
|
||||
## What It Does
|
||||
|
||||
1. Changes working directory to the directory containing `dvx.exe`
|
||||
2. Truncates `dvx.log` and initializes logging
|
||||
3. Calls `platformInit()` to suppress Ctrl+C and install signal handlers
|
||||
4. Calls `platformRegisterDxeExports()` to register platform and C
|
||||
runtime symbols (libc, libm, libgcc) for DXE module resolution
|
||||
5. Scans and loads all modules in two phases (see below)
|
||||
6. Finds `shellMain()` via `dlsym` across loaded modules
|
||||
7. Calls `shellMain()` -- the shell takes over from here
|
||||
8. On return, closes all module handles in reverse load order
|
||||
|
||||
|
||||
## Two-Phase Module Loading
|
||||
|
||||
### Phase 1: Libraries (libs/*.lib)
|
||||
|
||||
Recursively scans the `LIBS/` directory for `.lib` files. Each module may have a
|
||||
`.dep` file (same base name, `.dep` extension) listing base names of
|
||||
modules that must load first. The loader resolves the dependency graph
|
||||
and loads in topological order.
|
||||
|
||||
Load order (via dep files):
|
||||
```
|
||||
libtasks.lib (no deps)
|
||||
libdvx.lib (deps: libtasks)
|
||||
texthelp.lib (deps: libtasks, libdvx)
|
||||
listhelp.lib (deps: libtasks, libdvx)
|
||||
dvxshell.lib (deps: libtasks, libdvx, texthelp, listhelp)
|
||||
taskmgr.lib (deps: dvxshell, libtasks, libdvx, texthelp, listhelp)
|
||||
```
|
||||
|
||||
### Phase 2: Widgets (widgets/*.wgt)
|
||||
|
||||
Recursively scans the `WIDGETS/` directory for `.wgt` files. Widget modules may
|
||||
also have `.dep` files (e.g., `textinpt.dep` lists `texthelp`).
|
||||
Loaded in topological order, same as libs.
|
||||
|
||||
After loading each module, the loader checks for a `wgtRegister`
|
||||
export. If present, it is called immediately so the widget registers
|
||||
its class(es) and API with the core.
|
||||
|
||||
|
||||
## Dependency File Format
|
||||
|
||||
Plain text, one dependency base name per line. Empty lines and lines
|
||||
starting with `#` are ignored. Names are case-insensitive.
|
||||
|
||||
Example (`combobox.dep`):
|
||||
```
|
||||
texthelp
|
||||
listhelp
|
||||
```
|
||||
|
||||
|
||||
## Hosted Components
|
||||
|
||||
### dvxLog()
|
||||
|
||||
The global logging function is defined in `loaderMain.c` and exported
|
||||
to all DXE modules. It appends a line to `dvx.log`, opening and
|
||||
closing the file per write so it is never held open.
|
||||
|
||||
```c
|
||||
void dvxLog(const char *fmt, ...);
|
||||
```
|
||||
|
||||
### stb_ds
|
||||
|
||||
The `STB_DS_IMPLEMENTATION` is compiled into the loader with
|
||||
`STBDS_REALLOC` and `STBDS_FREE` overridden to use `dvxRealloc` and
|
||||
`dvxFree`. This means all `arrput`/`hmput`/`arrfree` calls in DXE
|
||||
code route through the per-app memory tracker, so stb_ds memory is
|
||||
attributed correctly in the Task Manager's memory column. stb_ds
|
||||
functions are exported to all DXE modules via the platform export
|
||||
table.
|
||||
|
||||
### Platform Layer
|
||||
|
||||
`dvxPlatformDos.c` is compiled into the loader (not into libdvx.lib).
|
||||
All platform functions are exported to DXE modules. This includes:
|
||||
|
||||
* Video: VESA VBE init, LFB mapping, mode enumeration
|
||||
* Input: INT 33h mouse, INT 16h keyboard, CuteMouse wheel API
|
||||
* Spans: `rep stosl`/`rep movsd` asm inner loops (8/16/32 bpp)
|
||||
* DXE: symbol registration, symbol overrides
|
||||
* Crash: signal handler installation, register dump logging
|
||||
* System: memory info, directory creation, path utilities
|
||||
|
||||
### Per-App Memory Tracking
|
||||
|
||||
The DXE export table maps standard C allocation symbols to tracked
|
||||
wrappers:
|
||||
|
||||
| C Symbol | Mapped To | Effect |
|
||||
|----------|-----------|--------|
|
||||
| `malloc` | `dvxMalloc` | Allocations attributed to `currentAppId` |
|
||||
| `calloc` | `dvxCalloc` | Same |
|
||||
| `realloc` | `dvxRealloc` | Transfers attribution on resize |
|
||||
| `free` | `dvxFree` | Decrements app's tracked usage |
|
||||
| `strdup` | `dvxStrdup` | Tracks the duplicated string |
|
||||
|
||||
This is transparent to DXE code -- apps call `malloc` normally and the
|
||||
tracked wrapper runs instead. The Task Manager reads per-app usage via
|
||||
`dvxMemGetAppUsage()`. When an app is reaped, `dvxMemResetApp()` zeroes
|
||||
its counter.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `loaderMain.c` | Entry point, module scanner, dependency resolver, logger |
|
||||
| `Makefile` | Builds `bin/dvx.exe` |
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
make # builds bin/dvx.exe
|
||||
make clean # removes objects and binary
|
||||
```
|
||||
|
||||
The loader links `loaderMain.c` + `dvxPlatformDos.c` into a native
|
||||
DJGPP executable. The CWSDPMI stub is prepended via exe2coff +
|
||||
CWSDSTUB.EXE for standalone execution.
|
||||
|
|
@ -1,478 +0,0 @@
|
|||
// loaderMain.c -- DVX bootstrap loader entry point
|
||||
//
|
||||
// Loads all DXE modules from two directories:
|
||||
// libs/ *.lib -- core libraries (libtasks, libdvx, dvxshell)
|
||||
// widgets/ *.wgt -- widget type plugins (box, button, listview, etc.)
|
||||
//
|
||||
// Each module may have a .dep file (same base name, .dep extension)
|
||||
// listing base names of modules that must be loaded before it.
|
||||
// The loader resolves the dependency graph and loads in topological
|
||||
// order. After loading, any module that exports wgtRegister() has
|
||||
// it called. Finally, the loader finds and calls shellMain().
|
||||
|
||||
#include "dvxPlatform.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// Route stb_ds allocations through the tracking wrappers so that
|
||||
// arrput/arrfree in DXE code is tracked per-app.
|
||||
extern void *dvxRealloc(void *ptr, size_t size);
|
||||
extern void dvxFree(void *ptr);
|
||||
#define STBDS_REALLOC(c, p, s) dvxRealloc((p), (s))
|
||||
#define STBDS_FREE(c, p) dvxFree(p)
|
||||
#define STB_DS_IMPLEMENTATION
|
||||
#include "stb_ds.h"
|
||||
|
||||
// ============================================================
|
||||
// Constants
|
||||
// ============================================================
|
||||
|
||||
#define LIBS_DIR "LIBS"
|
||||
#define WIDGET_DIR "WIDGETS"
|
||||
#define LOG_PATH "dvx.log"
|
||||
|
||||
// ============================================================
|
||||
// dvxLog -- append a line to dvx.log
|
||||
// ============================================================
|
||||
//
|
||||
// Global logging function exported to all DXE modules.
|
||||
// Opens/closes the file per-write so it's never held open.
|
||||
|
||||
void dvxLog(const char *fmt, ...) {
|
||||
FILE *f = fopen(LOG_PATH, "a");
|
||||
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(f, fmt, ap);
|
||||
va_end(ap);
|
||||
fprintf(f, "\n");
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Module entry for dependency resolution
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
char path[260];
|
||||
char baseName[16];
|
||||
char **deps;
|
||||
bool loaded;
|
||||
void *handle;
|
||||
} ModuleT;
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods);
|
||||
static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize);
|
||||
static void *findSymbol(void **handles, const char *symbol);
|
||||
static void freeMods(ModuleT *mods);
|
||||
static void **loadAllModules(void);
|
||||
static void loadInOrder(ModuleT *mods);
|
||||
static void logAndReadDeps(ModuleT *mods);
|
||||
static void readDeps(ModuleT *mod);
|
||||
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods);
|
||||
|
||||
// ============================================================
|
||||
// extractBaseName -- strip directory and extension, lowercase
|
||||
// ============================================================
|
||||
|
||||
static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) {
|
||||
// Find last directory separator
|
||||
const char *start = path;
|
||||
const char *p = path;
|
||||
|
||||
while (*p) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
start = p + 1;
|
||||
}
|
||||
|
||||
p++;
|
||||
}
|
||||
|
||||
// Copy up to the extension
|
||||
int32_t extLen = strlen(ext);
|
||||
int32_t len = strlen(start);
|
||||
|
||||
if (len > extLen && strcasecmp(start + len - extLen, ext) == 0) {
|
||||
len -= extLen;
|
||||
}
|
||||
|
||||
if (len >= outSize) {
|
||||
len = outSize - 1;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < len; i++) {
|
||||
out[i] = tolower((unsigned char)start[i]);
|
||||
}
|
||||
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// readDeps -- parse .dep file for a module
|
||||
// ============================================================
|
||||
//
|
||||
// The .dep file has the same path as the module but with a .dep
|
||||
// extension. Each line is a dependency base name. Empty lines
|
||||
// and lines starting with # are ignored.
|
||||
|
||||
static void readDeps(ModuleT *mod) {
|
||||
// Build dep file path: replace extension with .dep
|
||||
char depPath[260];
|
||||
strncpy(depPath, mod->path, sizeof(depPath) - 1);
|
||||
depPath[sizeof(depPath) - 1] = '\0';
|
||||
|
||||
char *dot = strrchr(depPath, '.');
|
||||
|
||||
if (!dot) {
|
||||
return;
|
||||
}
|
||||
|
||||
strcpy(dot, ".dep");
|
||||
|
||||
FILE *f = fopen(depPath, "r");
|
||||
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
char line[64];
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
// Strip \r and \n
|
||||
int32_t len = strlen(line);
|
||||
|
||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
||||
line[--len] = '\0';
|
||||
}
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (len == 0 || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lowercase the dep name for case-insensitive matching
|
||||
for (int32_t i = 0; i < len; i++) {
|
||||
line[i] = tolower((unsigned char)line[i]);
|
||||
}
|
||||
|
||||
arrput(mod->deps, strdup(line));
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// scanDir -- recursively find modules with a given extension
|
||||
// ============================================================
|
||||
|
||||
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
|
||||
DIR *dir = opendir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
const char *name = ent->d_name;
|
||||
|
||||
// Skip . and ..
|
||||
if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char path[260];
|
||||
snprintf(path, sizeof(path), "%s/%s", dirPath, name);
|
||||
|
||||
// Check for matching extension
|
||||
int32_t nameLen = strlen(name);
|
||||
int32_t extLen = strlen(ext);
|
||||
|
||||
if (nameLen > extLen && strcasecmp(name + nameLen - extLen, ext) == 0) {
|
||||
ModuleT mod;
|
||||
memset(&mod, 0, sizeof(mod));
|
||||
strncpy(mod.path, path, sizeof(mod.path) - 1);
|
||||
extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName));
|
||||
arrput(*mods, mod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recurse into subdirectories
|
||||
struct stat st;
|
||||
|
||||
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
scanDir(path, ext, mods);
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// allDepsLoaded -- check if a module's dependencies are satisfied
|
||||
// ============================================================
|
||||
//
|
||||
// A dep is satisfied if either:
|
||||
// 1. No module with that base name exists (external, assumed OK)
|
||||
// 2. The module with that base name is already loaded
|
||||
|
||||
static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods) {
|
||||
for (int32_t d = 0; d < arrlen(mod->deps); d++) {
|
||||
for (int32_t j = 0; j < arrlen(mods); j++) {
|
||||
if (strcasecmp(mods[j].baseName, mod->deps[d]) == 0) {
|
||||
if (!mods[j].loaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// loadInOrder -- topological sort and load
|
||||
// ============================================================
|
||||
//
|
||||
// Repeatedly scans the module list for entries whose dependencies
|
||||
// are all satisfied, loads them, and marks them done. Stops when
|
||||
// all modules are loaded or no progress can be made (circular dep).
|
||||
|
||||
static void loadInOrder(ModuleT *mods) {
|
||||
typedef void (*RegFnT)(void);
|
||||
int32_t total = arrlen(mods);
|
||||
int32_t loaded = 0;
|
||||
bool progress;
|
||||
|
||||
do {
|
||||
progress = false;
|
||||
|
||||
for (int32_t i = 0; i < total; i++) {
|
||||
if (mods[i].loaded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!allDepsLoaded(&mods[i], mods)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dvxLog("Loading: %s", mods[i].path);
|
||||
mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL);
|
||||
|
||||
if (!mods[i].handle) {
|
||||
const char *err = dlerror();
|
||||
dvxLog(" FAILED: %s", err ? err : "(unknown)");
|
||||
mods[i].loaded = true;
|
||||
loaded++;
|
||||
progress = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister");
|
||||
|
||||
if (regFn) {
|
||||
regFn();
|
||||
}
|
||||
|
||||
mods[i].loaded = true;
|
||||
loaded++;
|
||||
progress = true;
|
||||
}
|
||||
} while (progress && loaded < total);
|
||||
|
||||
if (loaded < total) {
|
||||
fprintf(stderr, "Module loader: %d of %d modules could not be loaded (circular deps or missing deps)\n", total - loaded, total);
|
||||
|
||||
for (int32_t i = 0; i < total; i++) {
|
||||
if (!mods[i].loaded) {
|
||||
fprintf(stderr, " %s\n", mods[i].path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// loadAllModules -- scan libs/ and widgets/, load in dep order
|
||||
// ============================================================
|
||||
//
|
||||
// Returns a stb_ds dynamic array of dlopen handles.
|
||||
|
||||
static void logAndReadDeps(ModuleT *mods) {
|
||||
dvxLog("Discovered %d modules:", arrlen(mods));
|
||||
|
||||
for (int32_t i = 0; i < arrlen(mods); i++) {
|
||||
dvxLog(" [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName);
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < arrlen(mods); i++) {
|
||||
readDeps(&mods[i]);
|
||||
|
||||
if (arrlen(mods[i].deps) > 0) {
|
||||
dvxLog(" %s deps:", mods[i].baseName);
|
||||
|
||||
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
|
||||
dvxLog(" %s", mods[i].deps[d]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void freeMods(ModuleT *mods) {
|
||||
for (int32_t i = 0; i < arrlen(mods); i++) {
|
||||
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
|
||||
free(mods[i].deps[d]);
|
||||
}
|
||||
|
||||
arrfree(mods[i].deps);
|
||||
}
|
||||
|
||||
arrfree(mods);
|
||||
}
|
||||
|
||||
|
||||
static void **loadAllModules(void) {
|
||||
void **handles = NULL;
|
||||
|
||||
// Phase 1: load libraries in dependency order (libs before widgets)
|
||||
ModuleT *libs = NULL;
|
||||
scanDir(LIBS_DIR, ".lib", &libs);
|
||||
logAndReadDeps(libs);
|
||||
loadInOrder(libs);
|
||||
|
||||
for (int32_t i = 0; i < arrlen(libs); i++) {
|
||||
if (libs[i].handle) {
|
||||
arrput(handles, libs[i].handle);
|
||||
}
|
||||
}
|
||||
|
||||
freeMods(libs);
|
||||
|
||||
// Phase 2: load widgets in dependency order (all libs already loaded)
|
||||
ModuleT *widgets = NULL;
|
||||
scanDir(WIDGET_DIR, ".wgt", &widgets);
|
||||
logAndReadDeps(widgets);
|
||||
loadInOrder(widgets);
|
||||
|
||||
for (int32_t i = 0; i < arrlen(widgets); i++) {
|
||||
if (widgets[i].handle) {
|
||||
arrput(handles, widgets[i].handle);
|
||||
}
|
||||
}
|
||||
|
||||
freeMods(widgets);
|
||||
|
||||
return handles;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// findSymbol -- search loaded handles for a symbol
|
||||
// ============================================================
|
||||
|
||||
static void *findSymbol(void **handles, const char *symbol) {
|
||||
for (int32_t i = 0; i < arrlen(handles); i++) {
|
||||
void *sym = dlsym(handles[i], symbol);
|
||||
|
||||
if (sym) {
|
||||
return sym;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// main
|
||||
// ============================================================
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Change to the directory containing the executable so relative
|
||||
// paths (LIBS/, WIDGETS/, APPS/, CONFIG/) resolve correctly.
|
||||
char exeDir[260];
|
||||
strncpy(exeDir, argv[0], sizeof(exeDir) - 1);
|
||||
exeDir[sizeof(exeDir) - 1] = '\0';
|
||||
|
||||
char *sep = platformPathDirEnd(exeDir);
|
||||
|
||||
if (sep) {
|
||||
*sep = '\0';
|
||||
platformChdir(exeDir);
|
||||
}
|
||||
|
||||
// Truncate log, then use append-per-write
|
||||
FILE *logInit = fopen(LOG_PATH, "w");
|
||||
|
||||
if (logInit) {
|
||||
fclose(logInit);
|
||||
}
|
||||
|
||||
// Suppress Ctrl+C before anything else
|
||||
platformInit();
|
||||
|
||||
dvxLog("DVX Loader starting...");
|
||||
|
||||
// Register platform + libc/libm/runtime symbols for DXE resolution
|
||||
platformRegisterDxeExports();
|
||||
dvxLog("Platform exports registered.");
|
||||
|
||||
// Load all modules from libs/ and widgets/ in dependency order.
|
||||
// Each module may have a .dep file specifying load-before deps.
|
||||
// Widget modules that export wgtRegister() get it called.
|
||||
void **handles = loadAllModules();
|
||||
|
||||
if (!handles || arrlen(handles) == 0) {
|
||||
fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR);
|
||||
arrfree(handles);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Find and call shellMain from whichever module exports it
|
||||
typedef int (*ShellMainFnT)(int, char **);
|
||||
ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain");
|
||||
|
||||
if (!shellMain) {
|
||||
dvxLog("ERROR: No module exports shellMain");
|
||||
|
||||
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
|
||||
dlclose(handles[i]);
|
||||
}
|
||||
|
||||
arrfree(handles);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = shellMain(argc, argv);
|
||||
|
||||
// Clean up in reverse load order
|
||||
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
|
||||
dlclose(handles[i]);
|
||||
}
|
||||
|
||||
arrfree(handles);
|
||||
return result;
|
||||
}
|
||||
46
mkcd.sh
46
mkcd.sh
|
|
@ -1,5 +1,28 @@
|
|||
#!/bin/bash
|
||||
# mkcd.sh -- Build DVX and create a CD-ROM ISO image for 86Box
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
# mkcd.sh -- Build DVX and create a CD-ROM ISO image for the target emulator
|
||||
#
|
||||
# Usage: ./mkcd.sh
|
||||
#
|
||||
|
|
@ -7,8 +30,8 @@
|
|||
# an ISO 9660 image from the bin/ directory. The ISO uses short
|
||||
# 8.3 filenames (-iso-level 1) for DOS compatibility.
|
||||
#
|
||||
# The ISO is placed in 86Box's data directory so it can be mounted
|
||||
# as a CD-ROM drive.
|
||||
# The ISO is placed in the emulator's data directory so it can be
|
||||
# mounted as a CD-ROM drive.
|
||||
|
||||
set -e
|
||||
|
||||
|
|
@ -21,7 +44,7 @@ echo "Building DVX..."
|
|||
make -C "$SCRIPT_DIR" all
|
||||
|
||||
# Verify core build output exists
|
||||
for f in dvx.exe libs/libtasks.lib libs/libdvx.lib libs/dvxshell.lib; do
|
||||
for f in dvx.exe libs/kpunch/libtasks/libtasks.lib libs/kpunch/libdvx/libdvx.lib libs/kpunch/dvxshell/dvxshell.lib; do
|
||||
if [ ! -f "$SCRIPT_DIR/bin/$f" ]; then
|
||||
echo "ERROR: bin/$f not found -- build failed?"
|
||||
exit 1
|
||||
|
|
@ -30,21 +53,24 @@ done
|
|||
|
||||
# Verify widget DXEs exist
|
||||
WGT_COUNT=0
|
||||
for f in "$SCRIPT_DIR"/bin/widgets/*.wgt; do
|
||||
[ -f "$f" ] && WGT_COUNT=$((WGT_COUNT + 1))
|
||||
done
|
||||
while IFS= read -r f; do
|
||||
WGT_COUNT=$((WGT_COUNT + 1))
|
||||
done < <(find "$SCRIPT_DIR/bin/widgets/" -name "*.wgt" -type f 2>/dev/null)
|
||||
echo "$WGT_COUNT widget modules found in bin/widgets/."
|
||||
|
||||
# Clean emulator local-filesystem artifacts
|
||||
find "$SCRIPT_DIR/bin/" -name ".DBLOCALFILE*" -delete 2>/dev/null
|
||||
|
||||
# Create the ISO image
|
||||
# -iso-level 1: strict 8.3 filenames (DOS compatibility)
|
||||
# -J: Joliet extensions (long names for Windows/Linux)
|
||||
# -R: Rock Ridge (long names for Linux)
|
||||
# -V: volume label
|
||||
# Note: -R (Rock Ridge) is omitted because DOS surfaces its
|
||||
# attribute sidecar files (._ATR_) as visible junk files.
|
||||
echo "Creating ISO image..."
|
||||
mkisofs \
|
||||
-iso-level 1 \
|
||||
-J \
|
||||
-R \
|
||||
-V "DVX" \
|
||||
-o "$ISO_PATH" \
|
||||
"$SCRIPT_DIR/bin/"
|
||||
|
|
@ -52,5 +78,5 @@ mkisofs \
|
|||
echo "ISO created: $ISO_PATH"
|
||||
echo "Size: $(du -h "$ISO_PATH" | cut -f1)"
|
||||
echo ""
|
||||
echo "In 86Box, mount $ISO_PATH as a CD-ROM drive."
|
||||
echo "Mount $ISO_PATH as a CD-ROM drive in the target emulator."
|
||||
echo "Then from DOS: D:\\DVX.EXE (or whatever drive letter)"
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# Packet Serial Transport Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
|
||||
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
||||
|
||||
OBJDIR = ../obj/packet
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = packet.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/libpacket.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/packet.o: packet.c packet.h ../rs232/rs232.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
428
packet/README.md
428
packet/README.md
|
|
@ -1,428 +0,0 @@
|
|||
# Packet -- Reliable Serial Transport with HDLC Framing
|
||||
|
||||
Packetized serial transport providing reliable, ordered delivery over an
|
||||
unreliable serial link. Uses HDLC-style byte-stuffed framing, CRC-16-CCITT
|
||||
error detection, and a Go-Back-N sliding window protocol for automatic
|
||||
retransmission.
|
||||
|
||||
This layer sits on top of an already-open rs232 COM port. It does not
|
||||
open or close the serial port itself.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
| pktSend() queue a packet for reliable delivery
|
||||
| pktPoll() receive, process ACKs/NAKs, check retransmit timers
|
||||
|
|
||||
[Packet Layer] framing, CRC, sequencing, sliding window ARQ
|
||||
|
|
||||
[rs232] raw byte I/O via ISR-driven ring buffers
|
||||
|
|
||||
UART
|
||||
```
|
||||
|
||||
The packet layer adds framing, error detection, and reliability to the
|
||||
raw byte stream provided by rs232. The caller provides a receive callback
|
||||
that is invoked synchronously from `pktPoll()` for each complete, CRC-verified,
|
||||
in-order data packet.
|
||||
|
||||
|
||||
## Frame Format
|
||||
|
||||
Before byte stuffing, each frame has the following layout:
|
||||
|
||||
```
|
||||
[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
|
||||
```
|
||||
|
||||
| Field | Size | Description |
|
||||
|-----------|-----------|----------------------------------------------|
|
||||
| `0x7E` | 1 byte | Flag byte -- frame delimiter |
|
||||
| `SEQ` | 1 byte | Sequence number (wrapping uint8_t, 0-255) |
|
||||
| `TYPE` | 1 byte | Frame type (DATA, ACK, NAK, RST) |
|
||||
| `LEN` | 1 byte | Payload length (0-255) |
|
||||
| Payload | 0-255 | Application data |
|
||||
| `CRC` | 2 bytes | CRC-16-CCITT, little-endian, over SEQ..payload |
|
||||
|
||||
The header is 3 bytes (SEQ + TYPE + LEN), the CRC is 2 bytes, so the
|
||||
minimum frame size (no payload) is 5 bytes. The maximum frame size (255-byte
|
||||
payload) is 260 bytes before byte stuffing.
|
||||
|
||||
### Frame Types
|
||||
|
||||
| Type | Value | Description |
|
||||
|--------|-------|----------------------------------------------------|
|
||||
| `DATA` | 0x00 | Carries application payload |
|
||||
| `ACK` | 0x01 | Cumulative acknowledgment -- next expected sequence |
|
||||
| `NAK` | 0x02 | Negative ack -- request retransmit from this seq |
|
||||
| `RST` | 0x03 | Connection reset -- clear all state |
|
||||
|
||||
### Byte Stuffing
|
||||
|
||||
HDLC transparency encoding ensures the flag byte (0x7E) and escape byte
|
||||
(0x7D) never appear in the frame body. Within the frame data (everything
|
||||
between flags), these two bytes are escaped by prefixing with 0x7D and
|
||||
XORing the original byte with 0x20:
|
||||
|
||||
- `0x7E` becomes `0x7D 0x5E`
|
||||
- `0x7D` becomes `0x7D 0x5D`
|
||||
|
||||
In the worst case, every byte in the frame is escaped, doubling the wire
|
||||
size. In practice, these byte values are uncommon in typical data and the
|
||||
overhead is minimal.
|
||||
|
||||
### Why HDLC Framing
|
||||
|
||||
HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit
|
||||
variable-length frames on a raw byte stream. The 0x7E flag byte
|
||||
unambiguously marks frame boundaries. This is proven, lightweight, and
|
||||
requires zero buffering at the framing layer.
|
||||
|
||||
The alternative -- length-prefixed framing -- is fragile on noisy links
|
||||
because a corrupted length field permanently desynchronizes the receiver.
|
||||
With HDLC framing, the receiver can always resynchronize by hunting for
|
||||
the next flag byte.
|
||||
|
||||
|
||||
## CRC-16-CCITT
|
||||
|
||||
Error detection uses CRC-16-CCITT (polynomial 0x1021, initial value
|
||||
0xFFFF). The CRC covers the SEQ, TYPE, LEN, and payload fields. It is
|
||||
stored little-endian in the frame (CRC low byte first, then CRC high byte).
|
||||
|
||||
The CRC is computed via a 256-entry lookup table (512 bytes of `.rodata`).
|
||||
Table-driven CRC is approximately 10x faster than bit-by-bit computation
|
||||
on a 486 -- a worthwhile trade for a function called on every frame
|
||||
transmitted and received.
|
||||
|
||||
|
||||
## Go-Back-N Sliding Window Protocol
|
||||
|
||||
### Why Go-Back-N
|
||||
|
||||
Go-Back-N ARQ is simpler than Selective Repeat -- the receiver does not
|
||||
need an out-of-order reassembly buffer and only tracks a single expected
|
||||
sequence number. This works well for the low bandwidth-delay product of a
|
||||
serial link. On a 115200 bps local connection, the round-trip time is
|
||||
negligible, so the window rarely fills.
|
||||
|
||||
Go-Back-N's retransmit-all-from-NAK behavior wastes bandwidth on lossy
|
||||
links, but serial links are nearly lossless. The CRC check is primarily a
|
||||
safety net for electrical noise, not a routine error recovery mechanism.
|
||||
|
||||
### Protocol Details
|
||||
|
||||
The sliding window is configurable from 1 to 8 slots (default 4).
|
||||
Sequence numbers are 8-bit unsigned integers that wrap naturally at 256.
|
||||
The sequence space (256) is much larger than 2x the maximum window (16),
|
||||
so there is no ambiguity between old and new frames.
|
||||
|
||||
**Sender behavior:**
|
||||
- Assigns a monotonically increasing sequence number to each DATA frame
|
||||
- Retains a copy of each sent frame in a retransmit slot until it is
|
||||
acknowledged
|
||||
- When the window is full (`txCount >= windowSize`), blocks or returns
|
||||
`PKT_ERR_TX_FULL` depending on the `block` parameter
|
||||
|
||||
**Receiver behavior:**
|
||||
- Accepts frames strictly in order (`seq == rxExpectSeq`)
|
||||
- On in-order delivery, increments `rxExpectSeq` and sends an ACK
|
||||
carrying the new expected sequence number (cumulative acknowledgment)
|
||||
- Out-of-order frames within the window trigger a NAK for the expected
|
||||
sequence number
|
||||
- Duplicate and out-of-window frames are silently discarded
|
||||
|
||||
**ACK processing:**
|
||||
- ACKs carry the next expected sequence number (cumulative)
|
||||
- On receiving an ACK, the sender frees all retransmit slots with
|
||||
sequence numbers less than the ACK's sequence number
|
||||
|
||||
**NAK processing:**
|
||||
- A NAK requests retransmission from a specific sequence number
|
||||
- The sender retransmits that frame AND all subsequent unacknowledged
|
||||
frames (the Go-Back-N property)
|
||||
- Each retransmitted slot has its timer reset
|
||||
|
||||
**RST processing:**
|
||||
- Resets all sequence numbers and buffers to zero on both sides
|
||||
- The remote side also sends a RST in response
|
||||
|
||||
### Timer-Based Retransmission
|
||||
|
||||
Each retransmit slot tracks the time it was last (re)transmitted. If
|
||||
500ms elapses without an ACK, the slot is retransmitted and the timer
|
||||
is reset. This handles the case where an ACK or NAK was lost on the
|
||||
wire -- without this safety net, the connection would stall permanently.
|
||||
|
||||
The 500ms timeout is conservative for a local serial link (RTT is under
|
||||
1ms) but accounts for the remote side being busy processing. On BBS
|
||||
connections through the Linux proxy, the round-trip includes TCP latency,
|
||||
making the generous timeout appropriate.
|
||||
|
||||
### Receive State Machine
|
||||
|
||||
Incoming bytes from the serial port are fed through a three-state HDLC
|
||||
deframing state machine:
|
||||
|
||||
| State | Description |
|
||||
|----------|------------------------------------------------------|
|
||||
| `HUNT` | Discarding bytes until a flag (0x7E) is seen |
|
||||
| `ACTIVE` | Accumulating frame bytes; flag ends frame, ESC escapes |
|
||||
| `ESCAPE` | Previous byte was 0x7D; XOR this byte with 0x20 |
|
||||
|
||||
The flag byte serves double duty: it ends the current frame AND starts
|
||||
the next one. Back-to-back frames share a single flag byte, saving
|
||||
bandwidth. A frame is only processed if it meets the minimum size
|
||||
requirement (5 bytes), so spurious flags produce harmless zero-length
|
||||
"frames" that are discarded.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
// Receive callback -- called for each verified, in-order data packet
|
||||
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);
|
||||
|
||||
// Opaque connection handle
|
||||
typedef struct PktConnS PktConnT;
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|------------------------|-------|--------------------------------------|
|
||||
| `PKT_MAX_PAYLOAD` | 255 | Maximum payload bytes per packet |
|
||||
| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size |
|
||||
| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size |
|
||||
| `PKT_SUCCESS` | 0 | Success |
|
||||
| `PKT_ERR_INVALID_PORT` | -1 | Invalid COM port |
|
||||
| `PKT_ERR_NOT_OPEN` | -2 | Connection not open |
|
||||
| `PKT_ERR_ALREADY_OPEN` | -3 | Connection already open |
|
||||
| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block |
|
||||
| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow |
|
||||
| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter |
|
||||
| `PKT_ERR_TX_FULL` | -7 | Transmit window full (non-blocking) |
|
||||
| `PKT_ERR_NO_DATA` | -8 | No data available |
|
||||
| `PKT_ERR_DISCONNECTED` | -9 | Serial port disconnected or error |
|
||||
|
||||
### Functions
|
||||
|
||||
#### pktOpen
|
||||
|
||||
```c
|
||||
PktConnT *pktOpen(int com, int windowSize,
|
||||
PktRecvCallbackT callback, void *callbackCtx);
|
||||
```
|
||||
|
||||
Creates a packetized connection over an already-open COM port.
|
||||
|
||||
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
|
||||
- `windowSize` -- sliding window size (1-8), or 0 for the default (4)
|
||||
- `callback` -- called from `pktPoll()` for each received, verified,
|
||||
in-order data packet. The `data` pointer is valid only during the
|
||||
callback.
|
||||
- `callbackCtx` -- user pointer passed through to the callback
|
||||
|
||||
Returns a connection handle, or `NULL` on failure (allocation error).
|
||||
|
||||
#### pktClose
|
||||
|
||||
```c
|
||||
void pktClose(PktConnT *conn);
|
||||
```
|
||||
|
||||
Frees the connection state. Does **not** close the underlying COM port.
|
||||
The caller is responsible for calling `rs232Close()` separately.
|
||||
|
||||
#### pktSend
|
||||
|
||||
```c
|
||||
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
|
||||
```
|
||||
|
||||
Sends a data packet. `len` must be in the range 1 to `PKT_MAX_PAYLOAD`
|
||||
(255). The data is copied into a retransmit slot before transmission, so
|
||||
the caller can reuse its buffer immediately.
|
||||
|
||||
- `block = true` -- If the transmit window is full, polls internally
|
||||
(calling `pktPoll()` in a tight loop) until an ACK frees a slot. Returns
|
||||
`PKT_ERR_DISCONNECTED` if the serial port drops during the wait.
|
||||
- `block = false` -- Returns `PKT_ERR_TX_FULL` immediately if the window
|
||||
is full.
|
||||
|
||||
Returns `PKT_SUCCESS` on success.
|
||||
|
||||
#### pktPoll
|
||||
|
||||
```c
|
||||
int pktPoll(PktConnT *conn);
|
||||
```
|
||||
|
||||
The main work function. Must be called frequently (every iteration of
|
||||
your main loop or event loop). It performs three tasks:
|
||||
|
||||
1. **Drain serial RX** -- reads all available bytes from the rs232 port
|
||||
and feeds them through the HDLC deframing state machine
|
||||
2. **Process frames** -- verifies CRC, handles DATA/ACK/NAK/RST frames,
|
||||
delivers data packets to the callback
|
||||
3. **Check retransmit timers** -- resends any slots that have timed out
|
||||
|
||||
Returns the number of DATA packets delivered to the callback this call,
|
||||
or `PKT_ERR_DISCONNECTED` if the serial port returned an error, or
|
||||
`PKT_ERR_INVALID_PARAM` if `conn` is NULL.
|
||||
|
||||
The callback is invoked synchronously, so the caller should be prepared
|
||||
for re-entrant calls to `pktSend()` from within the callback.
|
||||
|
||||
#### pktReset
|
||||
|
||||
```c
|
||||
int pktReset(PktConnT *conn);
|
||||
```
|
||||
|
||||
Resets all sequence numbers, TX slots, and RX state to zero. Sends a RST
|
||||
frame to the remote side so it resets as well. Useful for recovering from
|
||||
a desynchronized state.
|
||||
|
||||
#### pktCanSend
|
||||
|
||||
```c
|
||||
bool pktCanSend(PktConnT *conn);
|
||||
```
|
||||
|
||||
Returns `true` if there is room in the transmit window for another
|
||||
packet. Useful for non-blocking send loops to avoid calling `pktSend()`
|
||||
when it would return `PKT_ERR_TX_FULL`.
|
||||
|
||||
#### pktGetPending
|
||||
|
||||
```c
|
||||
int pktGetPending(PktConnT *conn);
|
||||
```
|
||||
|
||||
Returns the number of unacknowledged packets currently in the transmit
|
||||
window. Ranges from 0 (all sent packets acknowledged) to `windowSize`
|
||||
(window full). Useful for throttling sends and monitoring link health.
|
||||
|
||||
|
||||
## Usage Example
|
||||
|
||||
```c
|
||||
#include "packet.h"
|
||||
#include "../rs232/rs232.h"
|
||||
|
||||
void onPacket(void *ctx, const uint8_t *data, int len) {
|
||||
// process received packet -- data is valid only during this callback
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Open serial port first (packet layer does not manage it)
|
||||
rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
|
||||
|
||||
// Create packet connection with default window size (4)
|
||||
PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL);
|
||||
|
||||
// Send a packet (blocking -- waits for window space if needed)
|
||||
uint8_t msg[] = "Hello, packets!";
|
||||
pktSend(conn, msg, sizeof(msg), true);
|
||||
|
||||
// Main loop -- must call pktPoll() frequently
|
||||
while (1) {
|
||||
int delivered = pktPoll(conn);
|
||||
if (delivered == PKT_ERR_DISCONNECTED) {
|
||||
break;
|
||||
}
|
||||
// delivered = number of packets received this iteration
|
||||
}
|
||||
|
||||
pktClose(conn);
|
||||
rs232Close(RS232_COM1);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Non-Blocking Send Pattern
|
||||
|
||||
```c
|
||||
// Send as fast as the window allows, doing other work between sends
|
||||
while (bytesLeft > 0) {
|
||||
pktPoll(conn); // process ACKs, free window slots
|
||||
|
||||
if (pktCanSend(conn)) {
|
||||
int chunk = bytesLeft;
|
||||
if (chunk > PKT_MAX_PAYLOAD) {
|
||||
chunk = PKT_MAX_PAYLOAD;
|
||||
}
|
||||
|
||||
if (pktSend(conn, data + offset, chunk, false) == PKT_SUCCESS) {
|
||||
offset += chunk;
|
||||
bytesLeft -= chunk;
|
||||
}
|
||||
}
|
||||
|
||||
// do other work here (update UI, check for cancel, etc.)
|
||||
}
|
||||
|
||||
// Drain remaining ACKs
|
||||
while (pktGetPending(conn) > 0) {
|
||||
pktPoll(conn);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Internal Data Structures
|
||||
|
||||
### Connection State (PktConnT)
|
||||
|
||||
The connection handle contains:
|
||||
|
||||
- **COM port index** and **window size** (configuration)
|
||||
- **Callback** function pointer and context
|
||||
- **TX state**: next sequence to assign, oldest unacked sequence, array
|
||||
of retransmit slots, count of slots in use
|
||||
- **RX state**: next expected sequence, deframing state machine state,
|
||||
frame accumulation buffer
|
||||
|
||||
### Retransmit Slots (TxSlotT)
|
||||
|
||||
Each slot holds a copy of the sent payload, its sequence number, payload
|
||||
length, and a `clock_t` timestamp of when it was last transmitted. The
|
||||
retransmit timer compares this timestamp against the current time to
|
||||
detect timeout.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/libpacket.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Objects are placed in `../obj/packet/`, the library in `../lib/`.
|
||||
|
||||
Requires `librs232.a` at link time (for `rs232Read()` and `rs232Write()`).
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `packet.h` -- Public API header (types, constants, function prototypes)
|
||||
- `packet.c` -- Complete implementation (framing, CRC, ARQ, state machine)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `rs232/` -- Serial port I/O (must be linked: `-lrs232`)
|
||||
|
||||
## Used By
|
||||
|
||||
- `seclink/` -- Secure serial link (adds channel multiplexing and encryption)
|
||||
- `proxy/` -- Linux serial proxy (uses a socket-based adaptation)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// Stub for DJGPP <go32.h> -- Linux proxy build
|
||||
#ifndef GO32_H_STUB
|
||||
#define GO32_H_STUB
|
||||
|
||||
#define _dos_ds 0
|
||||
|
||||
#endif
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Stub for DJGPP <pc.h> -- Linux proxy build
|
||||
#ifndef PC_H_STUB
|
||||
#define PC_H_STUB
|
||||
|
||||
static inline void outportb(unsigned short port, unsigned char val) { (void)port; (void)val; }
|
||||
static inline unsigned char inportb(unsigned short port) { (void)port; return 0; }
|
||||
|
||||
#endif
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// Stub for DJGPP <sys/farptr.h> -- Linux proxy build
|
||||
#ifndef FARPTR_H_STUB
|
||||
#define FARPTR_H_STUB
|
||||
|
||||
static inline unsigned long _farpeekl(unsigned short sel, unsigned long ofs) { (void)sel; (void)ofs; return 0; }
|
||||
|
||||
#endif
|
||||
BIN
releases/dvx-a3.zip
(Stored with Git LFS)
Normal file
BIN
releases/dvx-a3.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -1,38 +0,0 @@
|
|||
# RS-232 Serial Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
|
||||
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
||||
|
||||
OBJDIR = ../obj/rs232
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = rs232.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/librs232.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/rs232.o: rs232.c rs232.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
417
rs232/README.md
417
rs232/README.md
|
|
@ -1,417 +0,0 @@
|
|||
# RS232 -- ISR-Driven Serial Port Library for DJGPP
|
||||
|
||||
Interrupt-driven UART communication library supporting up to 4 simultaneous
|
||||
COM ports with ring buffers and hardware/software flow control. Targets
|
||||
486-class DOS hardware running under DJGPP/DPMI.
|
||||
|
||||
Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License),
|
||||
stripped to DJGPP-only codepaths and restyled for the DVX project.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
The library is built around a single shared ISR (`comGeneralIsr`) that
|
||||
services all open COM ports. This design is necessary because COM1/COM3
|
||||
typically share IRQ4 and COM2/COM4 share IRQ3 -- a single handler that
|
||||
polls all ports avoids the complexity of per-IRQ dispatch.
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
| rs232Read() non-blocking drain from RX ring buffer
|
||||
| rs232Write() blocking polled write directly to UART THR
|
||||
| rs232WriteBuf() non-blocking write into TX ring buffer
|
||||
|
|
||||
[Ring Buffers] 2048-byte RX + TX per port, power-of-2 bitmask indexing
|
||||
|
|
||||
[ISR] comGeneralIsr -- shared handler for all open ports
|
||||
|
|
||||
[UART] 8250 / 16450 / 16550 / 16550A hardware
|
||||
```
|
||||
|
||||
### ISR Design
|
||||
|
||||
The ISR follows a careful protocol to remain safe under DPMI while
|
||||
keeping the system responsive:
|
||||
|
||||
1. **Mask** all COM port IRQs on the PIC to prevent ISR re-entry
|
||||
2. **STI** to allow higher-priority interrupts (timer tick, keyboard) through
|
||||
3. **Loop** over all open ports, draining each UART's pending interrupt
|
||||
conditions (data ready, TX hold empty, modem status, line status)
|
||||
4. **CLI**, send EOI to the PIC, re-enable COM IRQs, **STI** before IRET
|
||||
|
||||
This mask-then-STI pattern is standard for slow device ISRs on PC
|
||||
hardware. It prevents the same IRQ from re-entering while allowing the
|
||||
system timer and keyboard to function during UART processing.
|
||||
|
||||
### Ring Buffers
|
||||
|
||||
Both RX and TX buffers are 2048 bytes, sized as a power of 2 so that
|
||||
head/tail wraparound is a single AND operation (bitmask indexing) rather
|
||||
than an expensive modulo -- critical for ISR-speed code on a 486.
|
||||
|
||||
The buffers use a one-slot-wasted design to distinguish full from empty:
|
||||
`head == tail` means empty, `(head + 1) & MASK == tail` means full.
|
||||
|
||||
### Flow Control
|
||||
|
||||
Flow control operates entirely within the ISR using watermark thresholds.
|
||||
When the RX buffer crosses 80% full, the ISR signals the remote side to
|
||||
stop sending; when it drops below 20%, the ISR allows the remote to
|
||||
resume. This prevents buffer overflow without any application involvement.
|
||||
|
||||
Three modes are supported:
|
||||
|
||||
| Mode | Stop Signal | Resume Signal |
|
||||
|------------|-------------------|-------------------|
|
||||
| XON/XOFF | Send XOFF (0x13) | Send XON (0x11) |
|
||||
| RTS/CTS | Deassert RTS | Assert RTS |
|
||||
| DTR/DSR | Deassert DTR | Assert DTR |
|
||||
|
||||
On the TX side, the ISR monitors incoming XON/XOFF bytes and the CTS/DSR
|
||||
modem status lines to pause and resume transmission from the TX ring
|
||||
buffer.
|
||||
|
||||
### DPMI Memory Locking
|
||||
|
||||
The ISR code and all per-port state structures (`sComPorts` array) are
|
||||
locked in physical memory via `__dpmi_lock_linear_region`. This prevents
|
||||
page faults during interrupt handling -- a hard requirement for any ISR
|
||||
running under a DPMI host (DOS extender, Windows 3.x, OS/2 VDM, etc.).
|
||||
An IRET wrapper is allocated by DPMI to handle the real-mode to
|
||||
protected-mode transition on hardware interrupt entry.
|
||||
|
||||
|
||||
## UART Type Detection
|
||||
|
||||
`rs232GetUartType()` probes the UART hardware to identify the chip:
|
||||
|
||||
1. **Scratch register test** -- Writes two known values (0xAA, 0x55) to
|
||||
UART register 7 and reads them back. The 8250 lacks this register, so
|
||||
readback fails. If both values read back correctly, the UART is at
|
||||
least a 16450.
|
||||
|
||||
2. **FIFO test** -- Enables the FIFO via the FCR (FIFO Control Register),
|
||||
then reads bits 7:6 of the IIR (Interrupt Identification Register):
|
||||
- `0b11` = 16550A (working 16-byte FIFO)
|
||||
- `0b10` = 16550 (broken FIFO -- present in hardware but unusable)
|
||||
- `0b00` = 16450 (no FIFO at all)
|
||||
|
||||
The original FCR value is restored after probing.
|
||||
|
||||
| Constant | Value | Description |
|
||||
|---------------------|-------|----------------------------------------|
|
||||
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
|
||||
| `RS232_UART_8250` | 1 | Original IBM PC -- no FIFO, no scratch |
|
||||
| `RS232_UART_16450` | 2 | Scratch register present, no FIFO |
|
||||
| `RS232_UART_16550` | 3 | Broken FIFO (rare, unusable) |
|
||||
| `RS232_UART_16550A` | 4 | Working 16-byte FIFO (most common) |
|
||||
|
||||
On 16550A UARTs, the FIFO trigger threshold is configurable via
|
||||
`rs232SetFifoThreshold()` with levels of 1, 4, 8, or 14 bytes. The
|
||||
default is 14, which minimizes interrupt overhead at high baud rates.
|
||||
|
||||
|
||||
## IRQ Auto-Detection
|
||||
|
||||
When `rs232Open()` is called without a prior `rs232SetIrq()` override,
|
||||
the library auto-detects the UART's IRQ by:
|
||||
|
||||
1. Saving the current PIC interrupt mask registers (IMR)
|
||||
2. Enabling all IRQ lines on both PICs
|
||||
3. Generating a TX Hold Empty interrupt on the UART
|
||||
4. Reading the PIC's Interrupt Request Register (IRR) to see which line
|
||||
went high
|
||||
5. Disabling the interrupt, reading IRR again to mask out persistent bits
|
||||
6. Re-enabling once more to confirm the detection
|
||||
7. Restoring the original PIC mask
|
||||
|
||||
If auto-detection fails (common on virtualized hardware that does not
|
||||
model the IRR accurately), the library falls back to the default IRQ for
|
||||
the port (IRQ4 for COM1/COM3, IRQ3 for COM2/COM4).
|
||||
|
||||
The BIOS Data Area (at segment 0x0040) is read to determine each port's
|
||||
I/O base address. Ports not configured in the BDA are unavailable.
|
||||
|
||||
|
||||
## COM Port Support
|
||||
|
||||
| Constant | Value | Default IRQ | Default Base |
|
||||
|--------------|-------|-------------|--------------|
|
||||
| `RS232_COM1` | 0 | IRQ 4 | 0x3F8 |
|
||||
| `RS232_COM2` | 1 | IRQ 3 | 0x2F8 |
|
||||
| `RS232_COM3` | 2 | IRQ 4 | 0x3E8 |
|
||||
| `RS232_COM4` | 3 | IRQ 3 | 0x2E8 |
|
||||
|
||||
Base addresses are read from the BIOS Data Area at runtime. The default
|
||||
IRQ values are used only as a fallback when auto-detection fails. Both
|
||||
the base address and IRQ can be overridden before opening with
|
||||
`rs232SetBase()` and `rs232SetIrq()`.
|
||||
|
||||
|
||||
## Supported Baud Rates
|
||||
|
||||
All standard rates from 50 to 115200 bps are supported. The baud rate
|
||||
divisor is computed from the standard 1.8432 MHz UART crystal:
|
||||
|
||||
| Rate | Divisor | Rate | Divisor |
|
||||
|--------|---------|--------|---------|
|
||||
| 50 | 2304 | 4800 | 24 |
|
||||
| 75 | 1536 | 7200 | 16 |
|
||||
| 110 | 1047 | 9600 | 12 |
|
||||
| 150 | 768 | 19200 | 6 |
|
||||
| 300 | 384 | 38400 | 3 |
|
||||
| 600 | 192 | 57600 | 2 |
|
||||
| 1200 | 96 | 115200 | 1 |
|
||||
| 1800 | 64 | | |
|
||||
| 2400 | 48 | | |
|
||||
| 3800 | 32 | | |
|
||||
|
||||
Data bits (5-8), parity (N/O/E/M/S), and stop bits (1-2) are configured
|
||||
by writing the appropriate LCR (Line Control Register) bits.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Constant | Value | Description |
|
||||
|-------------------------------|-------|--------------------------|
|
||||
| `RS232_SUCCESS` | 0 | Success |
|
||||
| `RS232_ERR_UNKNOWN` | -1 | Unknown error |
|
||||
| `RS232_ERR_NOT_OPEN` | -2 | Port not open |
|
||||
| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open |
|
||||
| `RS232_ERR_NO_UART` | -4 | No UART detected at base |
|
||||
| `RS232_ERR_INVALID_PORT` | -5 | Bad port index |
|
||||
| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address |
|
||||
| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number |
|
||||
| `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate |
|
||||
| `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) |
|
||||
| `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character |
|
||||
| `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) |
|
||||
| `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode |
|
||||
| `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold |
|
||||
| `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument |
|
||||
| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ |
|
||||
| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed |
|
||||
|
||||
### Handshaking Modes
|
||||
|
||||
| Constant | Value | Description |
|
||||
|---------------------------|-------|----------------------|
|
||||
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
|
||||
| `RS232_HANDSHAKE_XONXOFF` | 1 | Software (XON/XOFF) |
|
||||
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
|
||||
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
|
||||
|
||||
### Open / Close
|
||||
|
||||
```c
|
||||
int rs232Open(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake);
|
||||
```
|
||||
|
||||
Opens a COM port. Reads the UART base address from the BIOS data area,
|
||||
auto-detects the IRQ, locks ISR memory via DPMI, installs the ISR, and
|
||||
configures the UART for the specified parameters. Returns `RS232_SUCCESS`
|
||||
or an error code.
|
||||
|
||||
- `com` -- port index (`RS232_COM1` through `RS232_COM4`)
|
||||
- `bps` -- baud rate (50 through 115200)
|
||||
- `dataBits` -- 5, 6, 7, or 8
|
||||
- `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark),
|
||||
`'S'` (space)
|
||||
- `stopBits` -- 1 or 2
|
||||
- `handshake` -- `RS232_HANDSHAKE_*` constant
|
||||
|
||||
```c
|
||||
int rs232Close(int com);
|
||||
```
|
||||
|
||||
Closes the port, disables UART interrupts, removes the ISR, restores the
|
||||
original interrupt vector, and unlocks DPMI memory (when the last port
|
||||
closes).
|
||||
|
||||
### Read / Write
|
||||
|
||||
```c
|
||||
int rs232Read(int com, char *data, int len);
|
||||
```
|
||||
|
||||
Non-blocking read. Drains up to `len` bytes from the RX ring buffer.
|
||||
Returns the number of bytes actually read (0 if the buffer is empty).
|
||||
If flow control is active and the buffer drops below the low-water mark,
|
||||
the ISR will re-enable receive from the remote side.
|
||||
|
||||
```c
|
||||
int rs232Write(int com, const char *data, int len);
|
||||
```
|
||||
|
||||
Blocking polled write. Sends `len` bytes directly to the UART THR
|
||||
(Transmit Holding Register), bypassing the TX ring buffer entirely.
|
||||
Polls LSR for THR empty before each byte. Returns `RS232_SUCCESS` or
|
||||
an error code.
|
||||
|
||||
```c
|
||||
int rs232WriteBuf(int com, const char *data, int len);
|
||||
```
|
||||
|
||||
Non-blocking buffered write. Copies as many bytes as will fit into the
|
||||
TX ring buffer. The ISR drains the TX buffer to the UART automatically.
|
||||
Returns the number of bytes actually queued. If the buffer is full, some
|
||||
bytes may be dropped.
|
||||
|
||||
### Buffer Management
|
||||
|
||||
```c
|
||||
int rs232ClearRxBuffer(int com);
|
||||
int rs232ClearTxBuffer(int com);
|
||||
```
|
||||
|
||||
Discard all data in the receive or transmit ring buffer by resetting
|
||||
head and tail pointers to zero.
|
||||
|
||||
### Getters
|
||||
|
||||
```c
|
||||
int rs232GetBase(int com); // UART I/O base address
|
||||
int32_t rs232GetBps(int com); // Current baud rate
|
||||
int rs232GetCts(int com); // CTS line state (0 or 1)
|
||||
int rs232GetData(int com); // Data bits setting
|
||||
int rs232GetDsr(int com); // DSR line state (0 or 1)
|
||||
int rs232GetDtr(int com); // DTR line state (0 or 1)
|
||||
int rs232GetHandshake(int com); // Handshaking mode
|
||||
int rs232GetIrq(int com); // IRQ number
|
||||
int rs232GetLsr(int com); // Line Status Register
|
||||
int rs232GetMcr(int com); // Modem Control Register
|
||||
int rs232GetMsr(int com); // Modem Status Register
|
||||
char rs232GetParity(int com); // Parity setting ('N','O','E','M','S')
|
||||
int rs232GetRts(int com); // RTS line state (0 or 1)
|
||||
int rs232GetRxBuffered(int com); // Bytes waiting in RX buffer
|
||||
int rs232GetStop(int com); // Stop bits setting
|
||||
int rs232GetTxBuffered(int com); // Bytes waiting in TX buffer
|
||||
int rs232GetUartType(int com); // UART type (RS232_UART_* constant)
|
||||
```
|
||||
|
||||
Most getters return cached register values from the per-port state
|
||||
structure, avoiding unnecessary I/O port reads. `rs232GetUartType()`
|
||||
actively probes the hardware (see UART Type Detection above).
|
||||
|
||||
### Setters
|
||||
|
||||
```c
|
||||
int rs232Set(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake);
|
||||
```
|
||||
|
||||
Reconfigure all port parameters at once. The port must already be open.
|
||||
|
||||
```c
|
||||
int rs232SetBase(int com, int base); // Override I/O base (before open)
|
||||
int rs232SetBps(int com, int32_t bps); // Change baud rate
|
||||
int rs232SetData(int com, int dataBits); // Change data bits
|
||||
int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR
|
||||
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger (1, 4, 8, 14)
|
||||
int rs232SetHandshake(int com, int handshake); // Change flow control mode
|
||||
int rs232SetIrq(int com, int irq); // Override IRQ (before open)
|
||||
int rs232SetMcr(int com, int mcr); // Write Modem Control Register
|
||||
int rs232SetParity(int com, char parity); // Change parity
|
||||
int rs232SetRts(int com, bool rts); // Assert/deassert RTS
|
||||
int rs232SetStop(int com, int stopBits); // Change stop bits
|
||||
```
|
||||
|
||||
|
||||
## Usage Example
|
||||
|
||||
```c
|
||||
#include "rs232.h"
|
||||
|
||||
int main(void) {
|
||||
// Open COM1 at 115200 8N1, no flow control
|
||||
int rc = rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
|
||||
if (rc != RS232_SUCCESS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Identify the UART chip
|
||||
int uartType = rs232GetUartType(RS232_COM1);
|
||||
// uartType == RS232_UART_16550A on most 486+ systems
|
||||
|
||||
// Enable 16550A FIFO with trigger at 14 bytes
|
||||
if (uartType == RS232_UART_16550A) {
|
||||
rs232SetFifoThreshold(RS232_COM1, 14);
|
||||
}
|
||||
|
||||
// Blocking send
|
||||
rs232Write(RS232_COM1, "Hello\r\n", 7);
|
||||
|
||||
// Non-blocking receive loop
|
||||
char buf[128];
|
||||
int n;
|
||||
while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) {
|
||||
// process buf[0..n-1]
|
||||
}
|
||||
|
||||
rs232Close(RS232_COM1);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The single shared ISR handles all four COM ports. On entry it disables
|
||||
UART interrupts for all open ports on the PIC, then re-enables CPU
|
||||
interrupts (STI) so higher-priority devices (timer, keyboard) are
|
||||
serviced promptly.
|
||||
|
||||
- Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing
|
||||
for zero-branch wraparound. Each port uses 4KB total (2KB RX + 2KB TX).
|
||||
|
||||
- Flow control watermarks are at 80% (assert stop) and 20% (deassert
|
||||
stop) of buffer capacity. These percentages are defined as compile-time
|
||||
constants and apply to both RX and TX directions.
|
||||
|
||||
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR code, ring
|
||||
buffers, and port state in physical memory. The ISR code region is
|
||||
locked for 2048 bytes starting at the `comGeneralIsr` function address.
|
||||
|
||||
- `rs232Write()` is a blocking polled write that bypasses the TX ring
|
||||
buffer entirely. It writes directly to the UART THR register, polling
|
||||
LSR for readiness between each byte. `rs232WriteBuf()` is the
|
||||
non-blocking alternative that queues into the TX ring buffer for ISR
|
||||
draining.
|
||||
|
||||
- Per-port state is stored in a static array of `Rs232StateT` structures
|
||||
(`sComPorts[4]`). This array is locked in physical memory alongside the
|
||||
ISR code.
|
||||
|
||||
- The BIOS Data Area (real-mode address 0040:0000) is read via DJGPP's
|
||||
far pointer API (`_farpeekw`) to obtain port base addresses at runtime.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/librs232.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Objects are placed in `../obj/rs232/`, the library in `../lib/`.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `rs232.h` -- Public API header
|
||||
- `rs232.c` -- Complete implementation (ISR, DPMI, ring buffers, UART I/O)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Used By
|
||||
|
||||
- `packet/` -- Packetized serial transport layer (HDLC framing, CRC, ARQ)
|
||||
- `seclink/` -- Secure serial link (opens and closes the COM port)
|
||||
- `proxy/` -- Linux serial proxy (uses a socket-based shim of this API)
|
||||
27
run.sh
27
run.sh
|
|
@ -1,2 +1,27 @@
|
|||
#!/bin/bash
|
||||
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x.conf
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
|
||||
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf
|
||||
#SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# SecLink Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
|
||||
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
||||
|
||||
OBJDIR = ../obj/seclink
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = secLink.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/libseclink.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/secLink.o: secLink.c secLink.h ../rs232/rs232.h ../packet/packet.h ../security/security.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
|
|
@ -1,450 +0,0 @@
|
|||
# SecLink -- Secure Serial Link Library
|
||||
|
||||
SecLink is the top-level API for the DVX serial/networking stack. It
|
||||
composes three lower-level libraries into a single interface for reliable,
|
||||
optionally encrypted, channel-multiplexed serial communication:
|
||||
|
||||
- **rs232** -- ISR-driven UART I/O with ring buffers and flow control
|
||||
- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ
|
||||
- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption
|
||||
|
||||
SecLink adds channel multiplexing and per-packet encryption control on
|
||||
top of the packet layer's reliable delivery.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
| secLinkSend() send data on a channel, optionally encrypted
|
||||
| secLinkPoll() receive, decrypt, deliver to callback
|
||||
| secLinkHandshake() DH key exchange (blocking)
|
||||
|
|
||||
[SecLink] channel header, encrypt/decrypt, key management
|
||||
|
|
||||
[Packet] HDLC framing, CRC-16, Go-Back-N ARQ
|
||||
|
|
||||
[RS232] ISR-driven UART, 2048-byte ring buffers
|
||||
|
|
||||
UART Hardware
|
||||
```
|
||||
|
||||
### Channel Multiplexing
|
||||
|
||||
SecLink prepends a one-byte header to every packet's payload before
|
||||
handing it to the packet layer:
|
||||
|
||||
```
|
||||
Bit 7 Bits 6..0
|
||||
----- ---------
|
||||
Encrypt Channel (0-127)
|
||||
```
|
||||
|
||||
This allows up to 128 independent logical channels over a single serial
|
||||
link. Each channel can carry a different type of traffic (terminal data,
|
||||
file transfer, control messages, etc.) without needing separate framing
|
||||
or sequencing per stream. The receive callback includes the channel
|
||||
number so the application can dispatch accordingly.
|
||||
|
||||
The encrypt flag (bit 7) tells the receiver whether the payload portion
|
||||
of this packet is encrypted. The channel header byte itself is always
|
||||
sent in the clear.
|
||||
|
||||
### Mixed Clear and Encrypted Traffic
|
||||
|
||||
Unencrypted packets can be sent before or after the DH handshake. This
|
||||
enables a startup protocol (version negotiation, capability exchange)
|
||||
before keys are established. Encrypted packets require a completed
|
||||
handshake -- attempting to send an encrypted packet before the handshake
|
||||
returns `SECLINK_ERR_NOT_READY`.
|
||||
|
||||
On the receive side, encrypted packets arriving before the handshake is
|
||||
complete are silently dropped. Cleartext packets are delivered regardless
|
||||
of handshake state.
|
||||
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
secLinkOpen() Open COM port and packet layer
|
||||
secLinkHandshake() DH key exchange (blocks until both sides complete)
|
||||
secLinkSend() Send data on a channel (encrypted or cleartext)
|
||||
secLinkPoll() Receive and deliver packets to callback
|
||||
secLinkClose() Tear down everything (ciphers, packet, COM port)
|
||||
```
|
||||
|
||||
### Handshake Protocol
|
||||
|
||||
The DH key exchange uses the packet layer's reliable delivery, so lost
|
||||
packets are automatically retransmitted. Both sides can send their public
|
||||
key simultaneously -- there is no initiator/responder distinction.
|
||||
|
||||
1. Both sides generate a DH keypair (256-bit private, 1024-bit public)
|
||||
2. Both sides send their 128-byte public key as a single packet
|
||||
3. On receiving the remote's public key, each side immediately computes
|
||||
the shared secret (`remote^private mod p`)
|
||||
4. Each side derives separate TX and RX cipher keys from the master key
|
||||
5. Cipher contexts are created and the link transitions to READY state
|
||||
6. The DH context (containing the private key) is destroyed immediately
|
||||
|
||||
**Directional key derivation:**
|
||||
|
||||
The side with the lexicographically lower public key uses
|
||||
`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other
|
||||
side uses the reverse assignment. This is critical for CTR mode security:
|
||||
if both sides used the same key and counter, they would produce identical
|
||||
keystreams, and XORing two ciphertexts would reveal the XOR of the
|
||||
plaintexts. The XOR-derived directional keys ensure each direction has a
|
||||
unique keystream even though both sides start their counters at zero.
|
||||
|
||||
**Forward secrecy:**
|
||||
|
||||
The DH context (containing the private key and shared secret) is
|
||||
destroyed immediately after deriving the session cipher keys. Even if
|
||||
the application's long-term state is compromised later, past session
|
||||
keys cannot be recovered from memory.
|
||||
|
||||
|
||||
## Payload Size
|
||||
|
||||
The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD`
|
||||
(254 bytes). This is the packet layer's 255-byte maximum minus the
|
||||
1-byte channel header that SecLink prepends.
|
||||
|
||||
For sending data larger than 254 bytes, use `secLinkSendBuf()` which
|
||||
automatically splits the data into 254-byte chunks and sends each one
|
||||
with blocking delivery.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
// Receive callback -- delivers plaintext with channel number
|
||||
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len,
|
||||
uint8_t channel);
|
||||
|
||||
// Opaque connection handle
|
||||
typedef struct SecLinkS SecLinkT;
|
||||
```
|
||||
|
||||
The receive callback is invoked from `secLinkPoll()` for each incoming
|
||||
packet. Encrypted packets are decrypted before delivery -- the callback
|
||||
always receives plaintext regardless of whether encryption was used on
|
||||
the wire. The `data` pointer is valid only during the callback.
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|-------------------------|-------|---------------------------------------|
|
||||
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
|
||||
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
|
||||
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
|
||||
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
|
||||
| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed |
|
||||
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
|
||||
| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed |
|
||||
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
|
||||
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full |
|
||||
|
||||
### Functions
|
||||
|
||||
#### secLinkOpen
|
||||
|
||||
```c
|
||||
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake,
|
||||
SecLinkRecvT callback, void *ctx);
|
||||
```
|
||||
|
||||
Opens the COM port via rs232, creates the packet layer with default
|
||||
window size (4), and returns a link handle. The callback is invoked from
|
||||
`secLinkPoll()` for each received packet (decrypted if applicable).
|
||||
|
||||
Returns `NULL` on failure (serial port error, packet layer allocation
|
||||
error, or memory allocation failure). On failure, all partially
|
||||
initialized resources are cleaned up.
|
||||
|
||||
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
|
||||
- `bps` -- baud rate (50 through 115200)
|
||||
- `dataBits` -- 5, 6, 7, or 8
|
||||
- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'`
|
||||
- `stopBits` -- 1 or 2
|
||||
- `handshake` -- `RS232_HANDSHAKE_*` constant
|
||||
- `callback` -- receive callback function
|
||||
- `ctx` -- user pointer passed through to the callback
|
||||
|
||||
#### secLinkClose
|
||||
|
||||
```c
|
||||
void secLinkClose(SecLinkT *link);
|
||||
```
|
||||
|
||||
Full teardown in order: destroys TX and RX cipher contexts (secure zero),
|
||||
destroys the DH context if still present, closes the packet layer, closes
|
||||
the COM port, zeroes the link structure, and frees memory.
|
||||
|
||||
#### secLinkHandshake
|
||||
|
||||
```c
|
||||
int secLinkHandshake(SecLinkT *link);
|
||||
```
|
||||
|
||||
Performs a Diffie-Hellman key exchange. Blocks until both sides have
|
||||
exchanged public keys and derived cipher keys. The RNG must be seeded
|
||||
(via `secRngSeed()` or `secRngAddEntropy()`) before calling this.
|
||||
|
||||
Internally:
|
||||
1. Creates a DH context and generates keys
|
||||
2. Sends the 128-byte public key via the packet layer (blocking)
|
||||
3. Polls the packet layer in a loop until the remote's public key arrives
|
||||
4. Computes the shared secret and derives directional cipher keys
|
||||
5. Destroys the DH context (forward secrecy)
|
||||
6. Transitions the link to READY state
|
||||
|
||||
Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure
|
||||
(DH key generation failure, send failure, or serial disconnect during
|
||||
the exchange).
|
||||
|
||||
#### secLinkSend
|
||||
|
||||
```c
|
||||
int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
|
||||
uint8_t channel, bool encrypt, bool block);
|
||||
```
|
||||
|
||||
Sends up to `SECLINK_MAX_PAYLOAD` (254) bytes on the given channel.
|
||||
|
||||
- `channel` -- logical channel number (0-127)
|
||||
- `encrypt` -- if `true`, encrypts the payload before sending. Requires
|
||||
a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise.
|
||||
- `block` -- if `true`, waits for transmit window space. If `false`,
|
||||
returns `SECLINK_ERR_SEND` when the packet layer's window is full.
|
||||
|
||||
**Cipher counter safety:** The function checks transmit window space
|
||||
BEFORE encrypting the payload. If it encrypted first and then the send
|
||||
failed, the cipher counter would advance without the data being sent,
|
||||
permanently desynchronizing the TX cipher state from the remote's RX
|
||||
cipher. This ordering is critical for correctness.
|
||||
|
||||
The channel header byte is prepended to the data, and only the payload
|
||||
portion (not the header) is encrypted.
|
||||
|
||||
Returns `SECLINK_SUCCESS` or an error code.
|
||||
|
||||
#### secLinkSendBuf
|
||||
|
||||
```c
|
||||
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len,
|
||||
uint8_t channel, bool encrypt);
|
||||
```
|
||||
|
||||
Sends an arbitrarily large buffer by splitting it into
|
||||
`SECLINK_MAX_PAYLOAD`-byte (254-byte) chunks. Always blocks until all
|
||||
data is sent. The receiver sees multiple packets on the same channel and
|
||||
must reassemble if needed.
|
||||
|
||||
Returns `SECLINK_SUCCESS` or the first error encountered.
|
||||
|
||||
#### secLinkPoll
|
||||
|
||||
```c
|
||||
int secLinkPoll(SecLinkT *link);
|
||||
```
|
||||
|
||||
Delegates to `pktPoll()` to read serial data, process frames, handle
|
||||
ACKs and retransmits. Received packets are routed through an internal
|
||||
callback that:
|
||||
|
||||
- During handshake: expects a 128-byte DH public key
|
||||
- When ready: strips the channel header, decrypts the payload if the
|
||||
encrypt flag is set, and forwards plaintext to the user callback
|
||||
|
||||
Returns the number of packets delivered, or a negative error code.
|
||||
|
||||
Must be called frequently (every iteration of your main loop).
|
||||
|
||||
#### secLinkGetPending
|
||||
|
||||
```c
|
||||
int secLinkGetPending(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns the number of unacknowledged packets in the transmit window.
|
||||
Delegates directly to `pktGetPending()`. Useful for non-blocking send
|
||||
loops to determine when there is room to send more data.
|
||||
|
||||
#### secLinkIsReady
|
||||
|
||||
```c
|
||||
bool secLinkIsReady(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns `true` if the DH handshake is complete and the link is ready for
|
||||
encrypted communication. Cleartext sends do not require the link to be
|
||||
ready.
|
||||
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Encrypted Link
|
||||
|
||||
```c
|
||||
#include "secLink.h"
|
||||
#include "../security/security.h"
|
||||
|
||||
void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {
|
||||
// handle received plaintext on 'channel'
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Seed the RNG before handshake
|
||||
uint8_t entropy[16];
|
||||
secRngGatherEntropy(entropy, sizeof(entropy));
|
||||
secRngSeed(entropy, sizeof(entropy));
|
||||
|
||||
// Open link on COM1 at 115200 8N1, no flow control
|
||||
SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1,
|
||||
RS232_HANDSHAKE_NONE, onRecv, NULL);
|
||||
if (!link) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// DH key exchange (blocks until both sides complete)
|
||||
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
|
||||
secLinkClose(link);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Send encrypted data on channel 0
|
||||
const char *msg = "Hello, secure world!";
|
||||
secLinkSend(link, (const uint8_t *)msg, strlen(msg), 0, true, true);
|
||||
|
||||
// Main loop
|
||||
while (1) {
|
||||
secLinkPoll(link);
|
||||
}
|
||||
|
||||
secLinkClose(link);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Mixed Encrypted and Cleartext Channels
|
||||
|
||||
```c
|
||||
#define CHAN_CONTROL 0 // cleartext control channel
|
||||
#define CHAN_DATA 1 // encrypted data channel
|
||||
|
||||
// Cleartext status message (no handshake needed)
|
||||
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
|
||||
|
||||
// Encrypted payload (requires completed handshake)
|
||||
secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true);
|
||||
```
|
||||
|
||||
### Non-Blocking File Transfer
|
||||
|
||||
```c
|
||||
int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
|
||||
uint8_t channel, bool encrypt) {
|
||||
int offset = 0;
|
||||
int bytesLeft = fileSize;
|
||||
|
||||
while (bytesLeft > 0) {
|
||||
secLinkPoll(link); // process ACKs, free window slots
|
||||
|
||||
if (secLinkGetPending(link) < 4) { // window has room
|
||||
int chunk = bytesLeft;
|
||||
if (chunk > SECLINK_MAX_PAYLOAD) {
|
||||
chunk = SECLINK_MAX_PAYLOAD;
|
||||
}
|
||||
|
||||
int rc = secLinkSend(link, fileData + offset, chunk,
|
||||
channel, encrypt, false);
|
||||
if (rc == SECLINK_SUCCESS) {
|
||||
offset += chunk;
|
||||
bytesLeft -= chunk;
|
||||
}
|
||||
// SECLINK_ERR_SEND means window full, retry next iteration
|
||||
}
|
||||
|
||||
// Application can do other work here:
|
||||
// update progress bar, check for cancel, etc.
|
||||
}
|
||||
|
||||
// Drain remaining ACKs
|
||||
while (secLinkGetPending(link) > 0) {
|
||||
secLinkPoll(link);
|
||||
}
|
||||
|
||||
return SECLINK_SUCCESS;
|
||||
}
|
||||
```
|
||||
|
||||
### Blocking Bulk Transfer
|
||||
|
||||
```c
|
||||
// Send an entire file in one call (blocks until complete)
|
||||
secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true);
|
||||
```
|
||||
|
||||
|
||||
## Internal State Machine
|
||||
|
||||
SecLink maintains a three-state internal state machine:
|
||||
|
||||
| State | Value | Description |
|
||||
|--------------|-------|----------------------------------------------|
|
||||
| `STATE_INIT` | 0 | Link open, no handshake attempted yet |
|
||||
| `STATE_HANDSHAKE` | 1 | DH key exchange in progress |
|
||||
| `STATE_READY` | 2 | Handshake complete, ciphers ready |
|
||||
|
||||
Transitions:
|
||||
- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called
|
||||
- `HANDSHAKE -> READY`: when the remote's public key is received and
|
||||
cipher keys are derived
|
||||
- Any state -> cleanup: when `secLinkClose()` is called
|
||||
|
||||
Cleartext packets can be sent and received in any state. Encrypted
|
||||
packets require `STATE_READY`.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/libseclink.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Objects are placed in `../obj/seclink/`, the library in `../lib/`.
|
||||
|
||||
Link against all four libraries in this order:
|
||||
|
||||
```
|
||||
-lseclink -lpacket -lsecurity -lrs232
|
||||
```
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `secLink.h` -- Public API header (types, constants, function prototypes)
|
||||
- `secLink.c` -- Complete implementation (handshake, send, receive, state
|
||||
machine)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
SecLink requires these libraries (all built into `../lib/`):
|
||||
|
||||
| Library | Purpose |
|
||||
|------------------|---------------------------------------------|
|
||||
| `librs232.a` | Serial port driver (ISR, ring buffers) |
|
||||
| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
|
||||
| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Security Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
|
||||
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
||||
|
||||
OBJDIR = ../obj/security
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = security.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/libsecurity.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/security.o: security.c security.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
|
|
@ -1,454 +0,0 @@
|
|||
# Security -- Diffie-Hellman Key Exchange and XTEA-CTR Cipher
|
||||
|
||||
Cryptographic library providing Diffie-Hellman key exchange, XTEA
|
||||
symmetric encryption in CTR mode, and a DRBG-based pseudo-random number
|
||||
generator. Optimized for 486-class DOS hardware running under DJGPP/DPMI.
|
||||
|
||||
This library has no dependencies on the serial stack and can be used
|
||||
independently for any application requiring key exchange, encryption,
|
||||
or random number generation.
|
||||
|
||||
|
||||
## Components
|
||||
|
||||
### 1. XTEA Cipher (CTR Mode)
|
||||
|
||||
XTEA (eXtended Tiny Encryption Algorithm) is a 64-bit block cipher with a
|
||||
128-bit key and 32 Feistel rounds. In CTR (counter) mode, it operates as
|
||||
a stream cipher: an incrementing counter is encrypted with the key to
|
||||
produce a keystream, which is XOR'd with the plaintext. Because XOR is
|
||||
its own inverse, the same operation encrypts and decrypts.
|
||||
|
||||
**Why XTEA instead of AES or DES:**
|
||||
|
||||
XTEA requires zero lookup tables, no key schedule, and compiles to
|
||||
approximately 20 instructions per round (shifts, adds, and XORs only).
|
||||
This makes it ideal for a 486 where the data cache is tiny (8KB) and
|
||||
AES's 4KB S-boxes would thrash it. DES is similarly table-heavy and has
|
||||
a complex key schedule. XTEA has no library dependencies -- the entire
|
||||
cipher fits in about a dozen lines of C. At 32 rounds, XTEA provides
|
||||
128-bit security with negligible per-byte overhead even on the slowest
|
||||
target hardware.
|
||||
|
||||
**CTR mode properties:**
|
||||
|
||||
- Encrypt and decrypt are the same function (XOR is symmetric)
|
||||
- No padding required -- operates on arbitrary-length data
|
||||
- Random access possible (set the counter to any value)
|
||||
- CRITICAL: the same counter value must never be reused with the same key.
|
||||
Reuse reveals the XOR of two plaintexts. The secLink layer prevents this
|
||||
by deriving separate TX/RX cipher keys for each direction.
|
||||
|
||||
**XTEA block cipher internals:**
|
||||
|
||||
The Feistel network uses the golden-ratio constant (delta = 0x9E3779B9)
|
||||
as a round key mixer. Each round combines the two 32-bit halves using
|
||||
shifts, additions, and XORs. The delta ensures each round uses a
|
||||
different effective subkey, preventing slide attacks. No S-boxes or lookup
|
||||
tables are involved anywhere in the computation.
|
||||
|
||||
### 2. Diffie-Hellman Key Exchange (1024-bit)
|
||||
|
||||
Uses the RFC 2409 Group 2 safe prime (1024-bit MODP group) with a
|
||||
generator of 2. Private exponents are 256 bits for fast computation on
|
||||
486-class hardware.
|
||||
|
||||
**Why 1024-bit DH with 256-bit private exponents:**
|
||||
|
||||
RFC 2409 Group 2 provides a well-audited, interoperable safe prime.
|
||||
256-bit private exponents (versus full 1024-bit) reduce the modular
|
||||
exponentiation from approximately 1024 squarings+multiplies to approximately
|
||||
256 squarings + approximately 128 multiplies (half the exponent bits are 1 on
|
||||
average). This makes key generation feasible on a 486 in under a second
|
||||
rather than minutes. The security reduction is negligible -- Pollard's
|
||||
rho on a 256-bit exponent requires approximately 2^128 operations, matching
|
||||
XTEA's key strength.
|
||||
|
||||
**Key validation:**
|
||||
|
||||
`secDhComputeSecret()` validates that the remote public key is in the
|
||||
range [2, p-2] to prevent small-subgroup attacks. Keys of 0, 1, or p-1
|
||||
would produce trivially guessable shared secrets.
|
||||
|
||||
**Key derivation:**
|
||||
|
||||
The 128-byte shared secret is reduced to a symmetric key via XOR-folding:
|
||||
each byte of the secret is XOR'd into the output key at position
|
||||
`i % keyLen`. For a 16-byte XTEA key, each output byte is the XOR of
|
||||
8 secret bytes, providing thorough mixing. A proper KDF (HKDF, etc.)
|
||||
would be more rigorous but adds complexity and code size for marginal
|
||||
benefit in this use case.
|
||||
|
||||
### 3. Pseudo-Random Number Generator
|
||||
|
||||
XTEA-CTR based DRBG (Deterministic Random Bit Generator). The RNG
|
||||
encrypts a monotonically increasing 64-bit counter with a 128-bit XTEA
|
||||
key, producing 8 bytes of pseudorandom output per block. The counter
|
||||
never repeats (64-bit space is sufficient for any practical session
|
||||
length), so the output is a pseudorandom stream as long as the key has
|
||||
sufficient entropy.
|
||||
|
||||
**Hardware entropy sources:**
|
||||
|
||||
- PIT (Programmable Interval Timer) -- runs at 1.193182 MHz. Its LSBs
|
||||
change rapidly and provide approximately 10 bits of entropy per read,
|
||||
depending on timing jitter. Two readings with intervening code execution
|
||||
provide additional jitter.
|
||||
- BIOS tick count -- 18.2 Hz timer at real-mode address 0040:046C. Adds
|
||||
a few more bits of entropy.
|
||||
|
||||
Total from hardware: roughly 20 bits of real entropy per call to
|
||||
`secRngGatherEntropy()`. This is not enough on its own for
|
||||
cryptographic use but is sufficient to seed the DRBG when supplemented
|
||||
by user interaction timing (keyboard, mouse jitter).
|
||||
|
||||
**Seeding and mixing:**
|
||||
|
||||
The seed function (`secRngSeed()`) XOR-folds the entropy into the XTEA
|
||||
key, derives the initial counter from the key bits, and then generates and
|
||||
discards 64 bytes to advance past any weak initial output. This discard
|
||||
step is standard DRBG practice -- it ensures the first bytes the caller
|
||||
receives do not leak information about the seed material.
|
||||
|
||||
Additional entropy can be stirred in at any time via `secRngAddEntropy()`
|
||||
without resetting the RNG state. This function XOR-folds new entropy into
|
||||
the key and then re-mixes by encrypting the key with itself, diffusing
|
||||
the new entropy across all key bits.
|
||||
|
||||
Auto-seeding: if `secRngBytes()` is called before `secRngSeed()`, it
|
||||
automatically gathers hardware entropy and seeds itself as a safety net.
|
||||
|
||||
|
||||
## BigNum Arithmetic
|
||||
|
||||
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
|
||||
stored as 32 x `uint32_t` words in little-endian order. Operations:
|
||||
|
||||
| Function | Description |
|
||||
|----------------|------------------------------------------------------|
|
||||
| `bnAdd` | Add two bignums, return carry |
|
||||
| `bnSub` | Subtract two bignums, return borrow |
|
||||
| `bnCmp` | Compare two bignums (-1, 0, +1) |
|
||||
| `bnBit` | Test a single bit by index |
|
||||
| `bnBitLength` | Find the highest set bit position |
|
||||
| `bnShiftLeft1` | Left-shift by 1, return carry |
|
||||
| `bnClear` | Zero all words |
|
||||
| `bnSet` | Set to a 32-bit value (clear upper words) |
|
||||
| `bnCopy` | Copy from source to destination |
|
||||
| `bnFromBytes` | Convert big-endian byte array to little-endian words |
|
||||
| `bnToBytes` | Convert little-endian words to big-endian byte array |
|
||||
| `bnMontMul` | Montgomery multiplication (CIOS variant) |
|
||||
| `bnModExp` | Modular exponentiation via Montgomery multiply |
|
||||
|
||||
|
||||
## Montgomery Multiplication
|
||||
|
||||
The CIOS (Coarsely Integrated Operand Scanning) variant computes
|
||||
`a * b * R^(-1) mod m` in a single pass without explicit division by the
|
||||
modulus. This replaces the expensive modular reduction step (division by a
|
||||
1024-bit number) with cheaper additions and right-shifts.
|
||||
|
||||
For each of the 32 outer iterations (one per word of operand `a`):
|
||||
1. Accumulate `a[i] * b` into the temporary product `t`
|
||||
2. Compute the Montgomery reduction factor `u = t[0] * m0inv mod 2^32`
|
||||
3. Add `u * mod` to `t` and shift right by 32 bits (implicit division)
|
||||
|
||||
After all iterations, the result is in the range [0, 2m), so a single
|
||||
conditional subtraction brings it into [0, m).
|
||||
|
||||
**Montgomery constants** (computed once, lazily on first DH use):
|
||||
|
||||
- `R^2 mod p` -- computed via 2048 iterations of shift-left-1 with
|
||||
conditional subtraction. This is the Montgomery domain conversion
|
||||
factor.
|
||||
- `-p[0]^(-1) mod 2^32` -- computed via Newton's method (5 iterations,
|
||||
doubling precision each step: 1->2->4->8->16->32 correct bits). This
|
||||
is the Montgomery reduction constant.
|
||||
|
||||
**Modular exponentiation** uses left-to-right binary square-and-multiply
|
||||
scanning. For a 256-bit private exponent, this requires approximately 256
|
||||
squarings plus approximately 128 multiplies (half the bits are 1 on average),
|
||||
where each operation is a Montgomery multiplication on 32-word numbers.
|
||||
|
||||
|
||||
## Secure Zeroing
|
||||
|
||||
Key material (private keys, shared secrets, cipher contexts) is erased
|
||||
using a volatile-pointer loop:
|
||||
|
||||
```c
|
||||
static void secureZero(void *ptr, int len) {
|
||||
volatile uint8_t *p = (volatile uint8_t *)ptr;
|
||||
for (int i = 0; i < len; i++) {
|
||||
p[i] = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `volatile` qualifier prevents the compiler from optimizing away the
|
||||
zeroing as a dead store. Without it, the compiler would see that the
|
||||
buffer is about to be freed and remove the memset entirely. This is
|
||||
critical for preventing sensitive key material from lingering in freed
|
||||
memory where a later `malloc` could expose it.
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
At serial port speeds, XTEA-CTR encryption overhead is minimal:
|
||||
|
||||
| Speed | Blocks/sec | CPU Cycles/sec | % of 33 MHz 486 |
|
||||
|----------|------------|----------------|------------------|
|
||||
| 9600 | 120 | ~240K | < 1% |
|
||||
| 57600 | 720 | ~1.4M | ~4% |
|
||||
| 115200 | 1440 | ~2.9M | ~9% |
|
||||
|
||||
DH key exchange takes approximately 0.3 seconds at 66 MHz or 0.6 seconds
|
||||
at 33 MHz (256-bit private exponent, 1024-bit modulus, Montgomery
|
||||
multiplication).
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|---------------------|-------|-----------------------------------|
|
||||
| `SEC_DH_KEY_SIZE` | 128 | DH public key size in bytes |
|
||||
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size in bytes |
|
||||
| `SEC_SUCCESS` | 0 | Success |
|
||||
| `SEC_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
|
||||
| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived |
|
||||
| `SEC_ERR_ALLOC` | -3 | Memory allocation failed |
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
typedef struct SecDhS SecDhT; // Opaque DH context
|
||||
typedef struct SecCipherS SecCipherT; // Opaque cipher context
|
||||
```
|
||||
|
||||
### RNG Functions
|
||||
|
||||
```c
|
||||
int secRngGatherEntropy(uint8_t *buf, int len);
|
||||
```
|
||||
|
||||
Reads hardware entropy from the PIT counter and BIOS tick count. Returns
|
||||
the number of bytes written (up to 8). Provides roughly 20 bits of true
|
||||
entropy -- not sufficient alone, but enough to seed the DRBG when
|
||||
supplemented by user interaction timing.
|
||||
|
||||
```c
|
||||
void secRngSeed(const uint8_t *entropy, int len);
|
||||
```
|
||||
|
||||
Initializes the DRBG with the given entropy. XOR-folds the input into
|
||||
the XTEA key, derives the initial counter, and generates and discards 64
|
||||
bytes to advance past weak initial output.
|
||||
|
||||
```c
|
||||
void secRngAddEntropy(const uint8_t *data, int len);
|
||||
```
|
||||
|
||||
Mixes additional entropy into the running RNG state without resetting it.
|
||||
XOR-folds data into the key and re-mixes by encrypting the key with
|
||||
itself. Use this to stir in keyboard timing, mouse jitter, or other
|
||||
runtime entropy sources.
|
||||
|
||||
```c
|
||||
void secRngBytes(uint8_t *buf, int len);
|
||||
```
|
||||
|
||||
Generates `len` pseudorandom bytes. Auto-seeds from hardware entropy if
|
||||
not previously seeded. Produces 8 bytes per XTEA block encryption of the
|
||||
internal counter.
|
||||
|
||||
### Diffie-Hellman Functions
|
||||
|
||||
```c
|
||||
SecDhT *secDhCreate(void);
|
||||
```
|
||||
|
||||
Allocates a new DH context. Returns `NULL` on allocation failure. The
|
||||
context must be destroyed with `secDhDestroy()` when no longer needed.
|
||||
|
||||
```c
|
||||
int secDhGenerateKeys(SecDhT *dh);
|
||||
```
|
||||
|
||||
Generates a 256-bit random private key and computes the corresponding
|
||||
1024-bit public key (`g^private mod p`). Lazily initializes Montgomery
|
||||
constants on first call. The RNG should be seeded before calling this.
|
||||
|
||||
```c
|
||||
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
|
||||
```
|
||||
|
||||
Exports the public key as a big-endian byte array into `buf`. On entry,
|
||||
`*len` must be at least `SEC_DH_KEY_SIZE` (128). On return, `*len` is
|
||||
set to 128.
|
||||
|
||||
```c
|
||||
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
|
||||
```
|
||||
|
||||
Computes the shared secret from the remote side's public key
|
||||
(`remote^private mod p`). Validates the remote key is in range [2, p-2].
|
||||
Both sides compute this independently and arrive at the same value.
|
||||
|
||||
```c
|
||||
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
|
||||
```
|
||||
|
||||
Derives a symmetric key by XOR-folding the 128-byte shared secret down
|
||||
to `keyLen` bytes. Each output byte is the XOR of `128/keyLen` input
|
||||
bytes.
|
||||
|
||||
```c
|
||||
void secDhDestroy(SecDhT *dh);
|
||||
```
|
||||
|
||||
Securely zeroes the entire DH context (private key, shared secret, public
|
||||
key) and frees the memory. Must be called to prevent key material from
|
||||
lingering in the heap.
|
||||
|
||||
### Cipher Functions
|
||||
|
||||
```c
|
||||
SecCipherT *secCipherCreate(const uint8_t *key);
|
||||
```
|
||||
|
||||
Creates an XTEA-CTR cipher context with the given 16-byte key. The
|
||||
internal counter starts at zero. Returns `NULL` on allocation failure or
|
||||
NULL key.
|
||||
|
||||
```c
|
||||
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
|
||||
```
|
||||
|
||||
Encrypts or decrypts `data` in place. CTR mode is symmetric -- the same
|
||||
function handles both directions. The internal counter advances by one
|
||||
for every 8 bytes processed (one XTEA block). The counter must never
|
||||
repeat with the same key; callers are responsible for ensuring this
|
||||
(secLink handles it by using separate cipher instances per direction).
|
||||
|
||||
```c
|
||||
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
|
||||
```
|
||||
|
||||
Sets the 64-bit nonce/counter to a specific value. Both the nonce
|
||||
(baseline) and the running counter are set to the same value. Call this
|
||||
before encrypting if you need a deterministic starting point.
|
||||
|
||||
```c
|
||||
void secCipherDestroy(SecCipherT *c);
|
||||
```
|
||||
|
||||
Securely zeroes the cipher context (key and counter state) and frees the
|
||||
memory.
|
||||
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Full Key Exchange
|
||||
|
||||
```c
|
||||
#include "security.h"
|
||||
#include <string.h>
|
||||
|
||||
// Seed the RNG
|
||||
uint8_t entropy[16];
|
||||
secRngGatherEntropy(entropy, sizeof(entropy));
|
||||
secRngSeed(entropy, sizeof(entropy));
|
||||
|
||||
// Create DH context and generate keys
|
||||
SecDhT *dh = secDhCreate();
|
||||
secDhGenerateKeys(dh);
|
||||
|
||||
// Export public key to send to remote
|
||||
uint8_t myPub[SEC_DH_KEY_SIZE];
|
||||
int pubLen = SEC_DH_KEY_SIZE;
|
||||
secDhGetPublicKey(dh, myPub, &pubLen);
|
||||
// ... send myPub to remote, receive remotePub ...
|
||||
|
||||
// Compute shared secret and derive a 16-byte XTEA key
|
||||
secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE);
|
||||
|
||||
uint8_t key[SEC_XTEA_KEY_SIZE];
|
||||
secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE);
|
||||
secDhDestroy(dh); // private key no longer needed
|
||||
|
||||
// Create cipher and encrypt
|
||||
SecCipherT *cipher = secCipherCreate(key);
|
||||
uint8_t message[] = "Secret message";
|
||||
secCipherCrypt(cipher, message, sizeof(message));
|
||||
// message is now encrypted
|
||||
|
||||
// Decrypt (reset counter first, then apply same operation)
|
||||
secCipherSetNonce(cipher, 0, 0);
|
||||
secCipherCrypt(cipher, message, sizeof(message));
|
||||
// message is now plaintext again
|
||||
|
||||
secCipherDestroy(cipher);
|
||||
```
|
||||
|
||||
### Standalone Encryption (Without DH)
|
||||
|
||||
```c
|
||||
// XTEA-CTR can be used independently of Diffie-Hellman
|
||||
uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ };
|
||||
SecCipherT *c = secCipherCreate(key);
|
||||
|
||||
uint8_t data[1024];
|
||||
// ... fill data ...
|
||||
secCipherCrypt(c, data, sizeof(data)); // encrypt in place
|
||||
|
||||
secCipherDestroy(c);
|
||||
```
|
||||
|
||||
### Random Number Generation
|
||||
|
||||
```c
|
||||
// Seed from hardware
|
||||
uint8_t hwEntropy[16];
|
||||
secRngGatherEntropy(hwEntropy, sizeof(hwEntropy));
|
||||
secRngSeed(hwEntropy, sizeof(hwEntropy));
|
||||
|
||||
// Stir in user-derived entropy (keyboard timing, etc.)
|
||||
uint8_t userEntropy[4];
|
||||
// ... gather from timing events ...
|
||||
secRngAddEntropy(userEntropy, sizeof(userEntropy));
|
||||
|
||||
// Generate random bytes
|
||||
uint8_t randomBuf[32];
|
||||
secRngBytes(randomBuf, sizeof(randomBuf));
|
||||
```
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/libsecurity.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Objects are placed in `../obj/security/`, the library in `../lib/`.
|
||||
|
||||
No external dependencies -- the library is self-contained. It uses only
|
||||
DJGPP's `<pc.h>`, `<sys/farptr.h>`, and `<go32.h>` for hardware entropy
|
||||
collection (PIT and BIOS tick count access).
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `security.h` -- Public API header (types, constants, function prototypes)
|
||||
- `security.c` -- Complete implementation (bignum, Montgomery, DH, XTEA, RNG)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Used By
|
||||
|
||||
- `seclink/` -- Secure serial link (DH handshake, cipher creation, RNG seeding)
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# DVX Serial Stack Makefile for DJGPP cross-compilation
|
||||
#
|
||||
# Builds serial.lib -- combines rs232, packet, security, and seclink
|
||||
# into a single DXE library loaded by the DVX system.
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../rs232 -I../packet -I../security -I../seclink -I../core -I../core/platform -I../tasks
|
||||
|
||||
OBJDIR = ../obj/serial
|
||||
LIBSDIR = ../bin/libs
|
||||
|
||||
SRCS = ../rs232/rs232.c ../packet/packet.c ../security/security.c ../seclink/secLink.c
|
||||
OBJS = $(OBJDIR)/rs232.o $(OBJDIR)/packet.o $(OBJDIR)/security.o $(OBJDIR)/secLink.o
|
||||
TARGET = $(LIBSDIR)/serial.lib
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET) $(LIBSDIR)/serial.dep
|
||||
|
||||
$(LIBSDIR)/serial.dep: ../config/serial.dep | $(LIBSDIR)
|
||||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBSDIR)
|
||||
$(DXE3GEN) -o $(LIBSDIR)/serial.dxe \
|
||||
-E _rs232 -E _pkt -E _secLink -E _secDh -E _secCipher -E _secRng \
|
||||
-U $(OBJS)
|
||||
mv $(LIBSDIR)/serial.dxe $@
|
||||
|
||||
$(OBJDIR)/rs232.o: ../rs232/rs232.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/packet.o: ../packet/packet.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/security.o: ../security/security.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/secLink.o: ../seclink/secLink.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBSDIR):
|
||||
mkdir -p $(LIBSDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/rs232.o: ../rs232/rs232.c ../rs232/rs232.h
|
||||
$(OBJDIR)/packet.o: ../packet/packet.c ../packet/packet.h ../rs232/rs232.h
|
||||
$(OBJDIR)/security.o: ../security/security.c ../security/security.h
|
||||
$(OBJDIR)/secLink.o: ../seclink/secLink.c ../seclink/secLink.h ../rs232/rs232.h ../packet/packet.h ../security/security.h
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/serial.dep
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
# DVX Shell Makefile for DJGPP cross-compilation
|
||||
#
|
||||
# Builds dvxshell.lib -- the shell module loaded by the DVX loader.
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../widgets -I../tasks -I../core/thirdparty
|
||||
|
||||
OBJDIR = ../obj/shell
|
||||
LIBSDIR = ../bin/libs
|
||||
CONFIGDIR = ../bin/config
|
||||
THEMEDIR = ../bin/config/themes
|
||||
WPAPERDIR = ../bin/config/wpaper
|
||||
|
||||
SRCS = shellMain.c shellApp.c shellInfo.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBSDIR)/dvxshell.lib
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
THEMES = $(THEMEDIR)/geos.thm $(THEMEDIR)/win31.thm $(THEMEDIR)/cde.thm
|
||||
WPAPERS = $(WPAPERDIR)/blueglow.jpg $(WPAPERDIR)/swoop.jpg $(WPAPERDIR)/triangle.jpg
|
||||
|
||||
all: $(TARGET) $(LIBSDIR)/dvxshell.dep $(CONFIGDIR)/dvx.ini $(THEMES) $(WPAPERS)
|
||||
|
||||
$(LIBSDIR)/dvxshell.dep: ../config/dvxshell.dep | $(LIBSDIR)
|
||||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBSDIR)
|
||||
$(DXE3GEN) -o $(LIBSDIR)/dvxshell.dxe -E _shell -U $(OBJS)
|
||||
mv $(LIBSDIR)/dvxshell.dxe $@
|
||||
|
||||
$(CONFIGDIR)/dvx.ini: ../config/dvx.ini | $(CONFIGDIR)
|
||||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBSDIR):
|
||||
mkdir -p $(LIBSDIR)
|
||||
|
||||
$(CONFIGDIR):
|
||||
mkdir -p $(CONFIGDIR)
|
||||
|
||||
$(THEMEDIR):
|
||||
mkdir -p $(THEMEDIR)
|
||||
|
||||
$(THEMEDIR)/%.thm: ../config/themes/%.thm | $(THEMEDIR)
|
||||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(WPAPERDIR):
|
||||
mkdir -p $(WPAPERDIR)
|
||||
|
||||
$(WPAPERDIR)/%.jpg: ../config/wpaper/%.jpg | $(WPAPERDIR)
|
||||
cp $< $@
|
||||
|
||||
# Dependencies
|
||||
SHELL_DEPS = shellApp.h ../core/dvxWidget.h ../core/dvxApp.h ../core/dvxTypes.h ../core/platform/dvxPlatform.h
|
||||
$(OBJDIR)/shellMain.o: shellMain.c $(SHELL_DEPS)
|
||||
$(OBJDIR)/shellApp.o: shellApp.c $(SHELL_DEPS)
|
||||
$(OBJDIR)/shellInfo.o: shellInfo.c shellInfo.h $(SHELL_DEPS)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/dvxshell.dep
|
||||
rm -rf $(WPAPERDIR) $(THEMEDIR) $(CONFIGDIR)
|
||||
180
shell/README.md
180
shell/README.md
|
|
@ -1,180 +0,0 @@
|
|||
# DVX Shell (dvxshell.lib)
|
||||
|
||||
The DVX Shell is a DXE3 module loaded by the DVX loader at startup.
|
||||
It initializes the GUI subsystem, loads DXE3 application modules on
|
||||
demand, runs the cooperative main loop, and provides crash recovery so
|
||||
a faulting app does not bring down the entire system.
|
||||
|
||||
|
||||
## Entry Point
|
||||
|
||||
The loader finds and calls `shellMain()` after all libs and widgets
|
||||
are loaded. `shellMain()`:
|
||||
|
||||
1. Loads preferences from `CONFIG/DVX.INI`
|
||||
2. Initializes the GUI via `dvxInit()` with configured video mode
|
||||
3. Applies saved mouse, color, and wallpaper settings from INI
|
||||
4. Shows a splash screen ("DVX - DOS Visual eXecutive / Loading...")
|
||||
5. Initializes the cooperative task system (`tsInit()`)
|
||||
6. Sets shell task (task 0) to `TS_PRIORITY_HIGH`
|
||||
7. Gathers system information via the platform layer
|
||||
8. Initializes the app slot table
|
||||
9. Points the memory tracker at `currentAppId` for per-app attribution
|
||||
10. Registers idle callback, Ctrl+Esc handler, and title change handler
|
||||
11. Installs the crash handler (before loading apps, so init crashes are caught)
|
||||
12. Loads the desktop app (default: `apps/progman/progman.app`)
|
||||
13. Dismisses the splash screen
|
||||
14. Enters the main loop
|
||||
|
||||
|
||||
## Main Loop
|
||||
|
||||
Each iteration of the main loop does four things:
|
||||
|
||||
1. `dvxUpdate()` -- process input events, dispatch callbacks, composite
|
||||
dirty rects, flush to LFB
|
||||
2. `tsYield()` -- give CPU time to app tasks (if any are active)
|
||||
3. `shellReapApps()` -- clean up any apps that terminated this frame
|
||||
4. `shellDesktopUpdate()` -- notify desktop app if apps were reaped
|
||||
|
||||
An idle callback (`idleYield`) is also registered so that `dvxUpdate()`
|
||||
yields to app tasks during quiet frames.
|
||||
|
||||
|
||||
## App Lifecycle
|
||||
|
||||
### DXE App Contract
|
||||
|
||||
Every DXE app exports two symbols:
|
||||
|
||||
* `appDescriptor` (`AppDescriptorT`) -- metadata:
|
||||
- `name` -- display name (max 64 chars)
|
||||
- `hasMainLoop` -- true for main-loop apps, false for callback-only
|
||||
- `multiInstance` -- true to allow multiple instances via temp copy
|
||||
- `stackSize` -- `SHELL_STACK_DEFAULT` (32KB) or explicit byte count
|
||||
- `priority` -- `TS_PRIORITY_NORMAL` or custom
|
||||
|
||||
* `appMain` (`int appMain(DxeAppContextT *)`) -- entry point
|
||||
|
||||
Optional export: `appShutdown` (`void appShutdown(void)`) -- called
|
||||
during graceful shutdown.
|
||||
|
||||
### Callback-Only Apps (hasMainLoop = false)
|
||||
|
||||
`appMain()` is called directly in the shell's task 0. It creates
|
||||
windows, registers callbacks, and returns immediately. The app lives
|
||||
entirely through GUI callbacks. The shell reaps callback-only apps
|
||||
automatically when their last window closes -- `shellReapApps()` checks
|
||||
each frame for running callback apps with zero remaining windows.
|
||||
|
||||
### Main-Loop Apps (hasMainLoop = true)
|
||||
|
||||
A dedicated cooperative task is created. `appMain()` runs in that task
|
||||
and can do its own polling/processing loop, calling `tsYield()` to
|
||||
share CPU. Lifecycle ends when `appMain()` returns or the task is
|
||||
killed.
|
||||
|
||||
### App States
|
||||
|
||||
```
|
||||
Free -> Loaded -> Running -> Terminating -> Free
|
||||
```
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| `AppStateFreeE` | Slot available for reuse |
|
||||
| `AppStateLoadedE` | DXE loaded, not yet started (transient) |
|
||||
| `AppStateRunningE` | Entry point called, active |
|
||||
| `AppStateTerminatingE` | Shutdown in progress, awaiting reap |
|
||||
|
||||
### App Slots
|
||||
|
||||
App slots are managed as a stb_ds dynamic array (no fixed max). Each
|
||||
slot tracks: app ID, name, path, DXE handle, state, task ID, entry/
|
||||
shutdown function pointers, and a pointer to the `DxeAppContextT`
|
||||
passed to the app.
|
||||
|
||||
`DxeAppContextT` is heap-allocated (via `calloc`) so its address is
|
||||
stable across `sApps` array reallocs -- apps save this pointer in their
|
||||
static globals and it must not move. The shell frees it during reap.
|
||||
|
||||
The `DxeAppContextT` gives each app:
|
||||
- `shellCtx` -- pointer to the shell's `AppContextT`
|
||||
- `appId` -- this app's unique ID
|
||||
- `appDir` -- directory containing the `.app` file (for resources)
|
||||
- `configDir` -- writable config directory (`CONFIG/<apppath>/`)
|
||||
|
||||
### App ID Tracking
|
||||
|
||||
`ctx->currentAppId` on AppContextT tracks which app is currently
|
||||
executing. The shell sets this before calling app code.
|
||||
`dvxCreateWindow()` stamps `win->appId` directly so the shell can
|
||||
associate windows with apps for cleanup.
|
||||
|
||||
For main-loop apps, `appTaskWrapper` receives the app ID (as an int
|
||||
cast to `void *`), not a direct pointer to `ShellAppT`. This is because
|
||||
the `sApps` dynamic array may reallocate between `tsCreate` and the
|
||||
first time the task runs, which would invalidate a direct pointer.
|
||||
|
||||
The shell calls `dvxSetBusy()` before `dlopen` to show the hourglass
|
||||
cursor during app loading, and clears it after `appMain` returns (for
|
||||
callback apps) or after task creation (for main-loop apps).
|
||||
|
||||
|
||||
## Crash Recovery
|
||||
|
||||
The platform layer installs signal handlers for SIGSEGV, SIGFPE, and
|
||||
SIGILL via `platformInstallCrashHandler()`. If a crash occurs:
|
||||
|
||||
1. Platform handler logs signal name and register dump (DJGPP)
|
||||
2. Handler `longjmp`s to the `setjmp` point in `shellMain()`
|
||||
3. `tsRecoverToMain()` fixes the scheduler's bookkeeping
|
||||
4. Shell logs app-specific info (name, path, task ID)
|
||||
5. Crashed app is force-killed (`shellForceKillApp()`)
|
||||
6. Error dialog is shown to the user
|
||||
7. Desktop is notified to refresh
|
||||
8. Main loop continues normally
|
||||
|
||||
This gives Windows 3.1-style fault tolerance -- one bad app does not
|
||||
take down the whole system.
|
||||
|
||||
|
||||
## Task Manager Integration
|
||||
|
||||
The Task Manager is a separate DXE (`taskmgr.lib` in `taskmgr/`), not
|
||||
built into the shell. It registers itself at load time via a DXE
|
||||
constructor that sets the `shellCtrlEscFn` function pointer. The shell
|
||||
calls this pointer on Ctrl+Esc. If `taskmgr.lib` is not loaded,
|
||||
`shellCtrlEscFn` is NULL and Ctrl+Esc does nothing.
|
||||
|
||||
See `taskmgr/README.md` for full Task Manager documentation.
|
||||
|
||||
|
||||
## Desktop Update Notifications
|
||||
|
||||
Apps (especially the desktop app) register callbacks via
|
||||
`shellRegisterDesktopUpdate()` to be notified when app state changes
|
||||
(load, reap, crash, title change). Multiple callbacks are supported.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `shellMain.c` | Entry point, main loop, crash recovery, splash screen, idle callback |
|
||||
| `shellApp.h` | App lifecycle types: `AppDescriptorT`, `DxeAppContextT`, `ShellAppT`, `AppStateE`; `shellCtrlEscFn` extern |
|
||||
| `shellApp.c` | App loading, reaping, task creation, DXE management, per-app memory tracking |
|
||||
| `shellInfo.h` | System information wrapper |
|
||||
| `shellInfo.c` | Gathers and caches hardware info via platform layer |
|
||||
| `Makefile` | Builds `bin/libs/dvxshell.lib` + config/themes/wallpapers |
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
make # builds dvxshell.lib + dvxshell.dep + config files
|
||||
make clean # removes objects, library, and config output
|
||||
```
|
||||
|
||||
Depends on: `libtasks.lib`, `libdvx.lib`, `texthelp.lib`, `listhelp.lib`
|
||||
(via dvxshell.dep).
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// shellInfo.h -- System information display for DVX Shell
|
||||
//
|
||||
// Thin wrapper around platformGetSystemInfo(). Calls the platform
|
||||
// layer to gather hardware info, logs each line to DVX.LOG, and
|
||||
// caches the result for the Program Manager's "System Information"
|
||||
// dialog.
|
||||
#ifndef SHELL_INFO_H
|
||||
#define SHELL_INFO_H
|
||||
|
||||
#include "dvxApp.h"
|
||||
|
||||
// Gather all hardware information via the platform layer, log it,
|
||||
// and store for later retrieval. Call once after dvxInit.
|
||||
void shellInfoInit(AppContextT *ctx);
|
||||
|
||||
// Return the formatted system information text. The pointer is valid
|
||||
// for the lifetime of the process (static buffer in the platform layer).
|
||||
const char *shellGetSystemInfo(void);
|
||||
|
||||
#endif // SHELL_INFO_H
|
||||
169
src/apps/kpunch/Makefile
Normal file
169
src/apps/kpunch/Makefile
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
# DVX Shell Applications Makefile -- builds DXE3 modules
|
||||
#
|
||||
# Source tree is now one dir per app, mirroring bin/apps/kpunch/.
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
|
||||
|
||||
OBJDIR = ../../../obj/apps
|
||||
BINDIR = ../../../bin/apps
|
||||
DVXRES = ../../../bin/host/dvxres
|
||||
|
||||
# C apps: one directory, one .c file each
|
||||
C_APPS = progman clock dvxdemo cpanel dvxhelp
|
||||
|
||||
BASCOMP = ../../../bin/host/bascomp
|
||||
# BASIC apps: each is a .dbp project in its own directory.
|
||||
# BASIC-only notepad, imgview, etc. replace the old C versions.
|
||||
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
|
||||
|
||||
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
|
||||
|
||||
all: $(C_APPS) dvxbasic $(BASIC_APPS)
|
||||
|
||||
dvxbasic:
|
||||
$(MAKE) -C dvxbasic
|
||||
|
||||
cpanel: $(BINDIR)/kpunch/cpanel/cpanel.app
|
||||
progman: $(BINDIR)/kpunch/progman/progman.app
|
||||
clock: $(BINDIR)/kpunch/clock/clock.app
|
||||
dvxdemo: $(BINDIR)/kpunch/dvxdemo/dvxdemo.app
|
||||
dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
|
||||
|
||||
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
|
||||
|
||||
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
|
||||
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd clock && ../$(DVXRES) build ../$@ clock.res
|
||||
|
||||
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
|
||||
|
||||
$(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
|
||||
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
|
||||
|
||||
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
|
||||
|
||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
# BASIC apps (compiled from .dbp projects via bascomp). Source for each app
|
||||
# is under the app's own directory.
|
||||
iconed: $(BINDIR)/kpunch/iconed/iconed.app
|
||||
|
||||
$(BINDIR)/kpunch/iconed/iconed.app: iconed/iconed.dbp iconed/iconed.frm iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic
|
||||
$(BASCOMP) iconed/iconed.dbp -o $@ -release
|
||||
|
||||
notepad: $(BINDIR)/kpunch/notepad/notepad.app
|
||||
|
||||
$(BINDIR)/kpunch/notepad/notepad.app: notepad/notepad.dbp notepad/notepad.frm notepad/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/notepad dvxbasic
|
||||
$(BASCOMP) notepad/notepad.dbp -o $@ -release
|
||||
|
||||
imgview: $(BINDIR)/kpunch/imgview/imgview.app
|
||||
|
||||
$(BINDIR)/kpunch/imgview/imgview.app: imgview/imgview.dbp imgview/imgview.frm imgview/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/imgview dvxbasic
|
||||
$(BASCOMP) imgview/imgview.dbp -o $@ -release
|
||||
|
||||
helpedit: $(BINDIR)/kpunch/dvxhelp/helpedit.app
|
||||
|
||||
$(BINDIR)/kpunch/dvxhelp/helpedit.app: dvxhelp/helpedit/helpedit.dbp dvxhelp/helpedit/helpedit.frm dvxhelp/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic
|
||||
$(BASCOMP) dvxhelp/helpedit/helpedit.dbp -o $@ -release
|
||||
$(DVXRES) add $@ helpfile text "dvxhelp.hlp"
|
||||
|
||||
resedit: $(BINDIR)/kpunch/resedit/resedit.app
|
||||
|
||||
$(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic
|
||||
$(BASCOMP) resedit/resedit.dbp -o $@ -release
|
||||
|
||||
basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
|
||||
|
||||
$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
|
||||
$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
|
||||
|
||||
widshow: $(BINDIR)/kpunch/widshow/widshow.app
|
||||
|
||||
$(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic
|
||||
$(BASCOMP) widshow/widshow.dbp -o $@ -release
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(BINDIR)/kpunch/cpanel: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/progman: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/clock: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/dvxdemo: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/dvxhelp: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/iconed: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/notepad: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/widshow: ; mkdir -p $@
|
||||
|
||||
# Header dependencies
|
||||
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
|
||||
# Every widget public header. Apps that pull in widget APIs (e.g. dvxdemo)
|
||||
# must rebuild when these change, otherwise the struct-layout contract
|
||||
# between the widget DXE's sApi and the app's cast to RadioApiT/etc.
|
||||
# silently drifts and produces late-binding crashes.
|
||||
WIDGET_H = $(wildcard ../../widgets/kpunch/*/*.h)
|
||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c $(COMMON_H) ../../libs/kpunch/libdvx/dvxPrefs.h ../../libs/kpunch/libdvx/platform/dvxPlat.h
|
||||
$(OBJDIR)/progman.o: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
|
||||
$(OBJDIR)/clock.o: clock/clock.c ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxDraw.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h ../../libs/kpunch/libtasks/taskSwch.h
|
||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H) $(WIDGET_H)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJDIR)/*.o
|
||||
rm -f $(BINDIR)/kpunch/cpanel/cpanel.app
|
||||
rm -f $(BINDIR)/kpunch/progman/progman.app
|
||||
rm -f $(BINDIR)/kpunch/clock/clock.app
|
||||
rm -f $(BINDIR)/kpunch/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/kpunch/dvxdemo/,$(DVXDEMO_BMPS))
|
||||
rm -f $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
|
||||
rm -f $(BINDIR)/kpunch/iconed/iconed.app
|
||||
rm -f $(BINDIR)/kpunch/notepad/notepad.app
|
||||
rm -f $(BINDIR)/kpunch/imgview/imgview.app
|
||||
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
|
||||
rm -f $(BINDIR)/kpunch/resedit/resedit.app
|
||||
rm -f $(BINDIR)/kpunch/basdemo/basdemo.app
|
||||
rm -f $(BINDIR)/kpunch/widshow/widshow.app
|
||||
$(MAKE) -C dvxbasic clean
|
||||
472
src/apps/kpunch/README.md
Normal file
472
src/apps/kpunch/README.md
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
# DVX Shell Applications
|
||||
|
||||
Every DVX application is a DXE3 module packaged as a `.app` file,
|
||||
installed under `bin/apps/kpunch/<name>/<name>.app`. The Program
|
||||
Manager scans this directory at startup and displays each discovered
|
||||
app as a launchable icon on the desktop.
|
||||
|
||||
This directory holds the source for the bundled applications plus
|
||||
the BASIC-based sample apps. Two flavours of app live side by side:
|
||||
|
||||
* **C apps** -- written directly against the DVX SDK headers,
|
||||
compiled to a DXE3 by `dxe3gen`. See `progman/`, `clock/`,
|
||||
`dvxdemo/`, `cpanel/`, `dvxhelp/`.
|
||||
* **BASIC apps** -- `.dbp` projects compiled to `.app` files by the
|
||||
host-side `bascomp`. The compiler links the BASIC bytecode into a
|
||||
copy of `basstub.app`, which acts as the runtime host. See
|
||||
`iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
|
||||
`dvxhelp/helpedit/`.
|
||||
|
||||
The rest of this document covers writing a C app against the SDK.
|
||||
For BASIC apps, see `dvxbasic/README.md`.
|
||||
|
||||
|
||||
## DXE App Contract
|
||||
|
||||
Every `.app` binary exports two required symbols and one optional
|
||||
symbol:
|
||||
|
||||
```c
|
||||
AppDescriptorT appDescriptor; // required
|
||||
int32_t appMain(DxeAppContextT *ctx); // required
|
||||
void appShutdown(void); // optional
|
||||
```
|
||||
|
||||
### `appDescriptor`
|
||||
|
||||
A statically-initialised struct read by the shell at load time to
|
||||
decide how to launch the app:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
char name[SHELL_APP_NAME_MAX]; // display name (<= 64 chars)
|
||||
bool hasMainLoop; // see below
|
||||
bool multiInstance; // may have multiple copies open
|
||||
int32_t stackSize; // SHELL_STACK_DEFAULT or byte count
|
||||
int32_t priority; // TS_PRIORITY_* for main-loop apps
|
||||
} AppDescriptorT;
|
||||
```
|
||||
|
||||
Example (from `clock/clock.c`):
|
||||
|
||||
```c
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "Clock",
|
||||
.hasMainLoop = true,
|
||||
.multiInstance = true,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_LOW
|
||||
};
|
||||
```
|
||||
|
||||
### `appMain`
|
||||
|
||||
Entry point. The shell calls it with a `DxeAppContextT` that exposes
|
||||
the shell's GUI context, the app's ID, its install directory, a
|
||||
writable config directory, launch arguments, and a help-topic query
|
||||
callback for F1.
|
||||
|
||||
Returning from `appMain` signals that the app has finished; the
|
||||
shell unloads the DXE, frees per-app memory, and reclaims the app
|
||||
slot.
|
||||
|
||||
### `appShutdown`
|
||||
|
||||
Called by the shell when the app is force-killed (by the Task
|
||||
Manager) or when the shell is shutting down with this app still
|
||||
running. Main-loop apps use it to set a flag that their main loop
|
||||
observes, so the loop exits cleanly and `appMain` can return.
|
||||
|
||||
|
||||
## App Types
|
||||
|
||||
### Callback-only apps (`hasMainLoop = false`)
|
||||
|
||||
`appMain` creates windows, registers callbacks, and returns 0. The
|
||||
shell drives all further work by invoking those callbacks in
|
||||
response to user input and timers. No dedicated task is allocated;
|
||||
no per-app stack is consumed. The app's lifetime ends when the last
|
||||
window closes (or when every callback has released all references).
|
||||
|
||||
Use this pattern for event-driven UI: Notepad, Program Manager, DVX
|
||||
Demo, Control Panel, Image Viewer.
|
||||
|
||||
### Main-loop apps (`hasMainLoop = true`)
|
||||
|
||||
The shell creates a cooperative task for the app and calls
|
||||
`appMain` from inside it. `appMain` runs a loop that calls
|
||||
`tsYield()` periodically to share the CPU. Exit by breaking out of
|
||||
the loop and returning.
|
||||
|
||||
Use this pattern when the app needs continuous background work that
|
||||
does not map cleanly onto event callbacks: Clock (polls the system
|
||||
clock every second), terminal emulators, games.
|
||||
|
||||
|
||||
## Resource Conventions
|
||||
|
||||
Apps attach resources to their `.app` binary using `dvxres build`
|
||||
(see `src/tools/README.md`). The standard resource names are:
|
||||
|
||||
| Name | Type | Meaning |
|
||||
|---------------|--------|---------|
|
||||
| `icon32` | icon | 32x32 BMP shown in Program Manager and the Task Manager. |
|
||||
| `name` | text | Human-readable display name (overrides `appDescriptor.name` for UI where available). |
|
||||
| `author` | text | Author / maintainer name. |
|
||||
| `publisher` | text | Publisher or studio. |
|
||||
| `copyright` | text | Copyright notice. |
|
||||
| `version` | text | Version string, free-form. |
|
||||
| `description` | text | One-sentence description shown in About dialogs. |
|
||||
| `helpfile` | text | Optional relative path to a `.hlp` file for F1 context help. |
|
||||
|
||||
Additional application-specific resources may be attached with any
|
||||
unique name. The runtime reads them via
|
||||
`dvxResOpen(appPath)` / `dvxResRead(handle, name, &size)` /
|
||||
`dvxResClose(handle)`.
|
||||
|
||||
Typical `.res` manifest:
|
||||
|
||||
```
|
||||
# clock.res
|
||||
icon32 icon icon32.bmp
|
||||
name text "Clock"
|
||||
author text "Scott Duensing"
|
||||
copyright text "Copyright 2026 Scott Duensing"
|
||||
publisher text "Kangaroo Punch Studios"
|
||||
description text "Digital clock with date display"
|
||||
```
|
||||
|
||||
|
||||
## Bundled Applications
|
||||
|
||||
### Program Manager (progman)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/progman/progman.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
The desktop. Recursively scans `apps/` for `.app` files and displays
|
||||
them as a launchable grid with icons, names, and publishers. Double
|
||||
click or Enter to launch. Includes a Help menu with the system help
|
||||
viewer and the About / System Info dialog. Registers with
|
||||
`shellRegisterDesktopUpdate()` so the grid refreshes when apps are
|
||||
loaded, terminated, or crash.
|
||||
|
||||
Widget headers used: `box/box.h`, `button/button.h`, `label/label.h`,
|
||||
`statusBar/statBar.h`, `textInput/textInpt.h`, `imageButton/imgBtn.h`,
|
||||
`scrollPane/scrlPane.h`, `wrapBox/wrapBox.h`.
|
||||
|
||||
### Notepad (notepad, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/notepad/notepad.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Plain-text editor with File menu (New, Open, Save, Save As) and Edit
|
||||
menu (Cut, Copy, Paste, Select All). Built from a BASIC project
|
||||
(`notepad.dbp` + `notepad.frm`); demonstrates the form designer,
|
||||
TextInput widget, and common dialog integration.
|
||||
|
||||
### Clock (clock)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/clock/clock.app` |
|
||||
| Type | Main-loop |
|
||||
| Multi-instance | Yes |
|
||||
|
||||
Digital clock showing time and date. Polls the system clock every
|
||||
second and invalidates the window to trigger a repaint. Uses the raw
|
||||
`onPaint` callback (no widgets) to draw centered text. Reference
|
||||
implementation for main-loop apps.
|
||||
|
||||
### DVX Demo (dvxdemo)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxdemo/dvxdemo.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
Widget toolkit showcase. Opens multiple windows demonstrating
|
||||
virtually every widget type:
|
||||
|
||||
* Main window -- raw paint callbacks (gradients, patterns, text)
|
||||
* Widget demo -- form widgets (TextInput, Checkbox, Radio, ListBox)
|
||||
* Controls window -- tabbed advanced widgets (Dropdown,
|
||||
ProgressBar, Slider, Spinner, TreeView, ListView, ScrollPane,
|
||||
Toolbar, Canvas, Splitter, Image)
|
||||
* Terminal window -- AnsiTerm widget
|
||||
|
||||
Resources include `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`,
|
||||
`save.bmp`. Reference implementation for widget-based UIs.
|
||||
|
||||
### Control Panel (cpanel)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/cpanel/cpanel.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
System configuration with four tabs:
|
||||
|
||||
* **Mouse** -- scroll direction, wheel speed, double-click speed,
|
||||
acceleration, cursor speed
|
||||
* **Colors** -- every system colour with live preview; theme load
|
||||
and save
|
||||
* **Desktop** -- wallpaper image and display mode
|
||||
* **Video** -- resolution and colour depth switching
|
||||
|
||||
Changes preview live. OK writes to `CONFIG/DVX.INI`; Cancel reverts
|
||||
to the snapshot taken when the panel opened.
|
||||
|
||||
### Image Viewer (imgview, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/imgview/imgview.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Displays BMP, PNG, JPEG, and GIF images scaled to fit the window
|
||||
with aspect ratio preserved. Resize to zoom. Open via the File menu
|
||||
or via launch arguments. Built from a BASIC project using the
|
||||
Canvas widget.
|
||||
|
||||
### DVX BASIC (dvxbasic)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxbasic/dvxbasic.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
Visual Basic 3 compatible IDE: form designer, per-procedure code
|
||||
editor, project manager, compiler, and runtime VM. See
|
||||
`dvxbasic/README.md` for a detailed architecture description.
|
||||
|
||||
`dvxbasic/` also builds:
|
||||
|
||||
* `bin/libs/kpunch/basrt/basrt.lib` -- the BASIC runtime (VM +
|
||||
values) as a separately loadable library, so compiled BASIC apps
|
||||
do not carry their own copy of the runtime.
|
||||
* `bin/apps/kpunch/dvxbasic/basstub.app` -- the stub used as the
|
||||
template for every compiled BASIC app.
|
||||
|
||||
### DVX Help Viewer (dvxhelp)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxhelp/dvxhelp.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | Yes |
|
||||
|
||||
Renders compiled `.hlp` files produced by `dvxhlpc`. Supports table
|
||||
of contents, keyword index, full-text search (trigram), hyperlinks,
|
||||
and inline images.
|
||||
|
||||
### Icon Editor (iconed, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/iconed/iconed.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Paint-program-style editor for creating 32x32 BMP icons. Pen /
|
||||
eraser / fill tools, 16-colour palette, BMP save / load.
|
||||
|
||||
### Help Editor (helpedit, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxhelp/helpedit.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Editor for `.dhs` help source files with syntax-aware controls. Runs
|
||||
the compiled system help viewer by default for live preview.
|
||||
|
||||
### Resource Editor (resedit, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/resedit/resedit.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
GUI wrapper around `dvxres`: open a `.app` / `.wgt` / `.lib`,
|
||||
browse its resources, add, replace, extract, or remove them.
|
||||
|
||||
### BASIC Demo (basdemo, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/basdemo/basdemo.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Gallery of short BASIC programs demonstrating the language and the
|
||||
form designer. Intended as sample source for reading.
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
make # build every app (C apps + BASIC apps)
|
||||
make <appname> # build a single app
|
||||
make clean # remove all app build artifacts
|
||||
```
|
||||
|
||||
The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
|
||||
|
||||
* **C apps** compile each `<app>/<app>.c` to an object file, run
|
||||
`dxe3gen -U` on it to produce `<app>.app`, then attach resources
|
||||
with `dvxres build` if a `.res` manifest exists.
|
||||
* **BASIC apps** run `bascomp <app>.dbp -o <app>.app -release`,
|
||||
which handles project-file parsing, compilation, resource
|
||||
attachment, and linking into `basstub.app`.
|
||||
|
||||
Build output goes to `bin/apps/kpunch/<app>/<app>.app`.
|
||||
|
||||
|
||||
## Writing a New C App
|
||||
|
||||
Minimal skeleton:
|
||||
|
||||
```c
|
||||
#include "dvxApp.h"
|
||||
#include "dvxWgt.h"
|
||||
#include "shellApp.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// Prototypes
|
||||
static int32_t appMainImpl(DxeAppContextT *ctx);
|
||||
static void onClose(WindowT *win);
|
||||
|
||||
// Required descriptor
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "My App",
|
||||
.hasMainLoop = false,
|
||||
.multiInstance = false,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = 0
|
||||
};
|
||||
|
||||
|
||||
static void onClose(WindowT *win) {
|
||||
(void)win;
|
||||
// Callback-only apps: nothing to do; the shell reaps us when
|
||||
// the last window closes.
|
||||
}
|
||||
|
||||
|
||||
int32_t appMain(DxeAppContextT *ctx) {
|
||||
return appMainImpl(ctx);
|
||||
}
|
||||
|
||||
|
||||
static int32_t appMainImpl(DxeAppContextT *ctx) {
|
||||
AppContextT *ac = ctx->shellCtx;
|
||||
|
||||
WindowT *win = dvxCreateWindow(ac, appDescriptor.name, 100, 100, 320, 240, true);
|
||||
|
||||
if (!win) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
win->onClose = onClose;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Add a Makefile target that mirrors the existing apps (see
|
||||
`apps/kpunch/Makefile` for the pattern), drop a `.res` manifest next
|
||||
to the source, and provide a 32x32 `icon32.bmp`. `make <app>` should
|
||||
then produce `bin/apps/kpunch/<app>/<app>.app`.
|
||||
|
||||
|
||||
## Testing Locally
|
||||
|
||||
After `make`, the app is deployed in-tree under `bin/apps/kpunch/`.
|
||||
Launch the full system (`bin/dvx.exe`) and the Program Manager picks
|
||||
up the new app automatically on the next boot.
|
||||
|
||||
To iterate quickly: rebuild the single app, restart DVX, and launch
|
||||
it. The log at `bin/DVX.LOG` records load errors, missing
|
||||
dependencies, and per-app crash diagnostics.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
apps/kpunch/
|
||||
Makefile top-level app build
|
||||
README.md this file
|
||||
|
||||
# C apps
|
||||
progman/
|
||||
progman.c Program Manager
|
||||
dvxhelp.hcf help-compiler config (system reference)
|
||||
clock/
|
||||
clock.c digital clock (main-loop reference)
|
||||
clock.res
|
||||
icon32.bmp
|
||||
dvxdemo/
|
||||
dvxdemo.c widget showcase
|
||||
dvxdemo.res
|
||||
icon32.bmp
|
||||
logo.bmp / new.bmp / open.bmp / sample.bmp / save.bmp
|
||||
cpanel/
|
||||
cpanel.c Control Panel
|
||||
cpanel.res
|
||||
icon32.bmp
|
||||
dvxhelp/
|
||||
dvxhelp.c help viewer
|
||||
dvxhelp.res
|
||||
help.dhs source for the dvxhelp app's own help
|
||||
hlpformat.h binary format shared with the compiler
|
||||
sample.dhs small sample source
|
||||
icon32.bmp
|
||||
helpedit/ BASIC project: help editor
|
||||
helpedit.dbp
|
||||
helpedit.frm
|
||||
ICON32.BMP
|
||||
|
||||
# BASIC apps
|
||||
iconed/
|
||||
iconed.dbp
|
||||
iconed.frm
|
||||
ICON32.BMP
|
||||
notepad/
|
||||
notepad.dbp
|
||||
notepad.frm
|
||||
ICON32.BMP
|
||||
imgview/
|
||||
imgview.dbp
|
||||
imgview.frm
|
||||
ICON32.BMP
|
||||
basdemo/
|
||||
basdemo.dbp
|
||||
basdemo.frm
|
||||
ICON32.BMP
|
||||
resedit/
|
||||
resedit.dbp
|
||||
resedit.frm
|
||||
ICON32.BMP
|
||||
|
||||
dvxbasic/ see dvxbasic/README.md
|
||||
compiler/ lexer, parser, codegen, strip, obfuscate
|
||||
runtime/ VM + value system
|
||||
formrt/ form runtime (BASIC <-> DVX widgets)
|
||||
ide/ IDE front end
|
||||
stub/ bascomp + basstub
|
||||
...
|
||||
```
|
||||
BIN
src/apps/kpunch/basdemo/ICON32.BMP
(Stored with Git LFS)
Normal file
BIN
src/apps/kpunch/basdemo/ICON32.BMP
(Stored with Git LFS)
Normal file
Binary file not shown.
18
src/apps/kpunch/basdemo/basdemo.dbp
Normal file
18
src/apps/kpunch/basdemo/basdemo.dbp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[Project]
|
||||
Name = BASIC Demo
|
||||
Author = Scott Duensing
|
||||
Publisher = Kangaroo Punch Studios
|
||||
Copyright = Copyright 2026 Scott Duensing
|
||||
Description = Comprehensive tour of DVX BASIC features
|
||||
Icon = ICON32.BMP
|
||||
|
||||
[Modules]
|
||||
File0 = ../../../include/basic/commdlg.bas
|
||||
Count = 1
|
||||
|
||||
[Forms]
|
||||
File0 = basdemo.frm
|
||||
Count = 1
|
||||
|
||||
[Settings]
|
||||
StartupForm = BasicDemo
|
||||
879
src/apps/kpunch/basdemo/basdemo.frm
Normal file
879
src/apps/kpunch/basdemo/basdemo.frm
Normal file
|
|
@ -0,0 +1,879 @@
|
|||
VERSION DVX 1.00
|
||||
|
||||
Begin Form BasicDemo
|
||||
Caption = "DVX BASIC Feature Tour"
|
||||
Layout = VBox
|
||||
AutoSize = False
|
||||
Resizable = True
|
||||
Centered = True
|
||||
Width = 600
|
||||
Height = 440
|
||||
|
||||
Begin Menu mnuFile
|
||||
Caption = "&File"
|
||||
Begin Menu mnuClear
|
||||
Caption = "&Clear OutArea"
|
||||
End
|
||||
Begin Menu mnuSaveOut
|
||||
Caption = "&Save OutArea..."
|
||||
End
|
||||
Begin Menu mnuSepF1
|
||||
Caption = "-"
|
||||
End
|
||||
Begin Menu mnuExit
|
||||
Caption = "E&xit"
|
||||
End
|
||||
End
|
||||
|
||||
Begin Menu mnuRun
|
||||
Caption = "&Demos"
|
||||
Begin Menu mnuRunAll
|
||||
Caption = "Run &All Text Demos"
|
||||
End
|
||||
Begin Menu mnuSepR1
|
||||
Caption = "-"
|
||||
End
|
||||
Begin Menu mnuGraphics
|
||||
Caption = "&Graphics..."
|
||||
End
|
||||
Begin Menu mnuDynamic
|
||||
Caption = "&Dynamic Form..."
|
||||
End
|
||||
Begin Menu mnuTimer
|
||||
Caption = "&Timer..."
|
||||
End
|
||||
End
|
||||
|
||||
Begin Menu mnuHelp
|
||||
Caption = "&Help"
|
||||
Begin Menu mnuAbout
|
||||
Caption = "&About..."
|
||||
End
|
||||
End
|
||||
|
||||
Begin Frame fraButtons
|
||||
Caption = "Language Demonstrations"
|
||||
Layout = VBox
|
||||
Weight = 0
|
||||
|
||||
Begin HBox rowA
|
||||
Weight = 0
|
||||
Begin CommandButton btnTypes
|
||||
Caption = "&Types"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnMath
|
||||
Caption = "&Math"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnStrings
|
||||
Caption = "&Strings"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnArrays
|
||||
Caption = "&Arrays"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnData
|
||||
Caption = "&DATA/READ"
|
||||
Weight = 1
|
||||
End
|
||||
End
|
||||
|
||||
Begin HBox rowB
|
||||
Weight = 0
|
||||
Begin CommandButton btnFlow
|
||||
Caption = "Control &Flow"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnUdt
|
||||
Caption = "&UDT"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnOpt
|
||||
Caption = "&Optional"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnError
|
||||
Caption = "&Errors"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnFormat
|
||||
Caption = "F&ormat"
|
||||
Weight = 1
|
||||
End
|
||||
End
|
||||
|
||||
Begin HBox rowC
|
||||
Weight = 0
|
||||
Begin CommandButton btnFileIO
|
||||
Caption = "File &I/O"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnSystem
|
||||
Caption = "S&ystem"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnIni
|
||||
Caption = "I&NI"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnDialogs
|
||||
Caption = "Di&alogs"
|
||||
Weight = 1
|
||||
End
|
||||
Begin CommandButton btnClear
|
||||
Caption = "Clea&r"
|
||||
Weight = 1
|
||||
End
|
||||
End
|
||||
End
|
||||
|
||||
Begin TextArea OutArea
|
||||
Weight = 1
|
||||
End
|
||||
|
||||
Begin Label LblStatus
|
||||
Caption = "Ready. Click any button to run a demo."
|
||||
Weight = 0
|
||||
End
|
||||
End
|
||||
|
||||
' The MIT License (MIT)
|
||||
'
|
||||
' Copyright (C) 2026 Scott Duensing
|
||||
'
|
||||
' Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
' of this software and associated documentation files (the "Software"), to
|
||||
' deal in the Software without restriction, including without limitation the
|
||||
' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
' sell copies of the Software, and to permit persons to whom the Software is
|
||||
' furnished to do so, subject to the following conditions:
|
||||
'
|
||||
' The above copyright notice and this permission notice shall be included in
|
||||
' all copies or substantial portions of the Software.
|
||||
'
|
||||
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
' IN THE SOFTWARE.
|
||||
|
||||
|
||||
OPTION EXPLICIT
|
||||
|
||||
TYPE PointT
|
||||
x AS INTEGER
|
||||
y AS INTEGER
|
||||
END TYPE
|
||||
|
||||
' Module-level state. Grouped here up front (rather than sprinkled
|
||||
' between SUB definitions) so the compiler sees a simple top-level
|
||||
' run-once block followed by a flat list of SUBs / FUNCTIONs.
|
||||
DIM gfxWin AS LONG
|
||||
DIM dynForm AS LONG
|
||||
DIM dynCount AS INTEGER
|
||||
DIM timerWin AS LONG
|
||||
DIM tickCount AS LONG
|
||||
|
||||
gfxWin = 0
|
||||
dynForm = 0
|
||||
dynCount = 0
|
||||
timerWin = 0
|
||||
tickCount = 0
|
||||
|
||||
Load BasicDemo
|
||||
BasicDemo.Show
|
||||
|
||||
OutArea.SetShowLineNumbers False
|
||||
OutArea.SetReadOnly True
|
||||
Say "Welcome to the DVX BASIC Feature Tour!"
|
||||
Say "Each button below runs a self-contained example."
|
||||
Say "Check the Demos menu for graphics, dynamic UI, and timer demos."
|
||||
Say ""
|
||||
|
||||
|
||||
SUB Say(s AS STRING)
|
||||
OutArea.AppendText s + CHR$(10)
|
||||
END SUB
|
||||
|
||||
|
||||
SUB Header(title AS STRING)
|
||||
Say ""
|
||||
Say "--- " + title + " ---"
|
||||
END SUB
|
||||
|
||||
|
||||
SUB mnuClear_Click
|
||||
OutArea.Text = ""
|
||||
LblStatus.Caption = "OutArea cleared."
|
||||
END SUB
|
||||
|
||||
|
||||
SUB mnuSaveOut_Click
|
||||
DIM path AS STRING
|
||||
path = basFileSave("Save OutArea", "Text Files (*.txt)|All Files (*.*)")
|
||||
IF path = "" THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
OPEN path FOR OUTPUT AS #1
|
||||
PRINT #1, OutArea.Text
|
||||
CLOSE #1
|
||||
LblStatus.Caption = "Saved: " + path
|
||||
END SUB
|
||||
|
||||
|
||||
SUB mnuExit_Click
|
||||
Unload BasicDemo
|
||||
END SUB
|
||||
|
||||
|
||||
SUB mnuRunAll_Click
|
||||
btnTypes_Click
|
||||
btnMath_Click
|
||||
btnStrings_Click
|
||||
btnArrays_Click
|
||||
btnData_Click
|
||||
btnFlow_Click
|
||||
btnUdt_Click
|
||||
btnOpt_Click
|
||||
btnError_Click
|
||||
btnFormat_Click
|
||||
btnFileIO_Click
|
||||
btnSystem_Click
|
||||
LblStatus.Caption = "All text demos complete."
|
||||
END SUB
|
||||
|
||||
|
||||
SUB mnuAbout_Click
|
||||
DIM msg AS STRING
|
||||
msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10)
|
||||
msg = msg + "A visual catalog of DVX BASIC language"
|
||||
msg = msg + " and runtime features." + CHR$(10) + CHR$(10)
|
||||
msg = msg + "Copyright 2026 Scott Duensing"
|
||||
MsgBox msg, vbOKOnly, "About"
|
||||
END SUB
|
||||
|
||||
SUB btnTypes_Click
|
||||
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
|
||||
Header "Types"
|
||||
|
||||
DIM i AS INTEGER
|
||||
DIM l AS LONG
|
||||
DIM s AS SINGLE
|
||||
DIM d AS DOUBLE
|
||||
DIM t AS STRING
|
||||
DIM b AS BOOLEAN
|
||||
|
||||
i = 32767
|
||||
l = 2147483647
|
||||
s = 3.14159
|
||||
d = 2.718281828459045
|
||||
t = "Hello, DVX!"
|
||||
b = True
|
||||
|
||||
Say "INTEGER (16-bit): " + STR$(i)
|
||||
Say "LONG (32-bit): " + STR$(l)
|
||||
Say "SINGLE (float): " + STR$(s)
|
||||
Say "DOUBLE (double): " + STR$(d)
|
||||
Say "STRING: " + CHR$(34) + t + CHR$(34)
|
||||
Say "BOOLEAN True: " + STR$(b)
|
||||
|
||||
' CONST with AS type annotation
|
||||
CONST PI AS DOUBLE = 3.1415926535
|
||||
Say "CONST PI = " + STR$(PI)
|
||||
|
||||
LblStatus.Caption = "Types demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnMath_Click
|
||||
' Math: integer + float operators, built-in functions
|
||||
Header "Math"
|
||||
|
||||
DIM a AS INTEGER
|
||||
DIM b AS INTEGER
|
||||
a = 17
|
||||
b = 5
|
||||
|
||||
Say "a = 17, b = 5"
|
||||
Say "a + b = " + STR$(a + b)
|
||||
Say "a - b = " + STR$(a - b)
|
||||
Say "a * b = " + STR$(a * b)
|
||||
Say "a \ b = " + STR$(a \ b) + " (integer divide)"
|
||||
Say "a MOD b = " + STR$(a MOD b)
|
||||
Say "a / b = " + STR$(a / b) + " (float divide)"
|
||||
|
||||
Say ""
|
||||
Say "SQR(144) = " + STR$(SQR(144))
|
||||
Say "ABS(-42) = " + STR$(ABS(-42))
|
||||
Say "INT(3.7) = " + STR$(INT(3.7))
|
||||
Say "FIX(-3.7) = " + STR$(FIX(-3.7))
|
||||
Say "SGN(-9) = " + STR$(SGN(-9))
|
||||
Say "SIN(0) = " + STR$(SIN(0))
|
||||
Say "COS(0) = " + STR$(COS(0))
|
||||
Say "2 ^ 10 = " + STR$(2 ^ 10)
|
||||
|
||||
RANDOMIZE TIMER
|
||||
DIM r AS INTEGER
|
||||
r = INT(RND * 100)
|
||||
Say "RND (0-99) = " + STR$(r)
|
||||
Say "TIMER = " + STR$(TIMER) + " (seconds since midnight)"
|
||||
|
||||
LblStatus.Caption = "Math demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnStrings_Click
|
||||
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
|
||||
Header "Strings"
|
||||
|
||||
DIM s AS STRING
|
||||
s = "The quick brown fox"
|
||||
|
||||
Say "Source: " + CHR$(34) + s + CHR$(34)
|
||||
Say "LEN = " + STR$(LEN(s))
|
||||
Say "LEFT$(s, 3) = " + LEFT$(s, 3)
|
||||
Say "RIGHT$(s, 3) = " + RIGHT$(s, 3)
|
||||
Say "MID$(s, 5, 5) = " + MID$(s, 5, 5)
|
||||
Say "UCASE$ = " + UCASE$(s)
|
||||
Say "LCASE$('HELLO') = " + LCASE$("HELLO")
|
||||
Say "INSTR(s, 'brown')= " + STR$(INSTR(s, "brown"))
|
||||
Say "TRIM$(' hi ') = " + CHR$(34) + TRIM$(" hi ") + CHR$(34)
|
||||
Say "STRING$(5, 42) = " + STRING$(5, 42)
|
||||
Say "CHR$(65) = " + CHR$(65)
|
||||
Say "ASC('A') = " + STR$(ASC("A"))
|
||||
Say "HEX$(255) = " + HEX$(255)
|
||||
Say "VAL('42.5xyz') = " + STR$(VAL("42.5xyz"))
|
||||
|
||||
LblStatus.Caption = "Strings demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnArrays_Click
|
||||
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
|
||||
Header "Arrays"
|
||||
|
||||
' 1D array
|
||||
DIM squares(9) AS INTEGER
|
||||
DIM i AS INTEGER
|
||||
FOR i = 0 TO 9
|
||||
squares(i) = i * i
|
||||
NEXT i
|
||||
|
||||
DIM lineS AS STRING
|
||||
lineS = "squares(0..9) = "
|
||||
FOR i = 0 TO 9
|
||||
lineS = lineS + STR$(squares(i)) + " "
|
||||
NEXT i
|
||||
Say lineS
|
||||
|
||||
' Bounds: DIM a(lo TO hi)
|
||||
DIM prices(1 TO 3) AS SINGLE
|
||||
prices(1) = 9.99
|
||||
prices(2) = 14.99
|
||||
prices(3) = 29.99
|
||||
Say "LBOUND(prices) = " + STR$(LBOUND(prices)) + ", UBOUND = " + STR$(UBOUND(prices))
|
||||
Say "prices(2) = " + STR$(prices(2))
|
||||
|
||||
' 2D array
|
||||
DIM matrix(2, 2) AS INTEGER
|
||||
matrix(0, 0) = 1 : matrix(0, 1) = 2 : matrix(0, 2) = 3
|
||||
matrix(1, 0) = 4 : matrix(1, 1) = 5 : matrix(1, 2) = 6
|
||||
matrix(2, 0) = 7 : matrix(2, 1) = 8 : matrix(2, 2) = 9
|
||||
Say "3x3 matrix:"
|
||||
DIM r AS INTEGER
|
||||
DIM c AS INTEGER
|
||||
FOR r = 0 TO 2
|
||||
lineS = " "
|
||||
FOR c = 0 TO 2
|
||||
lineS = lineS + STR$(matrix(r, c))
|
||||
NEXT c
|
||||
Say lineS
|
||||
NEXT r
|
||||
|
||||
' REDIM PRESERVE
|
||||
DIM nums(2) AS INTEGER
|
||||
nums(0) = 10
|
||||
nums(1) = 20
|
||||
nums(2) = 30
|
||||
REDIM PRESERVE nums(4) AS INTEGER
|
||||
nums(3) = 40
|
||||
nums(4) = 50
|
||||
Say "After REDIM PRESERVE: " + STR$(nums(0)) + " " + STR$(nums(1)) + " " + STR$(nums(2)) + " " + STR$(nums(3)) + " " + STR$(nums(4))
|
||||
|
||||
LblStatus.Caption = "Arrays demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnData_Click
|
||||
' DATA / READ / RESTORE
|
||||
Header "DATA / READ / RESTORE"
|
||||
|
||||
DATA "Red", 255, 0, 0
|
||||
DATA "Green", 0, 255, 0
|
||||
DATA "Blue", 0, 0, 255
|
||||
|
||||
DIM colorName AS STRING
|
||||
DIM r AS INTEGER
|
||||
DIM g AS INTEGER
|
||||
DIM b AS INTEGER
|
||||
DIM i AS INTEGER
|
||||
|
||||
FOR i = 1 TO 3
|
||||
READ colorName
|
||||
READ r
|
||||
READ g
|
||||
READ b
|
||||
Say colorName + ": (" + STR$(r) + "," + STR$(g) + "," + STR$(b) + ")"
|
||||
NEXT i
|
||||
|
||||
Say ""
|
||||
Say "RESTORE resets pointer. Reading first entry again:"
|
||||
RESTORE
|
||||
READ colorName
|
||||
Say " first = " + colorName
|
||||
|
||||
LblStatus.Caption = "DATA/READ demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnFlow_Click
|
||||
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
|
||||
Header "Control Flow"
|
||||
|
||||
' FOR with STEP
|
||||
Say "FOR i = 10 TO 0 STEP -2:"
|
||||
DIM i AS INTEGER
|
||||
DIM lineS AS STRING
|
||||
lineS = " "
|
||||
FOR i = 10 TO 0 STEP -2
|
||||
lineS = lineS + STR$(i)
|
||||
NEXT i
|
||||
Say lineS
|
||||
|
||||
' DO WHILE
|
||||
Say ""
|
||||
Say "DO WHILE n < 32 (doubling):"
|
||||
DIM n AS LONG
|
||||
n = 1
|
||||
lineS = " "
|
||||
DO WHILE n < 32
|
||||
lineS = lineS + STR$(n)
|
||||
n = n * 2
|
||||
LOOP
|
||||
Say lineS
|
||||
|
||||
' IF / ELSEIF / ELSE
|
||||
Say ""
|
||||
DIM score AS INTEGER
|
||||
score = 78
|
||||
IF score >= 90 THEN
|
||||
Say "score " + STR$(score) + " -> A"
|
||||
ELSEIF score >= 80 THEN
|
||||
Say "score " + STR$(score) + " -> B"
|
||||
ELSEIF score >= 70 THEN
|
||||
Say "score " + STR$(score) + " -> C"
|
||||
ELSE
|
||||
Say "score " + STR$(score) + " -> F"
|
||||
END IF
|
||||
|
||||
' SELECT CASE
|
||||
Say ""
|
||||
DIM day AS INTEGER
|
||||
day = 3
|
||||
SELECT CASE day
|
||||
CASE 1
|
||||
Say "day 1 = Monday"
|
||||
CASE 2, 3
|
||||
Say "day " + STR$(day) + " = midweek"
|
||||
CASE 4 TO 5
|
||||
Say "day " + STR$(day) + " = late week"
|
||||
CASE ELSE
|
||||
Say "day " + STR$(day) + " = weekend"
|
||||
END SELECT
|
||||
|
||||
' GOSUB / RETURN
|
||||
Say ""
|
||||
Say "GOSUB to a local label:"
|
||||
GOSUB labelHello
|
||||
Say "back from subroutine"
|
||||
|
||||
LblStatus.Caption = "Control flow demo complete."
|
||||
EXIT SUB
|
||||
|
||||
labelHello:
|
||||
Say " inside GOSUB"
|
||||
RETURN
|
||||
END SUB
|
||||
|
||||
SUB btnUdt_Click
|
||||
' User-Defined Type
|
||||
Header "User-Defined Type"
|
||||
|
||||
DIM p AS PointT
|
||||
p.x = 10
|
||||
p.y = 20
|
||||
Say "PointT p = (" + STR$(p.x) + "," + STR$(p.y) + ")"
|
||||
|
||||
' Array of UDT
|
||||
DIM corners(3) AS PointT
|
||||
corners(0).x = 0 : corners(0).y = 0
|
||||
corners(1).x = 10 : corners(1).y = 0
|
||||
corners(2).x = 10 : corners(2).y = 10
|
||||
corners(3).x = 0 : corners(3).y = 10
|
||||
Say "Rectangle corners:"
|
||||
DIM i AS INTEGER
|
||||
FOR i = 0 TO 3
|
||||
Say " (" + STR$(corners(i).x) + "," + STR$(corners(i).y) + ")"
|
||||
NEXT i
|
||||
|
||||
LblStatus.Caption = "UDT demo complete."
|
||||
END SUB
|
||||
|
||||
FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING
|
||||
IF greeting = "" THEN
|
||||
greeting = "Hello"
|
||||
END IF
|
||||
Greet = greeting + ", " + who + "!"
|
||||
END FUNCTION
|
||||
|
||||
|
||||
SUB btnOpt_Click
|
||||
Header "Optional Parameters"
|
||||
|
||||
Say Greet("World")
|
||||
Say Greet("Scott", "Howdy")
|
||||
Say Greet("DVX", "Greetings from")
|
||||
|
||||
LblStatus.Caption = "Optional params demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnError_Click
|
||||
' ON ERROR GOTO
|
||||
Header "ON ERROR GOTO"
|
||||
|
||||
ON ERROR GOTO handler
|
||||
|
||||
DIM a AS INTEGER
|
||||
DIM b AS INTEGER
|
||||
a = 10
|
||||
b = 0
|
||||
Say "Attempting 10/0 ..."
|
||||
Say " 10 / 0 = " + STR$(a / b)
|
||||
Say "(should not reach here)"
|
||||
EXIT SUB
|
||||
|
||||
handler:
|
||||
Say " caught! ERR = " + STR$(ERR)
|
||||
LblStatus.Caption = "Error handler ran successfully."
|
||||
END SUB
|
||||
|
||||
SUB btnFormat_Click
|
||||
' PRINT USING / FORMAT$
|
||||
Header "Formatting"
|
||||
|
||||
Say "FORMAT$(1234.5, '#,##0.00') = " + FORMAT$(1234.5, "#,##0.00")
|
||||
Say "FORMAT$(0.075, 'percent') = " + FORMAT$(0.075, "percent")
|
||||
Say "FORMAT$(-42, '+#0') = " + FORMAT$(-42, "+#0")
|
||||
Say "FORMAT$(3.14159, '0.00') = " + FORMAT$(3.14159, "0.00")
|
||||
|
||||
LblStatus.Caption = "Format demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnFileIO_Click
|
||||
' File I/O
|
||||
Header "File I/O"
|
||||
|
||||
DIM path AS STRING
|
||||
path = App.Data + "/demo.txt"
|
||||
Say "Writing: " + path
|
||||
|
||||
OPEN path FOR OUTPUT AS #1
|
||||
PRINT #1, "Line one"
|
||||
PRINT #1, "Line two"
|
||||
PRINT #1, "The answer is "; 42
|
||||
CLOSE #1
|
||||
|
||||
Say "LOF = " + STR$(FILELEN(path))
|
||||
|
||||
Say "Reading back:"
|
||||
OPEN path FOR INPUT AS #1
|
||||
DIM ln AS STRING
|
||||
DO WHILE NOT EOF(1)
|
||||
LINE INPUT #1, ln
|
||||
Say " " + ln
|
||||
LOOP
|
||||
CLOSE #1
|
||||
|
||||
KILL path
|
||||
Say "Deleted."
|
||||
|
||||
LblStatus.Caption = "File I/O demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnSystem_Click
|
||||
' System: App object, environment, current directory
|
||||
Header "System / App"
|
||||
|
||||
Say "App.Path = " + App.Path
|
||||
Say "App.Config = " + App.Config
|
||||
Say "App.Data = " + App.Data
|
||||
Say "CurDir = " + CurDir()
|
||||
Say "Date = " + Date$
|
||||
Say "Time = " + Time$
|
||||
Say "PATH env = " + LEFT$(Environ$("PATH"), 40) + "..."
|
||||
|
||||
LblStatus.Caption = "System demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnIni_Click
|
||||
' INI read/write
|
||||
Header "INI Read/Write"
|
||||
|
||||
DIM path AS STRING
|
||||
path = App.Data + "/demo.ini"
|
||||
Say "Writing: " + path
|
||||
|
||||
IniWrite path, "General", "UserName", "Scott"
|
||||
IniWrite path, "General", "Version", "1.00"
|
||||
IniWrite path, "Options", "AutoSave", "True"
|
||||
|
||||
Say "Reading back:"
|
||||
Say " UserName = " + IniRead$(path, "General", "UserName", "(missing)")
|
||||
Say " Version = " + IniRead$(path, "General", "Version", "(missing)")
|
||||
Say " AutoSave = " + IniRead$(path, "Options", "AutoSave", "(missing)")
|
||||
Say " Missing = " + IniRead$(path, "General", "NotThere", "(default)")
|
||||
|
||||
KILL path
|
||||
|
||||
LblStatus.Caption = "INI demo complete."
|
||||
END SUB
|
||||
|
||||
SUB btnDialogs_Click
|
||||
Header "Dialogs"
|
||||
|
||||
DIM response AS INTEGER
|
||||
response = MsgBox("MessageBox demo." + CHR$(10) + "Are you enjoying the demo?", vbYesNo + vbQuestion, "Feedback")
|
||||
IF response = vbYes THEN
|
||||
Say "MsgBox: user said yes"
|
||||
ELSE
|
||||
Say "MsgBox: user said no"
|
||||
END IF
|
||||
|
||||
DIM text AS STRING
|
||||
text = basInputBox2("Input", "What is your name?", "Anonymous")
|
||||
Say "InputBox returned: " + text
|
||||
|
||||
DIM choice AS INTEGER
|
||||
choice = basChoiceDialog("Favorite", "Pick a color:", "Red|Green|Blue|Yellow", 1)
|
||||
IF choice >= 0 THEN
|
||||
Say "Choice index: " + STR$(choice)
|
||||
ELSE
|
||||
Say "Choice cancelled"
|
||||
END IF
|
||||
|
||||
DIM n AS INTEGER
|
||||
n = basIntInput("Number", "Pick a number (1-100):", 42, 1, 100)
|
||||
Say "IntInput: " + STR$(n)
|
||||
|
||||
LblStatus.Caption = "Dialog demo complete."
|
||||
END SUB
|
||||
|
||||
|
||||
SUB btnClear_Click
|
||||
mnuClear_Click
|
||||
END SUB
|
||||
|
||||
SUB mnuGraphics_Click
|
||||
IF gfxWin <> 0 THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
DIM frm AS LONG
|
||||
SET frm = CreateForm("GraphicsForm", 380, 360)
|
||||
GraphicsForm.Caption = "Graphics Demo"
|
||||
gfxWin = frm
|
||||
|
||||
DIM cv AS LONG
|
||||
SET cv = CreateControl(frm, "PictureBox", "GfxCanvas")
|
||||
GfxCanvas.Width = 340
|
||||
GfxCanvas.Height = 260
|
||||
GfxCanvas.Weight = 1
|
||||
|
||||
DIM btnRow AS LONG
|
||||
SET btnRow = CreateControl(frm, "HBox", "GfxRow")
|
||||
|
||||
DIM bDraw AS LONG
|
||||
SET bDraw = CreateControl(frm, "CommandButton", "GfxDraw", btnRow)
|
||||
GfxDraw.Caption = "Draw"
|
||||
SetEvent bDraw, "Click", "GfxDrawAll"
|
||||
|
||||
DIM bClear AS LONG
|
||||
SET bClear = CreateControl(frm, "CommandButton", "GfxClear", btnRow)
|
||||
GfxClear.Caption = "Clear"
|
||||
SetEvent bClear, "Click", "GfxClearCanvas"
|
||||
|
||||
frm.Show
|
||||
|
||||
GfxDrawAll
|
||||
END SUB
|
||||
|
||||
|
||||
SUB GfxDrawAll
|
||||
DIM w AS LONG
|
||||
DIM h AS LONG
|
||||
w = 340
|
||||
h = 260
|
||||
|
||||
' Gradient background bars
|
||||
DIM y AS INTEGER
|
||||
DIM shade AS LONG
|
||||
FOR y = 0 TO h - 1 STEP 4
|
||||
shade = (y * 255) \ h
|
||||
GfxCanvas.FillRect 0, y, w, 4, RGB(shade, shade \ 2, 128)
|
||||
NEXT y
|
||||
|
||||
' Star field
|
||||
RANDOMIZE TIMER
|
||||
DIM s AS INTEGER
|
||||
DIM sx AS INTEGER
|
||||
DIM sy AS INTEGER
|
||||
FOR s = 1 TO 40
|
||||
sx = INT(RND * w)
|
||||
sy = INT(RND * h)
|
||||
GfxCanvas.SetPixel sx, sy, RGB(255, 255, 255)
|
||||
NEXT s
|
||||
|
||||
' Rectangle + outline
|
||||
GfxCanvas.FillRect 20, 20, 80, 50, RGB(240, 240, 0)
|
||||
GfxCanvas.DrawRect 20, 20, 80, 50, RGB(0, 0, 0)
|
||||
|
||||
' Circle approximated by line segments
|
||||
DIM cx AS INTEGER
|
||||
DIM cy AS INTEGER
|
||||
DIM r AS INTEGER
|
||||
cx = 250
|
||||
cy = 80
|
||||
r = 40
|
||||
DIM a AS DOUBLE
|
||||
DIM px AS INTEGER
|
||||
DIM py AS INTEGER
|
||||
DIM qx AS INTEGER
|
||||
DIM qy AS INTEGER
|
||||
px = cx + r
|
||||
py = cy
|
||||
FOR a = 0 TO 6.3 STEP 0.2
|
||||
qx = cx + INT(r * COS(a))
|
||||
qy = cy + INT(r * SIN(a))
|
||||
GfxCanvas.DrawLine px, py, qx, qy, RGB(255, 128, 0)
|
||||
px = qx
|
||||
py = qy
|
||||
NEXT a
|
||||
|
||||
' Text
|
||||
GfxCanvas.DrawText 60, 200, "Canvas + math + colors", RGB(255, 255, 255)
|
||||
GfxCanvas.DrawText 60, 220, "DVX BASIC graphics", RGB(255, 255, 0)
|
||||
|
||||
GfxCanvas.Refresh
|
||||
END SUB
|
||||
|
||||
|
||||
SUB GfxClearCanvas
|
||||
GfxCanvas.Clear RGB(0, 0, 0)
|
||||
GfxCanvas.Refresh
|
||||
END SUB
|
||||
|
||||
|
||||
SUB BasicDemo_Unload
|
||||
' Closing the main form shuts down the whole app, including any
|
||||
' child forms (Graphics, Dynamic, Timer) the user left open.
|
||||
END
|
||||
END SUB
|
||||
|
||||
|
||||
SUB GraphicsForm_Unload
|
||||
gfxWin = 0
|
||||
END SUB
|
||||
|
||||
SUB mnuDynamic_Click
|
||||
IF dynForm <> 0 THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
DIM frm AS LONG
|
||||
SET frm = CreateForm("DynForm", 320, 200)
|
||||
DynForm.Caption = "Dynamic Form (built in code)"
|
||||
dynForm = frm
|
||||
|
||||
DIM lbl AS LONG
|
||||
SET lbl = CreateControl(frm, "Label", "DynLabel")
|
||||
DynLabel.Caption = "This form was created 100% in code."
|
||||
|
||||
DIM lbl2 AS LONG
|
||||
SET lbl2 = CreateControl(frm, "Label", "CountLabel")
|
||||
CountLabel.Caption = "Counter: 0"
|
||||
|
||||
DIM btns AS LONG
|
||||
SET btns = CreateControl(frm, "HBox", "DynBtns")
|
||||
|
||||
DIM bInc AS LONG
|
||||
SET bInc = CreateControl(frm, "CommandButton", "BInc", btns)
|
||||
BInc.Caption = "Count Up"
|
||||
SetEvent bInc, "Click", "DynInc"
|
||||
|
||||
DIM bBye AS LONG
|
||||
SET bBye = CreateControl(frm, "CommandButton", "BBye", btns)
|
||||
BBye.Caption = "Close"
|
||||
SetEvent bBye, "Click", "DynBye"
|
||||
|
||||
frm.Show
|
||||
END SUB
|
||||
|
||||
|
||||
SUB DynInc
|
||||
dynCount = dynCount + 1
|
||||
CountLabel.Caption = "Counter: " + STR$(dynCount)
|
||||
END SUB
|
||||
|
||||
|
||||
SUB DynBye
|
||||
Unload DynForm
|
||||
END SUB
|
||||
|
||||
|
||||
SUB DynForm_Unload
|
||||
dynForm = 0
|
||||
dynCount = 0
|
||||
END SUB
|
||||
|
||||
SUB mnuTimer_Click
|
||||
IF timerWin <> 0 THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
DIM frm AS LONG
|
||||
SET frm = CreateForm("TimerForm", 260, 140)
|
||||
TimerForm.Caption = "Timer Demo"
|
||||
timerWin = frm
|
||||
|
||||
DIM lbl AS LONG
|
||||
SET lbl = CreateControl(frm, "Label", "TickLabel")
|
||||
TickLabel.Caption = "Ticks: 0"
|
||||
|
||||
DIM t AS LONG
|
||||
SET t = CreateControl(frm, "Timer", "Ticker")
|
||||
Ticker.Interval = 500
|
||||
SetEvent t, "Timer", "TickHandler"
|
||||
|
||||
frm.Show
|
||||
END SUB
|
||||
|
||||
|
||||
SUB TickHandler
|
||||
tickCount = tickCount + 1
|
||||
TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$
|
||||
END SUB
|
||||
|
||||
|
||||
SUB TimerForm_Unload
|
||||
timerWin = 0
|
||||
tickCount = 0
|
||||
END SUB
|
||||
|
|
@ -1,3 +1,25 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// clock.c -- Clock DXE application (main-loop with tsYield)
|
||||
//
|
||||
// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback-
|
||||
|
|
@ -19,11 +41,11 @@
|
|||
// would starve the shell and all other apps.
|
||||
|
||||
#include "dvxApp.h"
|
||||
#include "dvxWidget.h"
|
||||
#include "dvxWgt.h"
|
||||
#include "dvxDraw.h"
|
||||
#include "dvxVideo.h"
|
||||
#include "shellApp.h"
|
||||
#include "taskswitch.h"
|
||||
#include "taskSwch.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
|
@ -77,9 +99,15 @@ AppDescriptorT appDescriptor = {
|
|||
.priority = TS_PRIORITY_LOW
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Callbacks (fire in task 0 during dvxUpdate)
|
||||
// ============================================================
|
||||
|
||||
// The shell calls appShutdown (if exported) when force-killing an app via
|
||||
// Task Manager or during shell shutdown. For main-loop apps, this is the
|
||||
// signal to break out of the main loop. Without this, shellForceKillApp
|
||||
// would have to terminate the task forcibly, potentially leaking resources.
|
||||
void appShutdown(void) {
|
||||
sState.quit = true;
|
||||
}
|
||||
|
||||
|
||||
// Setting quit = true tells the main loop (running in a separate task) to
|
||||
// exit. The main loop then destroys the window and returns from appMain,
|
||||
|
|
@ -135,9 +163,6 @@ static void onPaint(WindowT *win, RectT *dirty) {
|
|||
drawText(&cd, ops, font, dateX, dateY, sState.dateStr, colors->contentFg, colors->contentBg, true);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Time update
|
||||
// ============================================================
|
||||
|
||||
static void updateTime(void) {
|
||||
time_t now = time(NULL);
|
||||
|
|
@ -157,26 +182,11 @@ static void updateTime(void) {
|
|||
hour12 = 12;
|
||||
}
|
||||
|
||||
snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", hour12, tm->tm_min, tm->tm_sec, ampm);
|
||||
snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", (int)hour12, tm->tm_min, tm->tm_sec, ampm);
|
||||
snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
|
||||
sState.lastUpdate = now;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Shutdown hook (optional DXE export)
|
||||
// ============================================================
|
||||
|
||||
// The shell calls appShutdown (if exported) when force-killing an app via
|
||||
// Task Manager or during shell shutdown. For main-loop apps, this is the
|
||||
// signal to break out of the main loop. Without this, shellForceKillApp
|
||||
// would have to terminate the task forcibly, potentially leaking resources.
|
||||
void appShutdown(void) {
|
||||
sState.quit = true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Entry point (runs in its own task)
|
||||
// ============================================================
|
||||
|
||||
// This runs in its own task. The shell creates the task before calling
|
||||
// appMain, and reaps it when appMain returns.
|
||||
|
|
@ -203,10 +213,8 @@ int32_t appMain(DxeAppContextT *ctx) {
|
|||
sWin->onClose = onClose;
|
||||
sWin->onPaint = onPaint;
|
||||
|
||||
// Initial paint -- dvxInvalidateWindow calls onPaint automatically
|
||||
dvxInvalidateWindow(ac, sWin);
|
||||
|
||||
// Main loop: check if the second has changed, repaint if so, then yield.
|
||||
// First paint happens automatically on the next dvxUpdate frame.
|
||||
// tsYield() transfers control back to the shell's task scheduler.
|
||||
// On a 486, time() resolution is 1 second, so we yield many times per
|
||||
// second between actual updates -- this keeps CPU usage near zero.
|
||||
7
src/apps/kpunch/clock/clock.res
Normal file
7
src/apps/kpunch/clock/clock.res
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# clock.res -- Resource manifest for Clock
|
||||
icon32 icon icon32.bmp
|
||||
name text "Clock"
|
||||
author text "Scott Duensing"
|
||||
copyright text "Copyright 2026 Scott Duensing"
|
||||
publisher text "Kangaroo Punch Studios"
|
||||
description text "Digital clock with date display"
|
||||
File diff suppressed because it is too large
Load diff
7
src/apps/kpunch/cpanel/cpanel.res
Normal file
7
src/apps/kpunch/cpanel/cpanel.res
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# cpanel.res -- Resource manifest for Control Panel
|
||||
icon32 icon icon32.bmp
|
||||
name text "Control Panel"
|
||||
author text "Scott Duensing"
|
||||
copyright text "Copyright 2026 Scott Duensing"
|
||||
publisher text "Kangaroo Punch Studios"
|
||||
description text "System settings and preferences"
|
||||
233
src/apps/kpunch/dvxbasic/Makefile
Normal file
233
src/apps/kpunch/dvxbasic/Makefile
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
# DVX BASIC Makefile for DJGPP cross-compilation
|
||||
#
|
||||
# Builds:
|
||||
# basrt.lib -- BASIC runtime library (VM + values + dlregsym init)
|
||||
# dvxbasic.app -- BASIC IDE (compiler + UI, links against basrt.lib)
|
||||
#
|
||||
# The runtime is a separate library so compiled BASIC apps can use
|
||||
# it without including the compiler. The library's constructor calls
|
||||
# dlregsym() to register its exports, making them available to DXEs
|
||||
# loaded later (apps, widgets, etc.).
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks -I../../../libs/kpunch/libdvx/thirdparty -I.
|
||||
|
||||
OBJDIR = ../../../../obj/dvxbasic
|
||||
LIBSDIR = ../../../../bin/libs
|
||||
APPDIR = ../../../../bin/apps/kpunch/dvxbasic
|
||||
HOSTDIR = ../../../../bin/host
|
||||
DVXRES = $(HOSTDIR)/dvxres
|
||||
|
||||
# Runtime library objects (VM + values + form runtime + serialization)
|
||||
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/serialize.o
|
||||
RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
|
||||
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
|
||||
|
||||
# Compiler objects (only needed by the IDE)
|
||||
COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o $(OBJDIR)/obfuscate.o $(OBJDIR)/compact.o $(OBJDIR)/basBuild.o
|
||||
|
||||
# IDE app objects
|
||||
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
|
||||
APP_OBJS = $(IDE_OBJS)
|
||||
APP_TARGET = $(APPDIR)/dvxbasic.app
|
||||
|
||||
# Standalone stub (embedded as IDE resource)
|
||||
STUB_OBJS = $(OBJDIR)/basstub.o
|
||||
STUB_TARGET = $(OBJDIR)/basstub.app
|
||||
|
||||
# Native test programs (host gcc, not cross-compiled)
|
||||
HOSTCC = gcc
|
||||
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -D_GNU_SOURCE -I. -I../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../libs/kpunch/libdvx/thirdparty
|
||||
|
||||
TEST_COMPILER = $(HOSTDIR)/test_compiler
|
||||
TEST_VM = $(HOSTDIR)/test_vm
|
||||
TEST_LEX = $(HOSTDIR)/test_lex
|
||||
TEST_QUICK = $(HOSTDIR)/test_quick
|
||||
TEST_COMPACT = $(HOSTDIR)/test_compact
|
||||
TEST_SUITE = $(HOSTDIR)/test_suite
|
||||
|
||||
STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
|
||||
PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
|
||||
TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
|
||||
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
TEST_SUITE_SRCS = test_suite.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
|
||||
# Command-line compiler (host tool)
|
||||
BASCOMP_SRCS = stub/bascomp.c basBuild.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../../libs/kpunch/libdvx/dvxPrefs.c ../../../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
BASCOMP_TARGET = $(HOSTDIR)/bascomp
|
||||
|
||||
# DOS command-line compiler
|
||||
DOSCC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DOSCFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../libs/kpunch/libdvx/thirdparty -I.
|
||||
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
|
||||
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
|
||||
SYSTEMDIR = ../../../../bin/system
|
||||
|
||||
.PHONY: all clean tests
|
||||
|
||||
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE
|
||||
|
||||
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE)
|
||||
$(TEST_SUITE)
|
||||
|
||||
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
|
||||
|
||||
$(TEST_SUITE): $(TEST_SUITE_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_SUITE_SRCS) -lm
|
||||
|
||||
$(TEST_VM): $(TEST_VM_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm
|
||||
|
||||
$(TEST_LEX): $(TEST_LEX_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -w -o $@ $(TEST_LEX_SRCS) -lm
|
||||
|
||||
$(TEST_QUICK): $(TEST_QUICK_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm
|
||||
|
||||
$(TEST_COMPACT): $(TEST_COMPACT_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPACT_SRCS) -lm
|
||||
|
||||
# Host command-line compiler -- basstub.app is appended as a STUB
|
||||
# resource so bascomp is self-contained (no BASSTUB.APP companion file).
|
||||
$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(STUB_TARGET) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $@ $(BASCOMP_SRCS) -lm
|
||||
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
||||
|
||||
# DOS command-line compiler (same STUB embed as the host build)
|
||||
$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) $(STUB_TARGET) | $(SYSTEMDIR)
|
||||
$(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm
|
||||
$(EXE2COFF) $(SYSTEMDIR)/bascomp.exe
|
||||
cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@
|
||||
rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe
|
||||
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
||||
|
||||
$(HOSTDIR):
|
||||
mkdir -p $(HOSTDIR)
|
||||
|
||||
$(SYSTEMDIR):
|
||||
mkdir -p $(SYSTEMDIR)
|
||||
|
||||
# Runtime library DXE (exports symbols via dlregsym constructor)
|
||||
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
|
||||
$(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS)
|
||||
mv $(RT_TARGETDIR)/basrt.dxe $@
|
||||
|
||||
$(RT_TARGETDIR)/basrt.dep: basrt.dep | $(RT_TARGETDIR)
|
||||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
# Standalone stub DXE (embedded as resource in IDE app)
|
||||
$(STUB_TARGET): $(STUB_OBJS) | $(APPDIR)
|
||||
$(DXE3GEN) -o $@ -U $(STUB_OBJS)
|
||||
|
||||
# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded)
|
||||
$(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR)
|
||||
$(DXE3GEN) -o $@ -U $(COMP_OBJS) $(APP_OBJS)
|
||||
$(DVXRES) build $@ dvxbasic.res
|
||||
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
||||
|
||||
|
||||
# Object files
|
||||
$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h formrt/frmParser.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/frmParser.o: formrt/frmParser.c formrt/frmParser.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/obfuscate.o: compiler/obfuscate.c compiler/obfuscate.h runtime/vm.h runtime/values.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/compact.o: compiler/compact.c compiler/compact.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/basBuild.o: basBuild.c basBuild.h basRes.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideMenuEditor.h ide/ideProject.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/lexer.o: compiler/lexer.c compiler/lexer.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/parser.o: compiler/parser.c compiler/parser.h compiler/lexer.h compiler/codegen.h compiler/symtab.h compiler/opcodes.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/symtab.o: compiler/symtab.c compiler/symtab.h compiler/opcodes.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/values.o: runtime/values.c runtime/values.h compiler/opcodes.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/vm.o: runtime/vm.c runtime/vm.h runtime/values.h compiler/opcodes.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
# Directories
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBSDIR):
|
||||
mkdir -p $(LIBSDIR)
|
||||
|
||||
$(RT_TARGETDIR):
|
||||
mkdir -p $(RT_TARGETDIR)
|
||||
|
||||
$(APPDIR):
|
||||
mkdir -p $(APPDIR)
|
||||
|
||||
clean:
|
||||
rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o
|
||||
rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT)
|
||||
55
src/apps/kpunch/dvxbasic/README.md
Normal file
55
src/apps/kpunch/dvxbasic/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# DVX BASIC
|
||||
|
||||
A Visual Basic 3 clone for the DVX GUI system. Provides a visual form
|
||||
designer, per-procedure code editor, project management, and a stack-
|
||||
based bytecode compiler and VM for running event-driven BASIC programs.
|
||||
|
||||
## Documentation
|
||||
|
||||
For BASIC authors, the complete user reference is split across several
|
||||
`.dhs` help sources in this directory, all compiled into the DVX BASIC
|
||||
Reference:
|
||||
|
||||
- `langref.dhs` -- language reference: data types, keywords, operators,
|
||||
control flow, built-in functions, I/O, error codes.
|
||||
- `ideguide.dhs` -- IDE guide: menus, toolbar, project model, form
|
||||
designer, code editor, debugger, preferences.
|
||||
- `ctrlover.dhs` -- control overview: common widget properties and
|
||||
events from a BASIC author's perspective.
|
||||
- `form.dhs` -- form lifecycle, properties, events.
|
||||
- `basrt.dhs` -- BASIC runtime reference: DECLARE LIBRARY mechanism,
|
||||
shipped include files.
|
||||
|
||||
After `make`, the compiled form is `bin/apps/kpunch/dvxbasic/dvxbasic.hlp`
|
||||
(viewable via the Help Viewer app or from within the IDE via F1) and
|
||||
`docs/dvx_basic_reference.html`.
|
||||
|
||||
## Subdirectories (for maintainers)
|
||||
|
||||
- `compiler/` -- lexer, parser, codegen, symbol table, opcodes.
|
||||
Single-pass compiler that translates BASIC source to stack-based
|
||||
p-code.
|
||||
- `runtime/` -- VM and tagged value system (values.c, vm.c, serialize.c).
|
||||
- `formrt/` -- form runtime, the bridge between the BASIC VM and the
|
||||
DVX widget system.
|
||||
- `ide/` -- the IDE itself (main, designer, project, properties,
|
||||
toolbox, menu editor).
|
||||
- `stub/` -- `basstub` (runtime loader for compiled BASIC `.app`
|
||||
files) and `bascomp` (standalone command-line compiler).
|
||||
|
||||
## Project Files
|
||||
|
||||
project.dbp INI-format project file
|
||||
module.bas BASIC module (code only)
|
||||
form.frm Form file (layout + code after the form's End)
|
||||
|
||||
`.bas` modules compile before `.frm` code sections, so CONST
|
||||
declarations are visible to form event handlers.
|
||||
|
||||
## Build
|
||||
|
||||
make -C src/apps/kpunch/dvxbasic # cross-compile for DOS
|
||||
make -C src/apps/kpunch/dvxbasic tests # build native test programs
|
||||
|
||||
Test programs: `test_compiler`, `test_vm`, `test_lex`, `test_quick`,
|
||||
`test_compact`.
|
||||
138
src/apps/kpunch/dvxbasic/basBuild.c
Normal file
138
src/apps/kpunch/dvxbasic/basBuild.c
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// basBuild.c -- shared "emit .app resource set" step
|
||||
//
|
||||
// See basBuild.h for the public contract. This file used to live inline
|
||||
// in two places: ideMain.c (the IDE's Make Executable path) and
|
||||
// bascomp.c (the standalone command-line compiler). Both implementations
|
||||
// were essentially identical, so they are now consolidated here.
|
||||
//
|
||||
// The canonical emit order is:
|
||||
// 1. BAS_RES_NAME (always, falls back to "BASIC App")
|
||||
// 2. BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION
|
||||
// (each skipped if empty)
|
||||
// 3. BAS_RES_ICON32 (file via iconPath, else iconData bytes)
|
||||
// 4. BAS_RES_HELPFILE (filename only, if helpFile set)
|
||||
// 5. BAS_RES_MODULE (bytecode)
|
||||
// 6. BAS_RES_DEBUG (optional)
|
||||
// 7. FORM0, FORM1, ... (one per spec->formCount)
|
||||
|
||||
#include "basBuild.h"
|
||||
#include "basRes.h"
|
||||
#include "../../../libs/kpunch/libdvx/dvxRes.h"
|
||||
#include "../../../libs/kpunch/libdvx/platform/dvxPlat.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ------------------------------------------------------------
|
||||
|
||||
|
||||
static void appendText(const char *path, const char *name, const char *value) {
|
||||
if (!value || !value[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1);
|
||||
}
|
||||
|
||||
|
||||
static void emitIcon(const char *path, const BasBuildSpecT *spec) {
|
||||
// If a disk path was given, load it and embed. Otherwise use the
|
||||
// pre-loaded bytes (used by the IDE for its "noicon" fallback).
|
||||
if (spec->iconPath && spec->iconPath[0]) {
|
||||
int32_t iconLen = 0;
|
||||
char *iconData = platformReadFile(spec->iconPath, &iconLen);
|
||||
|
||||
if (iconData) {
|
||||
dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
|
||||
free(iconData);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (spec->iconData && spec->iconSize > 0) {
|
||||
dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, spec->iconData, (uint32_t)spec->iconSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Public API
|
||||
// ------------------------------------------------------------
|
||||
|
||||
|
||||
int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
|
||||
if (!outPath || !spec) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Project metadata. Name is required -- fall back to a generic label
|
||||
// so the compiled app always has something sensible to display.
|
||||
const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : "BASIC App";
|
||||
appendText(outPath, BAS_RES_NAME, projName);
|
||||
|
||||
appendText(outPath, BAS_RES_AUTHOR, spec->author);
|
||||
appendText(outPath, BAS_RES_PUBLISHER, spec->publisher);
|
||||
appendText(outPath, BAS_RES_VERSION, spec->version);
|
||||
appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
|
||||
appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
|
||||
|
||||
// Icon.
|
||||
emitIcon(outPath, spec);
|
||||
|
||||
// Help file name (just the basename -- stub resolves it next to the app).
|
||||
if (spec->helpFile && spec->helpFile[0]) {
|
||||
const char *helpBase = platformPathBaseName(spec->helpFile);
|
||||
appendText(outPath, BAS_RES_HELPFILE, helpBase);
|
||||
}
|
||||
|
||||
// Bytecode module.
|
||||
if (spec->moduleData && spec->moduleLen > 0) {
|
||||
dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, spec->moduleData, (uint32_t)spec->moduleLen);
|
||||
}
|
||||
|
||||
// Optional debug info.
|
||||
if (spec->debugData && spec->debugLen > 0) {
|
||||
dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, spec->debugData, (uint32_t)spec->debugLen);
|
||||
}
|
||||
|
||||
// Form resources. Callers pre-strip / pre-obfuscate as needed; we just
|
||||
// write them out as FORM0, FORM1, ...
|
||||
for (int32_t i = 0; i < spec->formCount; i++) {
|
||||
if (!spec->formData || !spec->formData[i] || !spec->formLens || spec->formLens[i] <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char resName[16];
|
||||
snprintf(resName, sizeof(resName), "FORM%ld", (long)i);
|
||||
dvxResAppend(outPath, resName, DVX_RES_BINARY, spec->formData[i], (uint32_t)spec->formLens[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
87
src/apps/kpunch/dvxbasic/basBuild.h
Normal file
87
src/apps/kpunch/dvxbasic/basBuild.h
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// basBuild.h -- shared "emit .app resource set" step
|
||||
//
|
||||
// Both the DVX BASIC IDE (ideMain.c) and the standalone command-line
|
||||
// compiler (bascomp.c) need to attach the same group of resources to
|
||||
// an output .app file after writing the stub DXE. This header exposes
|
||||
// a single function that takes the metadata, bytecode, debug info and
|
||||
// form texts and appends all resources in the canonical order.
|
||||
//
|
||||
// Callers are still responsible for:
|
||||
// - writing the stub to outPath before calling us
|
||||
// - copying the .hlp file next to outPath (if any)
|
||||
// - freeing any buffers they passed in via BasBuildSpecT
|
||||
//
|
||||
// Resource names use BAS_RES_* from basRes.h so both callers stay in
|
||||
// sync automatically.
|
||||
|
||||
#ifndef BAS_BUILD_H
|
||||
#define BAS_BUILD_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
// Basic metadata (any NULL/empty is skipped).
|
||||
const char *projName;
|
||||
const char *author;
|
||||
const char *publisher;
|
||||
const char *version;
|
||||
const char *copyright;
|
||||
const char *description;
|
||||
const char *helpFile; // just the filename, not a path
|
||||
|
||||
// Icon: either a file to load and embed, OR pre-loaded bytes.
|
||||
// If iconPath is set (non-NULL and non-empty) it is read from disk.
|
||||
// Otherwise iconData / iconSize are used (may themselves be NULL/0).
|
||||
const char *iconPath;
|
||||
const void *iconData;
|
||||
int32_t iconSize;
|
||||
|
||||
// Bytecode module and optional debug info.
|
||||
const void *moduleData;
|
||||
int32_t moduleLen;
|
||||
const void *debugData; // may be NULL
|
||||
int32_t debugLen;
|
||||
|
||||
// Forms: parallel arrays of formCount entries, each a text blob.
|
||||
// formData[i] points at formLens[i] bytes of (already stripped /
|
||||
// possibly obfuscated) form source to embed as resource FORMi.
|
||||
int32_t formCount;
|
||||
const uint8_t *const *formData;
|
||||
const int32_t *formLens;
|
||||
} BasBuildSpecT;
|
||||
|
||||
// Append the full resource set to outPath. Returns 0 on success, non-zero
|
||||
// on failure. The stub must already have been written to outPath.
|
||||
int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // BAS_BUILD_H
|
||||
79
src/apps/kpunch/dvxbasic/basRes.h
Normal file
79
src/apps/kpunch/dvxbasic/basRes.h
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// basRes.h -- single source of truth for BASIC project / binary layout
|
||||
//
|
||||
// Keys used in .dbp project files and names of resources written into the
|
||||
// compiled .app DXE. These were previously scattered as raw string literals
|
||||
// across the IDE (ideMain.c, ideProject.c) and the standalone compiler
|
||||
// (bascomp.c), making it easy for one side to drift from the other.
|
||||
// All readers and writers now include this header.
|
||||
|
||||
#ifndef BAS_RES_H
|
||||
#define BAS_RES_H
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// [Project] section of a .dbp file
|
||||
// ------------------------------------------------------------
|
||||
|
||||
#define BAS_INI_SECTION_PROJECT "Project"
|
||||
#define BAS_INI_KEY_NAME "Name"
|
||||
#define BAS_INI_KEY_AUTHOR "Author"
|
||||
#define BAS_INI_KEY_PUBLISHER "Publisher"
|
||||
#define BAS_INI_KEY_VERSION "Version"
|
||||
#define BAS_INI_KEY_COPYRIGHT "Copyright"
|
||||
#define BAS_INI_KEY_DESCRIPTION "Description"
|
||||
#define BAS_INI_KEY_ICON "Icon"
|
||||
#define BAS_INI_KEY_HELPFILE "HelpFile"
|
||||
|
||||
// [Settings] / [Modules] / [Forms] sections of a .dbp file
|
||||
#define BAS_INI_SECTION_SETTINGS "Settings"
|
||||
#define BAS_INI_KEY_STARTUPFORM "StartupForm"
|
||||
#define BAS_INI_KEY_OPTIONEXPLICIT "OptionExplicit"
|
||||
#define BAS_INI_SECTION_MODULES "Modules"
|
||||
#define BAS_INI_SECTION_FORMS "Forms"
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Resource names inside a compiled .app DXE
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// Text metadata -- mirrors the [Project] keys above but lowercase
|
||||
// (resource names are case-sensitive; apps query these at runtime).
|
||||
#define BAS_RES_NAME "name"
|
||||
#define BAS_RES_AUTHOR "author"
|
||||
#define BAS_RES_PUBLISHER "publisher"
|
||||
#define BAS_RES_VERSION "version"
|
||||
#define BAS_RES_COPYRIGHT "copyright"
|
||||
#define BAS_RES_DESCRIPTION "description"
|
||||
#define BAS_RES_HELPFILE "helpfile"
|
||||
|
||||
// Icon -- 32x32 application icon
|
||||
#define BAS_RES_ICON32 "icon32"
|
||||
|
||||
// Binary payload -- bytecode + debug info
|
||||
#define BAS_RES_MODULE "MODULE"
|
||||
#define BAS_RES_DEBUG "DEBUG"
|
||||
|
||||
// Stub DXE embedded in the IDE app for release builds
|
||||
#define BAS_RES_STUB "STUB"
|
||||
|
||||
#endif // BAS_RES_H
|
||||
558
src/apps/kpunch/dvxbasic/basrt.dhs
Normal file
558
src/apps/kpunch/dvxbasic/basrt.dhs
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
.section Libraries
|
||||
.topic lib.basrt
|
||||
.title BASIC Runtime Library
|
||||
.toc 0 BASIC Runtime Library
|
||||
.index BASIC Runtime
|
||||
.index BasVmT
|
||||
.index BasValueT
|
||||
.index BasStringT
|
||||
.index BasArrayT
|
||||
.index BasModuleT
|
||||
|
||||
.h1 BASIC Runtime Library
|
||||
|
||||
Stack-based p-code virtual machine and value system for DVX BASIC. Embeddable: the host provides I/O and UI callbacks. No DVX GUI dependencies in the core runtime.
|
||||
|
||||
Headers: apps/dvxbasic/runtime/vm.h, apps/dvxbasic/runtime/values.h
|
||||
|
||||
.link lib.basrt.values Value System (values.h)
|
||||
.link lib.basrt.vm Virtual Machine (vm.h)
|
||||
|
||||
.topic lib.basrt.values
|
||||
.title Value System
|
||||
.toc 1 Value System
|
||||
.index BasValueT
|
||||
.index BasStringT
|
||||
.index BasArrayT
|
||||
.index BasUdtT
|
||||
.index basStringNew
|
||||
.index basArrayNew
|
||||
.index basValCopy
|
||||
.index basValRelease
|
||||
|
||||
.h1 Value System
|
||||
|
||||
Tagged union value type for the VM evaluation stack, variables, and array elements. Strings, arrays, and UDT instances are reference-counted for automatic memory management without a garbage collector.
|
||||
|
||||
Header: apps/dvxbasic/runtime/values.h
|
||||
|
||||
.h2 Type Tags
|
||||
|
||||
.table
|
||||
Constant Value C Union Field Description
|
||||
-------- ----- ------------- -----------
|
||||
BAS_TYPE_INTEGER 0 intVal 16-bit signed integer.
|
||||
BAS_TYPE_LONG 1 longVal 32-bit signed integer.
|
||||
BAS_TYPE_SINGLE 2 sngVal 32-bit float.
|
||||
BAS_TYPE_DOUBLE 3 dblVal 64-bit float.
|
||||
BAS_TYPE_STRING 4 strVal Reference-counted dynamic string.
|
||||
BAS_TYPE_BOOLEAN 5 boolVal True (-1) or False (0).
|
||||
BAS_TYPE_ARRAY 6 arrVal Reference-counted array.
|
||||
BAS_TYPE_UDT 7 udtVal Reference-counted user-defined type.
|
||||
BAS_TYPE_OBJECT 8 objVal Opaque host pointer (form, control).
|
||||
BAS_TYPE_REF 9 refVal ByRef pointer to a BasValueT slot.
|
||||
.endtable
|
||||
|
||||
.h2 BasValueT
|
||||
|
||||
Tagged union holding any BASIC value.
|
||||
|
||||
.code
|
||||
struct BasValueTag {
|
||||
uint8_t type; // BAS_TYPE_*
|
||||
union {
|
||||
int16_t intVal;
|
||||
int32_t longVal;
|
||||
float sngVal;
|
||||
double dblVal;
|
||||
BasStringT *strVal;
|
||||
int16_t boolVal;
|
||||
BasArrayT *arrVal;
|
||||
BasUdtT *udtVal;
|
||||
void *objVal;
|
||||
BasValueT *refVal;
|
||||
};
|
||||
};
|
||||
.endcode
|
||||
|
||||
.h2 Value Constructors
|
||||
|
||||
.code
|
||||
BasValueT basValInteger(int16_t v);
|
||||
BasValueT basValLong(int32_t v);
|
||||
BasValueT basValSingle(float v);
|
||||
BasValueT basValDouble(double v);
|
||||
BasValueT basValString(BasStringT *s);
|
||||
BasValueT basValStringFromC(const char *text);
|
||||
BasValueT basValBool(bool v);
|
||||
BasValueT basValObject(void *obj);
|
||||
.endcode
|
||||
|
||||
Each returns a BasValueT with the appropriate type tag set. basValString increments the string's reference count.
|
||||
|
||||
.h2 Value Lifetime
|
||||
|
||||
.code
|
||||
BasValueT basValCopy(BasValueT v);
|
||||
void basValRelease(BasValueT *v);
|
||||
.endcode
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basValCopy Copy a value. Increments reference count for strings, arrays, and UDTs.
|
||||
basValRelease Release a value. Decrements reference count and frees if it reaches zero.
|
||||
.endtable
|
||||
|
||||
.h2 Type Conversion
|
||||
|
||||
.code
|
||||
BasValueT basValToInteger(BasValueT v);
|
||||
BasValueT basValToLong(BasValueT v);
|
||||
BasValueT basValToSingle(BasValueT v);
|
||||
BasValueT basValToDouble(BasValueT v);
|
||||
BasValueT basValToString(BasValueT v);
|
||||
BasValueT basValToBool(BasValueT v);
|
||||
.endcode
|
||||
|
||||
Each returns a new value of the target type. The original is not released; the caller manages both lifetimes.
|
||||
|
||||
.h2 Value Utilities
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basValToNumber(v) Convert any numeric value to double.
|
||||
basValFormatString(v) Return a new ref-counted string representation of v.
|
||||
basValIsTruthy(v) True if non-zero number or non-empty string.
|
||||
basValCompare(a, b) Compare two values. Returns -1, 0, or 1.
|
||||
basValCompareCI(a, b) Case-insensitive comparison (OPTION COMPARE TEXT).
|
||||
basValPromoteType(a, b) Determine common type for binary ops (e.g. Integer + Single -> Single).
|
||||
.endtable
|
||||
|
||||
.h2 BasStringT
|
||||
|
||||
Reference-counted string with flexible array member for inline storage.
|
||||
|
||||
.code
|
||||
typedef struct {
|
||||
int32_t refCount;
|
||||
int32_t len;
|
||||
int32_t cap;
|
||||
char data[];
|
||||
} BasStringT;
|
||||
.endcode
|
||||
|
||||
.h3 String Functions
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basStringNew(text, len) Allocate from a C string. refCount starts at 1.
|
||||
basStringAlloc(cap) Allocate an empty string with given capacity.
|
||||
basStringRef(s) Increment reference count. Returns s.
|
||||
basStringUnref(s) Decrement reference count. Frees when it reaches zero.
|
||||
basStringConcat(a, b) Concatenate two strings. Returns a new string (refCount 1).
|
||||
basStringSub(s, start, len) Extract a substring. Returns a new string (refCount 1).
|
||||
basStringCompare(a, b) Compare. Returns <0, 0, >0 (like strcmp).
|
||||
basStringCompareCI(a, b) Case-insensitive compare.
|
||||
basStringSystemInit() Initialize the string system and empty string singleton.
|
||||
basStringSystemShutdown() Shut down the string system.
|
||||
.endtable
|
||||
|
||||
The global basEmptyString is a singleton that is never freed.
|
||||
|
||||
.h2 BasArrayT
|
||||
|
||||
Reference-counted multi-dimensional array (up to BAS_ARRAY_MAX_DIMS = 8 dimensions).
|
||||
|
||||
.code
|
||||
typedef struct {
|
||||
int32_t refCount;
|
||||
uint8_t elementType;
|
||||
int32_t dims;
|
||||
int32_t lbound[BAS_ARRAY_MAX_DIMS];
|
||||
int32_t ubound[BAS_ARRAY_MAX_DIMS];
|
||||
int32_t totalElements;
|
||||
BasValueT *elements;
|
||||
} BasArrayT;
|
||||
.endcode
|
||||
|
||||
.h3 Array Functions
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basArrayNew(dims, lbounds, ubounds, type) Allocate an array. refCount starts at 1.
|
||||
basArrayFree(arr) Free all elements and release the array.
|
||||
basArrayRef(arr) Increment reference count.
|
||||
basArrayUnref(arr) Decrement reference count. Frees at zero.
|
||||
basArrayIndex(arr, indices, ndims) Compute flat index from multi-dimensional indices. Returns -1 if out of bounds.
|
||||
.endtable
|
||||
|
||||
.h2 BasUdtT
|
||||
|
||||
Reference-counted user-defined type instance.
|
||||
|
||||
.code
|
||||
typedef struct {
|
||||
int32_t refCount;
|
||||
int32_t typeId;
|
||||
int32_t fieldCount;
|
||||
BasValueT *fields;
|
||||
} BasUdtT;
|
||||
.endcode
|
||||
|
||||
.h3 UDT Functions
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basUdtNew(typeId, fieldCount) Allocate a UDT instance. refCount starts at 1.
|
||||
basUdtFree(udt) Free all fields and release.
|
||||
basUdtRef(udt) Increment reference count.
|
||||
basUdtUnref(udt) Decrement reference count. Frees at zero.
|
||||
.endtable
|
||||
|
||||
.topic lib.basrt.vm
|
||||
.title Virtual Machine
|
||||
.toc 1 Virtual Machine
|
||||
.index BasVmT
|
||||
.index BasModuleT
|
||||
.index BasVmResultE
|
||||
.index basVmCreate
|
||||
.index basVmRun
|
||||
.index basVmStep
|
||||
.index basVmDestroy
|
||||
.index basVmCallSub
|
||||
|
||||
.h1 Virtual Machine
|
||||
|
||||
Stack-based p-code interpreter. Executes compiled BASIC bytecode modules. The host provides I/O, UI, SQL, and external library callbacks. The VM has no DVX dependencies; it can be embedded in any C program.
|
||||
|
||||
Header: apps/dvxbasic/runtime/vm.h
|
||||
|
||||
.h2 VM Limits
|
||||
|
||||
.table
|
||||
Constant Value Description
|
||||
-------- ----- -----------
|
||||
BAS_VM_STACK_SIZE 256 Evaluation stack depth.
|
||||
BAS_VM_CALL_STACK_SIZE 64 Maximum call nesting depth.
|
||||
BAS_VM_MAX_GLOBALS 512 Global variable slots.
|
||||
BAS_VM_MAX_LOCALS 64 Local variables per stack frame.
|
||||
BAS_VM_MAX_FOR_DEPTH 32 Maximum nested FOR loop depth.
|
||||
BAS_VM_MAX_FILES 16 Open file channels (1-based).
|
||||
.endtable
|
||||
|
||||
.h2 BasVmResultE
|
||||
|
||||
Result codes returned by basVmRun and basVmStep.
|
||||
|
||||
.table
|
||||
Code Value Description
|
||||
---- ----- -----------
|
||||
BAS_VM_OK 0 Program completed normally.
|
||||
BAS_VM_HALTED 1 HALT instruction reached.
|
||||
BAS_VM_YIELDED 2 DoEvents yielded control.
|
||||
BAS_VM_ERROR 3 Runtime error.
|
||||
BAS_VM_STACK_OVERFLOW 4 Evaluation stack overflow.
|
||||
BAS_VM_STACK_UNDERFLOW 5 Evaluation stack underflow.
|
||||
BAS_VM_CALL_OVERFLOW 6 Call stack overflow.
|
||||
BAS_VM_DIV_BY_ZERO 7 Division by zero.
|
||||
BAS_VM_TYPE_MISMATCH 8 Type mismatch in operation.
|
||||
BAS_VM_OUT_OF_MEMORY 9 Memory allocation failed.
|
||||
BAS_VM_BAD_OPCODE 10 Unknown opcode encountered.
|
||||
BAS_VM_FILE_ERROR 11 File I/O error.
|
||||
BAS_VM_SUBSCRIPT_RANGE 12 Array subscript out of range.
|
||||
BAS_VM_USER_ERROR 13 ON ERROR raised by program.
|
||||
BAS_VM_STEP_LIMIT 14 Step limit reached (not an error).
|
||||
BAS_VM_BREAKPOINT 15 Breakpoint or step completed (not an error).
|
||||
.endtable
|
||||
|
||||
.h2 Lifecycle
|
||||
|
||||
.code
|
||||
BasVmT *basVmCreate(void);
|
||||
void basVmDestroy(BasVmT *vm);
|
||||
void basVmLoadModule(BasVmT *vm, BasModuleT *module);
|
||||
void basVmReset(BasVmT *vm);
|
||||
.endcode
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basVmCreate Allocate and initialize a new VM instance.
|
||||
basVmDestroy Destroy the VM and free all resources.
|
||||
basVmLoadModule Load a compiled module (BasModuleT) into the VM.
|
||||
basVmReset Reset to initial state (clear stack, globals, PC).
|
||||
.endtable
|
||||
|
||||
.h2 Execution
|
||||
|
||||
.code
|
||||
BasVmResultE basVmRun(BasVmT *vm);
|
||||
BasVmResultE basVmStep(BasVmT *vm);
|
||||
void basVmSetStepLimit(BasVmT *vm, int32_t limit);
|
||||
.endcode
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basVmRun Execute the loaded module until it ends, halts, yields, errors, or hits a breakpoint/step limit.
|
||||
basVmStep Execute a single instruction and return. Useful for debugger stepping.
|
||||
basVmSetStepLimit Set maximum instructions per basVmRun call. 0 = unlimited (default). Returns BAS_VM_STEP_LIMIT when reached.
|
||||
.endtable
|
||||
|
||||
.h2 I/O Callbacks
|
||||
|
||||
The host provides these callbacks for PRINT, INPUT, and DoEvents statements.
|
||||
|
||||
.code
|
||||
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
|
||||
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
|
||||
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
|
||||
.endcode
|
||||
|
||||
.h3 Callback Types
|
||||
|
||||
.code
|
||||
typedef void (*BasPrintFnT)(void *ctx, const char *text, bool newline);
|
||||
typedef bool (*BasInputFnT)(void *ctx, const char *prompt,
|
||||
char *buf, int32_t bufSize);
|
||||
typedef bool (*BasDoEventsFnT)(void *ctx);
|
||||
.endcode
|
||||
|
||||
.table
|
||||
Type Description
|
||||
---- -----------
|
||||
BasPrintFnT Called for PRINT output. text is null-terminated. newline indicates line advance.
|
||||
BasInputFnT Called for INPUT. Fill buf (up to bufSize-1 chars). Return true on success, false on cancel.
|
||||
BasDoEventsFnT Called for DoEvents. Process pending events and return. Return false to stop the program.
|
||||
.endtable
|
||||
|
||||
.h2 UI Callbacks
|
||||
|
||||
For form and control integration. The VM resolves all UI operations through these callbacks, keeping it independent of any specific GUI toolkit.
|
||||
|
||||
.code
|
||||
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
|
||||
.endcode
|
||||
|
||||
.h3 BasUiCallbacksT
|
||||
|
||||
.table
|
||||
Field Signature Description
|
||||
----- --------- -----------
|
||||
getProp BasValueT (*)(ctx, ctrlRef, propName) Get a control property value.
|
||||
setProp void (*)(ctx, ctrlRef, propName, value) Set a control property.
|
||||
callMethod BasValueT (*)(ctx, ctrlRef, methodName, args, argc) Call a method on a control.
|
||||
createCtrl void *(*)(ctx, formRef, typeName, ctrlName) Create a control on a form.
|
||||
findCtrl void *(*)(ctx, formRef, ctrlName) Find a control by name.
|
||||
findCtrlIdx void *(*)(ctx, formRef, ctrlName, index) Find a control array element.
|
||||
loadForm void *(*)(ctx, formName) Load a form by name.
|
||||
unloadForm void (*)(ctx, formRef) Unload a form.
|
||||
showForm void (*)(ctx, formRef, modal) Show a form (modal or modeless).
|
||||
hideForm void (*)(ctx, formRef) Hide a form (keep in memory).
|
||||
msgBox int32_t (*)(ctx, message, flags) Display a message box.
|
||||
inputBox BasStringT *(*)(ctx, prompt, title, defaultText) Display an input box.
|
||||
ctx void * User pointer passed to all callbacks.
|
||||
.endtable
|
||||
|
||||
.h2 SQL Callbacks
|
||||
|
||||
For database integration.
|
||||
|
||||
.code
|
||||
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
|
||||
.endcode
|
||||
|
||||
.h3 BasSqlCallbacksT
|
||||
|
||||
.table
|
||||
Field Signature Description
|
||||
----- --------- -----------
|
||||
sqlOpen int32_t (*)(path) Open a database. Returns handle.
|
||||
sqlClose void (*)(db) Close a database.
|
||||
sqlExec bool (*)(db, sql) Execute a non-query SQL statement.
|
||||
sqlError const char *(*)(db) Get last error message.
|
||||
sqlQuery int32_t (*)(db, sql) Execute a query. Returns result set handle.
|
||||
sqlNext bool (*)(rs) Advance to next row.
|
||||
sqlEof bool (*)(rs) Check if at end of result set.
|
||||
sqlFieldCount int32_t (*)(rs) Get number of columns.
|
||||
sqlFieldName const char *(*)(rs, col) Get column name by index.
|
||||
sqlFieldText const char *(*)(rs, col) Get column value as text by index.
|
||||
sqlFieldByName const char *(*)(rs, name) Get column value as text by name.
|
||||
sqlFieldInt int32_t (*)(rs, col) Get column value as integer.
|
||||
sqlFieldDbl double (*)(rs, col) Get column value as double.
|
||||
sqlFreeResult void (*)(rs) Free a result set.
|
||||
sqlAffectedRows int32_t (*)(db) Get number of rows affected by last statement.
|
||||
.endtable
|
||||
|
||||
.h2 External Library Callbacks
|
||||
|
||||
For DECLARE LIBRARY support. The VM resolves external functions at runtime via the host.
|
||||
|
||||
.code
|
||||
void basVmSetExternCallbacks(BasVmT *vm,
|
||||
const BasExternCallbacksT *ext);
|
||||
.endcode
|
||||
|
||||
.h3 BasExternCallbacksT
|
||||
|
||||
.table
|
||||
Field Signature Description
|
||||
----- --------- -----------
|
||||
resolveExtern void *(*)(ctx, libName, funcName) Resolve a native function by library and symbol name. Cached after first call.
|
||||
callExtern BasValueT (*)(ctx, funcPtr, funcName, args, argc, retType) Call a resolved native function, marshalling arguments and return value.
|
||||
ctx void * User pointer passed to both callbacks.
|
||||
.endtable
|
||||
|
||||
.h2 Form Context
|
||||
|
||||
Set the active form context during event dispatch.
|
||||
|
||||
.code
|
||||
void basVmSetCurrentForm(BasVmT *vm, void *formRef);
|
||||
void basVmSetCurrentFormVars(BasVmT *vm,
|
||||
BasValueT *vars, int32_t count);
|
||||
.endcode
|
||||
|
||||
.h2 Stack Access
|
||||
|
||||
Push and pop values on the evaluation stack for host integration.
|
||||
|
||||
.code
|
||||
bool basVmPush(BasVmT *vm, BasValueT val);
|
||||
bool basVmPop(BasVmT *vm, BasValueT *val);
|
||||
.endcode
|
||||
|
||||
Both return true on success, false on stack overflow/underflow.
|
||||
|
||||
.h2 Error Reporting
|
||||
|
||||
.code
|
||||
const char *basVmGetError(const BasVmT *vm);
|
||||
.endcode
|
||||
|
||||
Returns the current error message string. Valid after basVmRun returns BAS_VM_ERROR.
|
||||
|
||||
.h2 Sub/Function Calls from Host
|
||||
|
||||
Call a SUB or FUNCTION by its code address from host code.
|
||||
|
||||
.code
|
||||
bool basVmCallSub(BasVmT *vm, int32_t codeAddr);
|
||||
bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr,
|
||||
const BasValueT *args, int32_t argCount);
|
||||
bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr,
|
||||
const BasValueT *args, int32_t argCount,
|
||||
BasValueT *outArgs, int32_t outCount);
|
||||
.endcode
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basVmCallSub Call a SUB with no arguments.
|
||||
basVmCallSubWithArgs Call a SUB with arguments pushed onto the stack frame.
|
||||
basVmCallSubWithArgsOut Call a SUB and read back modified argument values after it returns.
|
||||
.endtable
|
||||
|
||||
All three push a call frame, execute until the SUB returns, then restore the previous execution state. Return true on normal completion, false on error or if the VM is not idle.
|
||||
|
||||
.h2 Debugger API
|
||||
|
||||
.code
|
||||
void basVmSetBreakpoints(BasVmT *vm,
|
||||
int32_t *lines, int32_t count);
|
||||
void basVmStepInto(BasVmT *vm);
|
||||
void basVmStepOver(BasVmT *vm);
|
||||
void basVmStepOut(BasVmT *vm);
|
||||
void basVmRunToCursor(BasVmT *vm, int32_t line);
|
||||
int32_t basVmGetCurrentLine(const BasVmT *vm);
|
||||
.endcode
|
||||
|
||||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
basVmSetBreakpoints Set the breakpoint list (sorted array of source line numbers, host-owned memory).
|
||||
basVmStepInto Break at the next OP_LINE instruction.
|
||||
basVmStepOver Break when call depth returns to the current level.
|
||||
basVmStepOut Break when call depth drops below the current level.
|
||||
basVmRunToCursor Break when execution reaches the specified source line.
|
||||
basVmGetCurrentLine Get the current source line number (from the last OP_LINE instruction).
|
||||
.endtable
|
||||
|
||||
The breakpoint callback notifies the host when a breakpoint fires during nested sub calls:
|
||||
|
||||
.code
|
||||
typedef void (*BasBreakpointFnT)(void *ctx, int32_t line);
|
||||
.endcode
|
||||
|
||||
.h2 BasModuleT
|
||||
|
||||
Compiled module produced by the BASIC compiler and loaded into the VM.
|
||||
|
||||
.code
|
||||
typedef struct {
|
||||
uint8_t *code;
|
||||
int32_t codeLen;
|
||||
BasStringT **constants;
|
||||
int32_t constCount;
|
||||
int32_t globalCount;
|
||||
int32_t entryPoint;
|
||||
BasValueT *dataPool;
|
||||
int32_t dataCount;
|
||||
BasProcEntryT *procs;
|
||||
int32_t procCount;
|
||||
BasFormVarInfoT *formVarInfo;
|
||||
int32_t formVarInfoCount;
|
||||
BasDebugVarT *debugVars;
|
||||
int32_t debugVarCount;
|
||||
BasDebugUdtDefT *debugUdtDefs;
|
||||
int32_t debugUdtDefCount;
|
||||
} BasModuleT;
|
||||
.endcode
|
||||
|
||||
.table
|
||||
Field Description
|
||||
----- -----------
|
||||
code P-code bytecode array.
|
||||
codeLen Length of bytecode in bytes.
|
||||
constants String constant pool.
|
||||
constCount Number of string constants.
|
||||
globalCount Number of global variable slots needed.
|
||||
entryPoint PC of the first instruction (module-level code).
|
||||
dataPool DATA statement value pool (for READ).
|
||||
dataCount Number of values in the data pool.
|
||||
procs Procedure table (SUBs and FUNCTIONs).
|
||||
procCount Number of procedures.
|
||||
formVarInfo Per-form variable counts and init code addresses.
|
||||
formVarInfoCount Number of forms with form-scoped variables.
|
||||
debugVars Variable names and metadata for the debugger.
|
||||
debugVarCount Number of debug variable entries.
|
||||
debugUdtDefs UDT type definitions for the debugger.
|
||||
debugUdtDefCount Number of debug UDT definitions.
|
||||
.endtable
|
||||
39
src/apps/kpunch/dvxbasic/compiler/basEvents.h
Normal file
39
src/apps/kpunch/dvxbasic/compiler/basEvents.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// basEvents.h -- canonical list of BASIC event suffixes
|
||||
//
|
||||
// A procedure whose name ends with one of these suffixes is treated as an
|
||||
// event handler (Ctrl_Load, Form_Click, etc). Referenced by the stripper
|
||||
// (to retain handlers in release builds), the obfuscator (to preserve event
|
||||
// naming in rewritten forms), and the IDE (to populate the Object/Event
|
||||
// dropdowns). Keep them in one place so adding a new event only touches
|
||||
// one file.
|
||||
|
||||
#ifndef BAS_EVENTS_H
|
||||
#define BAS_EVENTS_H
|
||||
|
||||
// NULL-terminated list of event suffixes. Case-insensitive match.
|
||||
// Declared extern here, defined once in strip.c.
|
||||
extern const char *basEventSuffixes[];
|
||||
|
||||
#endif // BAS_EVENTS_H
|
||||
393
src/apps/kpunch/dvxbasic/compiler/codegen.c
Normal file
393
src/apps/kpunch/dvxbasic/compiler/codegen.c
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// codegen.c -- DVX BASIC p-code emitter implementation
|
||||
|
||||
#include "codegen.h"
|
||||
#include "symtab.h"
|
||||
#include "opcodes.h"
|
||||
#include "thirdparty/stb_ds_wrap.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
|
||||
// Function prototypes (alphabetical)
|
||||
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
|
||||
bool basAddData(BasCodeGenT *cg, BasValueT val);
|
||||
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName);
|
||||
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
|
||||
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
|
||||
void basCodeGenFree(BasCodeGenT *cg);
|
||||
void basCodeGenInit(BasCodeGenT *cg);
|
||||
int32_t basCodePos(const BasCodeGenT *cg);
|
||||
void basEmit16(BasCodeGenT *cg, int16_t v);
|
||||
void basEmit8(BasCodeGenT *cg, uint8_t b);
|
||||
void basEmitDouble(BasCodeGenT *cg, double v);
|
||||
void basEmitFloat(BasCodeGenT *cg, float v);
|
||||
void basEmitU16(BasCodeGenT *cg, uint16_t v);
|
||||
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
|
||||
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
|
||||
|
||||
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
|
||||
// Check if this string is already in the pool
|
||||
for (int32_t i = 0; i < cg->constCount; i++) {
|
||||
if (cg->constants[i]->len == len && memcmp(cg->constants[i]->data, text, len) == 0) {
|
||||
return (uint16_t)i;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t idx = (uint16_t)cg->constCount;
|
||||
BasStringT *s = basStringNew(text, len);
|
||||
arrput(cg->constants, s);
|
||||
cg->constCount = (int32_t)arrlen(cg->constants);
|
||||
return idx;
|
||||
}
|
||||
|
||||
|
||||
bool basAddData(BasCodeGenT *cg, BasValueT val) {
|
||||
BasValueT copy = basValCopy(val);
|
||||
arrput(cg->dataPool, copy);
|
||||
cg->dataCount = (int32_t)arrlen(cg->dataPool);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName) {
|
||||
BasDebugVarT dv;
|
||||
memset(&dv, 0, sizeof(dv));
|
||||
snprintf(dv.name, BAS_MAX_PROC_NAME, "%s", name);
|
||||
dv.scope = scope;
|
||||
dv.dataType = dataType;
|
||||
dv.index = index;
|
||||
dv.procIndex = procIndex;
|
||||
|
||||
if (formName && formName[0]) {
|
||||
snprintf(dv.formName, BAS_MAX_PROC_NAME, "%s", formName);
|
||||
}
|
||||
arrput(cg->debugVars, dv);
|
||||
cg->debugVarCount = (int32_t)arrlen(cg->debugVars);
|
||||
}
|
||||
|
||||
|
||||
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
|
||||
BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
|
||||
|
||||
if (!mod) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Copy code
|
||||
mod->code = (uint8_t *)malloc(cg->codeLen);
|
||||
|
||||
if (!mod->code) {
|
||||
free(mod);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(mod->code, cg->code, cg->codeLen);
|
||||
mod->codeLen = cg->codeLen;
|
||||
|
||||
// Copy constant pool (share string refs)
|
||||
if (cg->constCount > 0) {
|
||||
mod->constants = (BasStringT **)malloc(cg->constCount * sizeof(BasStringT *));
|
||||
|
||||
if (!mod->constants) {
|
||||
free(mod->code);
|
||||
free(mod);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < cg->constCount; i++) {
|
||||
mod->constants[i] = basStringRef(cg->constants[i]);
|
||||
}
|
||||
}
|
||||
|
||||
mod->constCount = cg->constCount;
|
||||
mod->globalCount = cg->globalCount;
|
||||
mod->entryPoint = 0;
|
||||
|
||||
// Copy data pool
|
||||
if (cg->dataCount > 0) {
|
||||
mod->dataPool = (BasValueT *)malloc(cg->dataCount * sizeof(BasValueT));
|
||||
|
||||
if (!mod->dataPool) {
|
||||
free(mod->constants);
|
||||
free(mod->code);
|
||||
free(mod);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < cg->dataCount; i++) {
|
||||
mod->dataPool[i] = basValCopy(cg->dataPool[i]);
|
||||
}
|
||||
}
|
||||
|
||||
mod->dataCount = cg->dataCount;
|
||||
|
||||
// Copy form variable info
|
||||
if (cg->formVarInfoCount > 0) {
|
||||
mod->formVarInfo = (BasFormVarInfoT *)malloc(cg->formVarInfoCount * sizeof(BasFormVarInfoT));
|
||||
|
||||
if (mod->formVarInfo) {
|
||||
memcpy(mod->formVarInfo, cg->formVarInfo, cg->formVarInfoCount * sizeof(BasFormVarInfoT));
|
||||
}
|
||||
|
||||
mod->formVarInfoCount = cg->formVarInfoCount;
|
||||
}
|
||||
|
||||
// Copy debug variable info
|
||||
if (cg->debugVarCount > 0) {
|
||||
mod->debugVars = (BasDebugVarT *)malloc(cg->debugVarCount * sizeof(BasDebugVarT));
|
||||
|
||||
if (mod->debugVars) {
|
||||
memcpy(mod->debugVars, cg->debugVars, cg->debugVarCount * sizeof(BasDebugVarT));
|
||||
}
|
||||
|
||||
mod->debugVarCount = cg->debugVarCount;
|
||||
}
|
||||
|
||||
// Copy UDT type definitions for debugger
|
||||
if (cg->debugUdtDefCount > 0) {
|
||||
mod->debugUdtDefs = (BasDebugUdtDefT *)malloc(cg->debugUdtDefCount * sizeof(BasDebugUdtDefT));
|
||||
|
||||
if (mod->debugUdtDefs) {
|
||||
for (int32_t i = 0; i < cg->debugUdtDefCount; i++) {
|
||||
mod->debugUdtDefs[i] = cg->debugUdtDefs[i];
|
||||
mod->debugUdtDefs[i].fields = NULL;
|
||||
|
||||
if (cg->debugUdtDefs[i].fieldCount > 0 && cg->debugUdtDefs[i].fields) {
|
||||
mod->debugUdtDefs[i].fields = (BasDebugFieldT *)malloc(
|
||||
cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT));
|
||||
|
||||
if (mod->debugUdtDefs[i].fields) {
|
||||
memcpy(mod->debugUdtDefs[i].fields, cg->debugUdtDefs[i].fields,
|
||||
cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod->debugUdtDefCount = cg->debugUdtDefCount;
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
|
||||
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
|
||||
BasModuleT *mod = basCodeGenBuildModule(cg);
|
||||
|
||||
if (!mod || !symtab) {
|
||||
return mod;
|
||||
}
|
||||
|
||||
BasSymTabT *tab = (BasSymTabT *)symtab;
|
||||
|
||||
// Count SUB/FUNCTION entries
|
||||
int32_t procCount = 0;
|
||||
|
||||
// Count globals that need runtime type init (currently: STRING).
|
||||
// This list survives stripping, unlike debugVars.
|
||||
int32_t globalInitCount = 0;
|
||||
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
|
||||
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) {
|
||||
procCount++;
|
||||
}
|
||||
|
||||
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
|
||||
globalInitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (globalInitCount > 0) {
|
||||
mod->globalInits = (BasGlobalInitT *)malloc(globalInitCount * sizeof(BasGlobalInitT));
|
||||
|
||||
if (mod->globalInits) {
|
||||
int32_t gi = 0;
|
||||
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
|
||||
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
|
||||
mod->globalInits[gi].index = s->index;
|
||||
mod->globalInits[gi].dataType = s->dataType;
|
||||
gi++;
|
||||
}
|
||||
}
|
||||
|
||||
mod->globalInitCount = gi;
|
||||
}
|
||||
}
|
||||
|
||||
if (procCount == 0) {
|
||||
return mod;
|
||||
}
|
||||
|
||||
mod->procs = (BasProcEntryT *)malloc(procCount * sizeof(BasProcEntryT));
|
||||
|
||||
if (!mod->procs) {
|
||||
return mod;
|
||||
}
|
||||
|
||||
int32_t idx = 0;
|
||||
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
|
||||
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) {
|
||||
BasProcEntryT *p = &mod->procs[idx++];
|
||||
strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1);
|
||||
p->name[BAS_MAX_PROC_NAME - 1] = '\0';
|
||||
strncpy(p->formName, s->formName, BAS_MAX_PROC_NAME - 1);
|
||||
p->formName[BAS_MAX_PROC_NAME - 1] = '\0';
|
||||
p->codeAddr = s->codeAddr;
|
||||
p->paramCount = s->paramCount;
|
||||
p->localCount = s->localCount;
|
||||
p->returnType = s->dataType;
|
||||
p->isFunction = (s->kind == SYM_FUNCTION);
|
||||
}
|
||||
}
|
||||
|
||||
mod->procCount = idx;
|
||||
return mod;
|
||||
}
|
||||
|
||||
|
||||
void basCodeGenFree(BasCodeGenT *cg) {
|
||||
for (int32_t i = 0; i < cg->constCount; i++) {
|
||||
basStringUnref(cg->constants[i]);
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < cg->dataCount; i++) {
|
||||
basValRelease(&cg->dataPool[i]);
|
||||
}
|
||||
|
||||
arrfree(cg->code);
|
||||
arrfree(cg->constants);
|
||||
arrfree(cg->dataPool);
|
||||
arrfree(cg->formVarInfo);
|
||||
arrfree(cg->debugVars);
|
||||
|
||||
for (int32_t i = 0; i < cg->debugUdtDefCount; i++) {
|
||||
free(cg->debugUdtDefs[i].fields);
|
||||
}
|
||||
|
||||
arrfree(cg->debugUdtDefs);
|
||||
cg->code = NULL;
|
||||
cg->constants = NULL;
|
||||
cg->dataPool = NULL;
|
||||
cg->formVarInfo = NULL;
|
||||
cg->debugVars = NULL;
|
||||
cg->debugUdtDefs = NULL;
|
||||
cg->constCount = 0;
|
||||
cg->dataCount = 0;
|
||||
cg->codeLen = 0;
|
||||
cg->formVarInfoCount = 0;
|
||||
cg->debugVarCount = 0;
|
||||
cg->debugUdtDefCount = 0;
|
||||
}
|
||||
|
||||
|
||||
void basCodeGenInit(BasCodeGenT *cg) {
|
||||
memset(cg, 0, sizeof(*cg));
|
||||
}
|
||||
|
||||
|
||||
int32_t basCodePos(const BasCodeGenT *cg) {
|
||||
return cg->codeLen;
|
||||
}
|
||||
|
||||
|
||||
void basEmit16(BasCodeGenT *cg, int16_t v) {
|
||||
uint8_t buf[2];
|
||||
memcpy(buf, &v, 2);
|
||||
arrput(cg->code, buf[0]);
|
||||
arrput(cg->code, buf[1]);
|
||||
cg->codeLen = (int32_t)arrlen(cg->code);
|
||||
}
|
||||
|
||||
|
||||
void basEmit8(BasCodeGenT *cg, uint8_t b) {
|
||||
arrput(cg->code, b);
|
||||
cg->codeLen = (int32_t)arrlen(cg->code);
|
||||
}
|
||||
|
||||
|
||||
void basEmitDouble(BasCodeGenT *cg, double v) {
|
||||
uint8_t buf[sizeof(double)];
|
||||
memcpy(buf, &v, sizeof(double));
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)sizeof(double); i++) {
|
||||
arrput(cg->code, buf[i]);
|
||||
}
|
||||
|
||||
cg->codeLen = (int32_t)arrlen(cg->code);
|
||||
}
|
||||
|
||||
|
||||
void basEmitFloat(BasCodeGenT *cg, float v) {
|
||||
uint8_t buf[sizeof(float)];
|
||||
memcpy(buf, &v, sizeof(float));
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)sizeof(float); i++) {
|
||||
arrput(cg->code, buf[i]);
|
||||
}
|
||||
|
||||
cg->codeLen = (int32_t)arrlen(cg->code);
|
||||
}
|
||||
|
||||
|
||||
void basEmitU16(BasCodeGenT *cg, uint16_t v) {
|
||||
uint8_t buf[2];
|
||||
memcpy(buf, &v, 2);
|
||||
arrput(cg->code, buf[0]);
|
||||
arrput(cg->code, buf[1]);
|
||||
cg->codeLen = (int32_t)arrlen(cg->code);
|
||||
}
|
||||
|
||||
|
||||
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name) {
|
||||
if (!mod || !mod->procs || !name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||
if (strcasecmp(mod->procs[i].name, name) == 0) {
|
||||
return &mod->procs[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) {
|
||||
if (pos >= 0 && pos + 2 <= cg->codeLen) {
|
||||
memcpy(&cg->code[pos], &val, 2);
|
||||
}
|
||||
}
|
||||
114
src/apps/kpunch/dvxbasic/compiler/codegen.h
Normal file
114
src/apps/kpunch/dvxbasic/compiler/codegen.h
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// codegen.h -- DVX BASIC p-code emitter
|
||||
//
|
||||
// Builds a p-code byte stream and string constant pool from
|
||||
// calls made by the parser. Provides helpers for backpatching
|
||||
// forward jumps.
|
||||
//
|
||||
// Embeddable: no DVX dependencies, pure C.
|
||||
|
||||
#ifndef DVXBASIC_CODEGEN_H
|
||||
#define DVXBASIC_CODEGEN_H
|
||||
|
||||
#include "../runtime/vm.h"
|
||||
#include "../runtime/values.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ============================================================
|
||||
// Code generator state
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
uint8_t *code; // stb_ds dynamic array
|
||||
int32_t codeLen;
|
||||
BasStringT **constants; // stb_ds dynamic array
|
||||
int32_t constCount;
|
||||
int32_t globalCount;
|
||||
BasValueT *dataPool; // stb_ds dynamic array
|
||||
int32_t dataCount;
|
||||
BasFormVarInfoT *formVarInfo; // stb_ds dynamic array
|
||||
int32_t formVarInfoCount;
|
||||
BasDebugVarT *debugVars; // stb_ds dynamic array
|
||||
int32_t debugVarCount;
|
||||
int32_t debugProcCount; // incremented per SUB/FUNCTION for debug var tracking
|
||||
BasDebugUdtDefT *debugUdtDefs; // stb_ds dynamic array
|
||||
int32_t debugUdtDefCount;
|
||||
} BasCodeGenT;
|
||||
|
||||
// ============================================================
|
||||
// API
|
||||
// ============================================================
|
||||
|
||||
void basCodeGenInit(BasCodeGenT *cg);
|
||||
void basCodeGenFree(BasCodeGenT *cg);
|
||||
|
||||
// Emit single byte
|
||||
void basEmit8(BasCodeGenT *cg, uint8_t b);
|
||||
|
||||
// Emit 16-bit signed value
|
||||
void basEmit16(BasCodeGenT *cg, int16_t v);
|
||||
|
||||
// Emit 16-bit unsigned value
|
||||
void basEmitU16(BasCodeGenT *cg, uint16_t v);
|
||||
|
||||
// Emit 32-bit float
|
||||
void basEmitFloat(BasCodeGenT *cg, float v);
|
||||
|
||||
// Emit 64-bit double
|
||||
void basEmitDouble(BasCodeGenT *cg, double v);
|
||||
|
||||
// Get current code position (for jump targets)
|
||||
int32_t basCodePos(const BasCodeGenT *cg);
|
||||
|
||||
// Patch a 16-bit value at a previous position (for backpatching jumps)
|
||||
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
|
||||
|
||||
// Add a string to the constant pool. Returns the pool index.
|
||||
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
|
||||
|
||||
// Add a value to the data pool (for DATA statements). Returns true on success.
|
||||
bool basAddData(BasCodeGenT *cg, BasValueT val);
|
||||
|
||||
// Build a BasModuleT from the generated code. The caller takes
|
||||
// ownership of the module and must free it with basModuleFree().
|
||||
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
|
||||
|
||||
// Build a module with procedure table from parser symbol table.
|
||||
// symtab is a BasSymTabT* (cast to void* to avoid circular include).
|
||||
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
|
||||
|
||||
// Free a module built by basCodeGenBuildModule.
|
||||
void basModuleFree(BasModuleT *mod);
|
||||
|
||||
// Add a debug variable entry (for debugger variable display).
|
||||
// formName is only used for SCOPE_FORM vars (NULL or "" for others).
|
||||
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName);
|
||||
|
||||
// Find a procedure by name in a module's procedure table.
|
||||
// Case-insensitive. Returns NULL if not found.
|
||||
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
|
||||
|
||||
#endif // DVXBASIC_CODEGEN_H
|
||||
604
src/apps/kpunch/dvxbasic/compiler/compact.c
Normal file
604
src/apps/kpunch/dvxbasic/compiler/compact.c
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// compact.c -- Release build bytecode compaction
|
||||
//
|
||||
// Walks the module's bytecode, removes OP_LINE instructions (3 bytes
|
||||
// each), and rewrites all code-address references so control flow
|
||||
// still lands on the correct instructions.
|
||||
//
|
||||
// Address references:
|
||||
// - BasProcEntryT::codeAddr (absolute)
|
||||
// - BasFormVarInfoT::initCodeAddr (absolute, 0 = no init)
|
||||
// - OP_CALL operand (absolute uint16)
|
||||
// - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE operand (relative int16)
|
||||
// - OP_FOR_NEXT loopTop operand (relative int16)
|
||||
// - OP_ON_ERROR handler operand (relative int16; 0 = disable, not remapped)
|
||||
// - GOSUB return address (emitted as OP_PUSH_INT32 followed by OP_JMP,
|
||||
// where the pushed value equals the PC immediately after the JMP)
|
||||
//
|
||||
// Safety: if any opcode cannot be sized, any jump overflows int16, or
|
||||
// the walk doesn't reach codeLen exactly, the module is left untouched.
|
||||
|
||||
#include "compact.h"
|
||||
#include "opcodes.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// Function prototypes (alphabetical)
|
||||
int32_t basCompactBytecode(BasModuleT *mod);
|
||||
static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNewLen);
|
||||
static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos);
|
||||
static int32_t opOperandSize(uint8_t op);
|
||||
static int16_t readI16LE(const uint8_t *p);
|
||||
static int32_t readI32LE(const uint8_t *p);
|
||||
static uint16_t readU16LE(const uint8_t *p);
|
||||
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen);
|
||||
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero);
|
||||
static void writeI16LE(uint8_t *p, int16_t v);
|
||||
static void writeI32LE(uint8_t *p, int32_t v);
|
||||
static void writeU16LE(uint8_t *p, uint16_t v);
|
||||
|
||||
int32_t basCompactBytecode(BasModuleT *mod) {
|
||||
if (!mod || !mod->code || mod->codeLen <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint8_t *oldCode = mod->code;
|
||||
int32_t oldCodeLen = mod->codeLen;
|
||||
|
||||
// Count OP_LINE occurrences. If none, nothing to do.
|
||||
int32_t lineCount = 0;
|
||||
{
|
||||
int32_t pc = 0;
|
||||
|
||||
while (pc < oldCodeLen) {
|
||||
uint8_t op = oldCode[pc];
|
||||
int32_t operand = opOperandSize(op);
|
||||
|
||||
if (operand < 0 || pc + 1 + operand > oldCodeLen) {
|
||||
return 0; // unknown opcode -- skip compaction
|
||||
}
|
||||
|
||||
if (op == OP_LINE) {
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
pc += 1 + operand;
|
||||
}
|
||||
|
||||
if (pc != oldCodeLen) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (lineCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t newCodeLen = 0;
|
||||
int32_t *remap = buildRemap(oldCode, oldCodeLen, &newCodeLen);
|
||||
|
||||
if (!remap) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t *newCode = (uint8_t *)malloc(newCodeLen > 0 ? newCodeLen : 1);
|
||||
|
||||
if (!newCode) {
|
||||
free(remap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Copy bytes (skipping OP_LINE) and rewrite address operands.
|
||||
bool ok = true;
|
||||
int32_t oldPc = 0;
|
||||
|
||||
while (oldPc < oldCodeLen && ok) {
|
||||
uint8_t op = oldCode[oldPc];
|
||||
int32_t operand = opOperandSize(op);
|
||||
int32_t instSize = 1 + operand;
|
||||
|
||||
if (op == OP_LINE) {
|
||||
oldPc += instSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t newPc = remap[oldPc];
|
||||
|
||||
// Copy the instruction verbatim first; we'll overwrite operands that
|
||||
// need remapping below.
|
||||
memcpy(newCode + newPc, oldCode + oldPc, instSize);
|
||||
|
||||
switch (op) {
|
||||
case OP_CALL: {
|
||||
uint16_t oldAddr = readU16LE(oldCode + oldPc + 1);
|
||||
|
||||
if (!remapAbsU16(newCode, newPc, 1, oldAddr, remap, newCodeLen)) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_JMP:
|
||||
case OP_JMP_TRUE:
|
||||
case OP_JMP_FALSE: {
|
||||
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
|
||||
|
||||
if (!remapRelI16(newCode, newPc, 1, oldOff,
|
||||
oldPc + 3, newPc + 3,
|
||||
remap, oldCodeLen, newCodeLen, false)) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FOR_NEXT: {
|
||||
int16_t oldOff = readI16LE(oldCode + oldPc + 4);
|
||||
|
||||
if (!remapRelI16(newCode, newPc, 4, oldOff,
|
||||
oldPc + 6, newPc + 6,
|
||||
remap, oldCodeLen, newCodeLen, false)) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FOR_INIT: {
|
||||
// [uint16 varIdx] [uint8 scope] [int16 skipOffset]
|
||||
int16_t oldOff = readI16LE(oldCode + oldPc + 4);
|
||||
|
||||
if (!remapRelI16(newCode, newPc, 4, oldOff,
|
||||
oldPc + 6, newPc + 6,
|
||||
remap, oldCodeLen, newCodeLen, false)) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_ON_ERROR: {
|
||||
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
|
||||
|
||||
if (!remapRelI16(newCode, newPc, 1, oldOff,
|
||||
oldPc + 3, newPc + 3,
|
||||
remap, oldCodeLen, newCodeLen, true)) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_PUSH_INT32: {
|
||||
// Detect GOSUB return-address push and remap the absolute address.
|
||||
if (isGosubPush(oldCode, oldCodeLen, oldPc)) {
|
||||
int32_t oldAddr = readI32LE(oldCode + oldPc + 1);
|
||||
|
||||
if (oldAddr < 0 || oldAddr > oldCodeLen) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
int32_t newAddr = remap[oldAddr];
|
||||
|
||||
if (newAddr < 0 || newAddr > newCodeLen) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
writeI32LE(newCode + newPc + 1, newAddr);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
oldPc += instSize;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
free(newCode);
|
||||
free(remap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Rewrite proc entry points
|
||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||
int32_t oldAddr = mod->procs[i].codeAddr;
|
||||
|
||||
if (oldAddr < 0 || oldAddr > oldCodeLen) {
|
||||
free(newCode);
|
||||
free(remap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
mod->procs[i].codeAddr = remap[oldAddr];
|
||||
}
|
||||
|
||||
// Rewrite form-var init code addresses. Negative means "no init code".
|
||||
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
|
||||
int32_t oldAddr = mod->formVarInfo[i].initCodeAddr;
|
||||
|
||||
if (oldAddr < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (oldAddr > oldCodeLen) {
|
||||
free(newCode);
|
||||
free(remap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t oldLen = mod->formVarInfo[i].initCodeLen;
|
||||
int32_t oldEnd = oldAddr + oldLen;
|
||||
|
||||
if (oldEnd > oldCodeLen) {
|
||||
oldEnd = oldCodeLen;
|
||||
}
|
||||
|
||||
int32_t newAddr = remap[oldAddr];
|
||||
int32_t newEnd = remap[oldEnd];
|
||||
|
||||
mod->formVarInfo[i].initCodeAddr = newAddr;
|
||||
mod->formVarInfo[i].initCodeLen = newEnd - newAddr;
|
||||
}
|
||||
|
||||
// Rewrite entry point
|
||||
if (mod->entryPoint >= 0 && mod->entryPoint <= oldCodeLen) {
|
||||
mod->entryPoint = remap[mod->entryPoint];
|
||||
}
|
||||
|
||||
// Swap in the new code
|
||||
free(mod->code);
|
||||
mod->code = newCode;
|
||||
mod->codeLen = newCodeLen;
|
||||
|
||||
int32_t removed = oldCodeLen - newCodeLen;
|
||||
|
||||
free(remap);
|
||||
return removed;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Walk bytecode, build remap
|
||||
// ============================================================
|
||||
//
|
||||
// remap[oldPos] = newPos for every byte position in [0, oldCodeLen].
|
||||
// For OP_LINE bytes (removed): remap points at where the NEXT instruction
|
||||
// starts in the new code.
|
||||
// Final entry remap[oldCodeLen] = newCodeLen.
|
||||
//
|
||||
// Returns malloc'd array of size (oldCodeLen + 1), or NULL on failure.
|
||||
static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNewLen) {
|
||||
int32_t *remap = (int32_t *)malloc((codeLen + 1) * sizeof(int32_t));
|
||||
|
||||
if (!remap) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t oldPc = 0;
|
||||
int32_t newPc = 0;
|
||||
|
||||
while (oldPc < codeLen) {
|
||||
uint8_t op = code[oldPc];
|
||||
int32_t operand = opOperandSize(op);
|
||||
|
||||
if (operand < 0) {
|
||||
free(remap);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t instSize = 1 + operand;
|
||||
|
||||
if (oldPc + instSize > codeLen) {
|
||||
free(remap);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (op == OP_LINE) {
|
||||
// These bytes are removed; they map to where the next instruction starts.
|
||||
for (int32_t i = 0; i < instSize; i++) {
|
||||
remap[oldPc + i] = newPc;
|
||||
}
|
||||
} else {
|
||||
for (int32_t i = 0; i < instSize; i++) {
|
||||
remap[oldPc + i] = newPc + i;
|
||||
}
|
||||
|
||||
newPc += instSize;
|
||||
}
|
||||
|
||||
oldPc += instSize;
|
||||
}
|
||||
|
||||
if (oldPc != codeLen) {
|
||||
free(remap);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
remap[codeLen] = newPc;
|
||||
*outNewLen = newPc;
|
||||
return remap;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// GOSUB pattern detection
|
||||
// ============================================================
|
||||
//
|
||||
// GOSUB emits:
|
||||
// oldPc: OP_PUSH_INT32 (1 byte)
|
||||
// oldPc+1: int32 value V (4 bytes)
|
||||
// oldPc+5: OP_JMP (1 byte)
|
||||
// oldPc+6: int16 offset (2 bytes)
|
||||
// oldPc+8: <next instruction>
|
||||
// The invariant is V == oldPc + 8 (the pushed return address).
|
||||
//
|
||||
// Returns true if the given position is the start of such a pattern.
|
||||
static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) {
|
||||
if (pos + 8 > codeLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (code[pos] != OP_PUSH_INT32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (code[pos + 5] != OP_JMP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t value = readI32LE(code + pos + 1);
|
||||
return value == pos + 8;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Opcode operand size table
|
||||
// ============================================================
|
||||
// Returns operand byte count (excluding the 1-byte opcode), or -1 if unknown.
|
||||
static int32_t opOperandSize(uint8_t op) {
|
||||
switch (op) {
|
||||
// No operand bytes
|
||||
case OP_NOP:
|
||||
case OP_PUSH_TRUE: case OP_PUSH_FALSE:
|
||||
case OP_POP: case OP_DUP:
|
||||
case OP_LOAD_REF: case OP_STORE_REF:
|
||||
case OP_ADD_INT: case OP_SUB_INT: case OP_MUL_INT:
|
||||
case OP_IDIV_INT: case OP_MOD_INT: case OP_NEG_INT:
|
||||
case OP_ADD_FLT: case OP_SUB_FLT: case OP_MUL_FLT:
|
||||
case OP_DIV_FLT: case OP_NEG_FLT: case OP_POW:
|
||||
case OP_STR_CONCAT: case OP_STR_LEFT: case OP_STR_RIGHT:
|
||||
case OP_STR_MID: case OP_STR_MID2: case OP_STR_LEN:
|
||||
case OP_STR_INSTR: case OP_STR_INSTR3:
|
||||
case OP_STR_UCASE: case OP_STR_LCASE:
|
||||
case OP_STR_TRIM: case OP_STR_LTRIM: case OP_STR_RTRIM:
|
||||
case OP_STR_CHR: case OP_STR_ASC: case OP_STR_SPACE:
|
||||
case OP_CMP_EQ: case OP_CMP_NE: case OP_CMP_LT:
|
||||
case OP_CMP_GT: case OP_CMP_LE: case OP_CMP_GE:
|
||||
case OP_AND: case OP_OR: case OP_NOT:
|
||||
case OP_XOR: case OP_EQV: case OP_IMP:
|
||||
case OP_GOSUB_RET: case OP_RET: case OP_RET_VAL:
|
||||
case OP_FOR_POP:
|
||||
case OP_CONV_INT_FLT: case OP_CONV_FLT_INT:
|
||||
case OP_CONV_INT_STR: case OP_CONV_STR_INT:
|
||||
case OP_CONV_FLT_STR: case OP_CONV_STR_FLT:
|
||||
case OP_CONV_INT_LONG: case OP_CONV_LONG_INT:
|
||||
case OP_PRINT: case OP_PRINT_NL: case OP_PRINT_TAB:
|
||||
case OP_INPUT:
|
||||
case OP_FILE_CLOSE: case OP_FILE_PRINT: case OP_FILE_INPUT:
|
||||
case OP_FILE_EOF: case OP_FILE_LINE_INPUT:
|
||||
case OP_LOAD_PROP: case OP_STORE_PROP:
|
||||
case OP_LOAD_FORM: case OP_UNLOAD_FORM:
|
||||
case OP_HIDE_FORM: case OP_DO_EVENTS:
|
||||
case OP_MSGBOX: case OP_INPUTBOX: case OP_ME_REF:
|
||||
case OP_CREATE_CTRL: case OP_FIND_CTRL: case OP_FIND_CTRL_IDX:
|
||||
case OP_CREATE_CTRL_EX:
|
||||
case OP_ERASE:
|
||||
case OP_RESUME: case OP_RESUME_NEXT:
|
||||
case OP_RAISE_ERR: case OP_ERR_NUM: case OP_ERR_CLEAR:
|
||||
case OP_MATH_ABS: case OP_MATH_INT: case OP_MATH_FIX:
|
||||
case OP_MATH_SGN: case OP_MATH_SQR: case OP_MATH_SIN:
|
||||
case OP_MATH_COS: case OP_MATH_TAN: case OP_MATH_ATN:
|
||||
case OP_MATH_LOG: case OP_MATH_EXP: case OP_MATH_RND:
|
||||
case OP_MATH_RANDOMIZE:
|
||||
case OP_RGB:
|
||||
case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE:
|
||||
case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX:
|
||||
case OP_STR_STRING: case OP_STR_OCT: case OP_CONV_BOOL:
|
||||
case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
|
||||
case OP_SLEEP: case OP_ENVIRON:
|
||||
case OP_READ_DATA: case OP_RESTORE:
|
||||
case OP_FILE_WRITE: case OP_FILE_WRITE_SEP: case OP_FILE_WRITE_NL:
|
||||
case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK:
|
||||
case OP_FILE_LOF: case OP_FILE_LOC: case OP_FILE_FREEFILE:
|
||||
case OP_FILE_INPUT_N:
|
||||
case OP_STR_MID_ASGN: case OP_PRINT_USING:
|
||||
case OP_PRINT_TAB_N: case OP_PRINT_SPC_N:
|
||||
case OP_FORMAT: case OP_SHELL:
|
||||
case OP_APP_PATH: case OP_APP_CONFIG: case OP_APP_DATA:
|
||||
case OP_INI_READ: case OP_INI_WRITE:
|
||||
case OP_FS_KILL: case OP_FS_NAME: case OP_FS_FILECOPY:
|
||||
case OP_FS_MKDIR: case OP_FS_RMDIR: case OP_FS_CHDIR:
|
||||
case OP_FS_CHDRIVE: case OP_FS_CURDIR: case OP_FS_DIR:
|
||||
case OP_FS_DIR_NEXT: case OP_FS_FILELEN:
|
||||
case OP_FS_GETATTR: case OP_FS_SETATTR:
|
||||
case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL:
|
||||
case OP_END: case OP_HALT:
|
||||
return 0;
|
||||
|
||||
case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
|
||||
case OP_PUSH_ARR_ADDR:
|
||||
case OP_PRINT_SPC: case OP_FILE_OPEN:
|
||||
case OP_CALL_METHOD: case OP_SHOW_FORM:
|
||||
case OP_LBOUND: case OP_UBOUND:
|
||||
case OP_COMPARE_MODE:
|
||||
return 1;
|
||||
|
||||
case OP_PUSH_INT16: case OP_PUSH_STR:
|
||||
case OP_LOAD_LOCAL: case OP_STORE_LOCAL:
|
||||
case OP_LOAD_GLOBAL: case OP_STORE_GLOBAL:
|
||||
case OP_LOAD_FIELD: case OP_STORE_FIELD:
|
||||
case OP_PUSH_LOCAL_ADDR: case OP_PUSH_GLOBAL_ADDR:
|
||||
case OP_JMP: case OP_JMP_TRUE: case OP_JMP_FALSE:
|
||||
case OP_CTRL_REF:
|
||||
case OP_LOAD_FORM_VAR: case OP_STORE_FORM_VAR:
|
||||
case OP_PUSH_FORM_ADDR:
|
||||
case OP_DIM_ARRAY: case OP_REDIM:
|
||||
case OP_ON_ERROR:
|
||||
case OP_STR_FIXLEN:
|
||||
case OP_LINE:
|
||||
return 2;
|
||||
|
||||
case OP_STORE_ARRAY_FIELD:
|
||||
return 3;
|
||||
|
||||
case OP_PUSH_INT32: case OP_PUSH_FLT32:
|
||||
case OP_CALL:
|
||||
return 4;
|
||||
|
||||
case OP_FOR_INIT:
|
||||
case OP_FOR_NEXT:
|
||||
return 5;
|
||||
|
||||
case OP_CALL_EXTERN:
|
||||
return 6;
|
||||
|
||||
case OP_PUSH_FLT64:
|
||||
return 8;
|
||||
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Little-endian helpers (bytecode is always LE regardless of host)
|
||||
// ============================================================
|
||||
static int16_t readI16LE(const uint8_t *p) {
|
||||
return (int16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
|
||||
static int32_t readI32LE(const uint8_t *p) {
|
||||
return (int32_t)((uint32_t)p[0] |
|
||||
((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) |
|
||||
((uint32_t)p[3] << 24));
|
||||
}
|
||||
|
||||
|
||||
static uint16_t readU16LE(const uint8_t *p) {
|
||||
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Apply remap to a single instruction's operand
|
||||
// ============================================================
|
||||
//
|
||||
// Returns true on success, false if an offset overflows int16 or a
|
||||
// target doesn't land on a valid instruction in the old code.
|
||||
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen) {
|
||||
int32_t newAddr = remap[oldAddr];
|
||||
|
||||
if (newAddr < 0 || newAddr > newCodeLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newAddr > 0xFFFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
writeU16LE(newCode + newOpPos + operandOffset, (uint16_t)newAddr);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Rewrite a relative int16 offset.
|
||||
// oldOpPos, oldPcAfter: position of opcode and PC after reading offset
|
||||
// newOpPos, newPcAfter: same in the new code
|
||||
// operandOffset: byte offset from opcode to the int16 operand
|
||||
// oldOffset: the offset as stored in the old code
|
||||
//
|
||||
// Handles ON_ERROR's special case (offset == 0 means "disable").
|
||||
// For ON_ERROR the caller passes allowZero=true; the zero is preserved as-is.
|
||||
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero) {
|
||||
if (allowZero && oldOffset == 0) {
|
||||
writeI16LE(newCode + newOpPos + operandOffset, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t oldTarget = oldPcAfter + oldOffset;
|
||||
|
||||
if (oldTarget < 0 || oldTarget > codeLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t newTarget = remap[oldTarget];
|
||||
|
||||
if (newTarget < 0 || newTarget > newCodeLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t newOffset = newTarget - newPcAfter;
|
||||
|
||||
if (newOffset < INT16_MIN || newOffset > INT16_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
writeI16LE(newCode + newOpPos + operandOffset, (int16_t)newOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void writeI16LE(uint8_t *p, int16_t v) {
|
||||
uint16_t u = (uint16_t)v;
|
||||
p[0] = (uint8_t)(u & 0xFF);
|
||||
p[1] = (uint8_t)((u >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
|
||||
static void writeI32LE(uint8_t *p, int32_t v) {
|
||||
uint32_t u = (uint32_t)v;
|
||||
p[0] = (uint8_t)(u & 0xFF);
|
||||
p[1] = (uint8_t)((u >> 8) & 0xFF);
|
||||
p[2] = (uint8_t)((u >> 16) & 0xFF);
|
||||
p[3] = (uint8_t)((u >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
|
||||
static void writeU16LE(uint8_t *p, uint16_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFF);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xFF);
|
||||
}
|
||||
43
src/apps/kpunch/dvxbasic/compiler/compact.h
Normal file
43
src/apps/kpunch/dvxbasic/compiler/compact.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// compact.h -- Release build bytecode compaction
|
||||
//
|
||||
// Removes OP_LINE instructions from the module's bytecode and
|
||||
// rewrites all code-address references (proc entries, CALL operands,
|
||||
// JMP/FOR_NEXT/ON_ERROR relative offsets, GOSUB return addresses,
|
||||
// and formVarInfo init code addresses).
|
||||
//
|
||||
// Safe by construction: if any sanity check fails (unknown opcode,
|
||||
// offset overflow, walk not landing on codeLen, etc.), the module
|
||||
// is left unchanged and the function returns 0.
|
||||
|
||||
#ifndef DVXBASIC_COMPACT_H
|
||||
#define DVXBASIC_COMPACT_H
|
||||
|
||||
#include "../runtime/vm.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// Returns the number of bytes removed, or 0 if compaction was skipped.
|
||||
int32_t basCompactBytecode(BasModuleT *mod);
|
||||
|
||||
#endif // DVXBASIC_COMPACT_H
|
||||
897
src/apps/kpunch/dvxbasic/compiler/lexer.c
Normal file
897
src/apps/kpunch/dvxbasic/compiler/lexer.c
Normal file
|
|
@ -0,0 +1,897 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// lexer.c -- DVX BASIC lexer implementation
|
||||
//
|
||||
// Single-pass tokenizer. Keywords are case-insensitive. Identifiers
|
||||
// preserve their original case for display but comparisons are
|
||||
// case-insensitive. Line continuations (underscore at end of line)
|
||||
// are handled transparently.
|
||||
|
||||
#include "lexer.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// ============================================================
|
||||
// Keyword table
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
const char *text;
|
||||
uint8_t textLen; // precomputed so lookupKeyword() can length-reject cheaply
|
||||
BasTokenTypeE type;
|
||||
} KeywordEntryT;
|
||||
|
||||
#define KW(s, t) { s, (uint8_t)(sizeof(s) - 1), t }
|
||||
|
||||
static const KeywordEntryT sKeywords[] = {
|
||||
KW("AND", TOK_AND),
|
||||
KW("APP", TOK_APP),
|
||||
KW("APPEND", TOK_APPEND),
|
||||
KW("AS", TOK_AS),
|
||||
KW("BASE", TOK_BASE),
|
||||
KW("BINARY", TOK_BINARY),
|
||||
KW("BOOLEAN", TOK_BOOLEAN),
|
||||
KW("BYVAL", TOK_BYVAL),
|
||||
KW("CALL", TOK_CALL),
|
||||
KW("CASE", TOK_CASE),
|
||||
KW("CHDIR", TOK_CHDIR),
|
||||
KW("CHDRIVE", TOK_CHDRIVE),
|
||||
KW("CLOSE", TOK_CLOSE),
|
||||
KW("CREATECONTROL", TOK_CREATECONTROL),
|
||||
KW("CREATEFORM", TOK_CREATEFORM),
|
||||
KW("CURDIR", TOK_CURDIR),
|
||||
KW("CURDIR$", TOK_CURDIR),
|
||||
KW("CONST", TOK_CONST),
|
||||
KW("DATA", TOK_DATA),
|
||||
KW("DECLARE", TOK_DECLARE),
|
||||
KW("DEF", TOK_DEF),
|
||||
KW("DEFDBL", TOK_DEFDBL),
|
||||
KW("DEFINT", TOK_DEFINT),
|
||||
KW("DEFLNG", TOK_DEFLNG),
|
||||
KW("DEFSNG", TOK_DEFSNG),
|
||||
KW("DEFSTR", TOK_DEFSTR),
|
||||
KW("DIM", TOK_DIM),
|
||||
KW("DIR", TOK_DIR),
|
||||
KW("DIR$", TOK_DIR),
|
||||
KW("DO", TOK_DO),
|
||||
KW("DOEVENTS", TOK_DOEVENTS),
|
||||
KW("DOUBLE", TOK_DOUBLE),
|
||||
KW("ELSE", TOK_ELSE),
|
||||
KW("ELSEIF", TOK_ELSEIF),
|
||||
KW("END", TOK_END),
|
||||
KW("EOF", TOK_EOF_KW),
|
||||
KW("EQV", TOK_EQV),
|
||||
KW("ERASE", TOK_ERASE),
|
||||
KW("ERR", TOK_ERR),
|
||||
KW("ERROR", TOK_ERROR_KW),
|
||||
KW("EXPLICIT", TOK_EXPLICIT),
|
||||
KW("EXIT", TOK_EXIT),
|
||||
KW("FALSE", TOK_FALSE_KW),
|
||||
KW("FILECOPY", TOK_FILECOPY),
|
||||
KW("FILELEN", TOK_FILELEN),
|
||||
KW("FOR", TOK_FOR),
|
||||
KW("FUNCTION", TOK_FUNCTION),
|
||||
KW("GET", TOK_GET),
|
||||
KW("GETATTR", TOK_GETATTR),
|
||||
KW("GOSUB", TOK_GOSUB),
|
||||
KW("GOTO", TOK_GOTO),
|
||||
KW("HIDE", TOK_HIDE),
|
||||
KW("IF", TOK_IF),
|
||||
KW("IMP", TOK_IMP),
|
||||
KW("INIREAD", TOK_INIREAD),
|
||||
KW("INIREAD$", TOK_INIREAD),
|
||||
KW("INIWRITE", TOK_INIWRITE),
|
||||
KW("INPUT", TOK_INPUT),
|
||||
KW("INTEGER", TOK_INTEGER),
|
||||
KW("IS", TOK_IS),
|
||||
KW("KILL", TOK_KILL),
|
||||
KW("LBOUND", TOK_LBOUND),
|
||||
KW("LET", TOK_LET),
|
||||
KW("LINE", TOK_LINE),
|
||||
KW("LOAD", TOK_LOAD),
|
||||
KW("LONG", TOK_LONG),
|
||||
KW("LOOP", TOK_LOOP),
|
||||
KW("ME", TOK_ME),
|
||||
KW("MKDIR", TOK_MKDIR),
|
||||
KW("MOD", TOK_MOD),
|
||||
KW("INPUTBOX", TOK_INPUTBOX),
|
||||
KW("INPUTBOX$", TOK_INPUTBOX),
|
||||
KW("MSGBOX", TOK_MSGBOX),
|
||||
KW("NAME", TOK_NAME),
|
||||
KW("NEXT", TOK_NEXT),
|
||||
KW("NOT", TOK_NOT),
|
||||
KW("NOTHING", TOK_NOTHING),
|
||||
KW("ON", TOK_ON),
|
||||
KW("OPEN", TOK_OPEN),
|
||||
KW("OPTIONAL", TOK_OPTIONAL),
|
||||
KW("OPTION", TOK_OPTION),
|
||||
KW("OR", TOK_OR),
|
||||
KW("OUTPUT", TOK_OUTPUT),
|
||||
KW("PRESERVE", TOK_PRESERVE),
|
||||
KW("PRINT", TOK_PRINT),
|
||||
KW("PUT", TOK_PUT),
|
||||
KW("RANDOM", TOK_RANDOM),
|
||||
KW("RANDOMIZE", TOK_RANDOMIZE),
|
||||
KW("READ", TOK_READ),
|
||||
KW("REDIM", TOK_REDIM),
|
||||
KW("REM", TOK_REM),
|
||||
KW("REMOVECONTROL", TOK_REMOVECONTROL),
|
||||
KW("RESTORE", TOK_RESTORE),
|
||||
KW("RESUME", TOK_RESUME),
|
||||
KW("RETURN", TOK_RETURN),
|
||||
KW("RMDIR", TOK_RMDIR),
|
||||
KW("SEEK", TOK_SEEK),
|
||||
KW("SELECT", TOK_SELECT),
|
||||
KW("SET", TOK_SET),
|
||||
KW("SETATTR", TOK_SETATTR),
|
||||
KW("SETEVENT", TOK_SETEVENT),
|
||||
KW("SHARED", TOK_SHARED),
|
||||
KW("SHELL", TOK_SHELL),
|
||||
KW("SHOW", TOK_SHOW),
|
||||
KW("SINGLE", TOK_SINGLE),
|
||||
KW("SLEEP", TOK_SLEEP),
|
||||
KW("STATIC", TOK_STATIC),
|
||||
KW("STEP", TOK_STEP),
|
||||
KW("STRING", TOK_STRING_KW),
|
||||
KW("SUB", TOK_SUB),
|
||||
KW("SWAP", TOK_SWAP),
|
||||
KW("THEN", TOK_THEN),
|
||||
KW("TIMER", TOK_TIMER),
|
||||
KW("TO", TOK_TO),
|
||||
KW("TRUE", TOK_TRUE_KW),
|
||||
KW("TYPE", TOK_TYPE),
|
||||
KW("UBOUND", TOK_UBOUND),
|
||||
KW("UNLOAD", TOK_UNLOAD),
|
||||
KW("UNTIL", TOK_UNTIL),
|
||||
KW("WEND", TOK_WEND),
|
||||
KW("WHILE", TOK_WHILE),
|
||||
KW("WITH", TOK_WITH),
|
||||
KW("WRITE", TOK_WRITE),
|
||||
KW("XOR", TOK_XOR),
|
||||
{ NULL, 0, TOK_ERROR }
|
||||
};
|
||||
|
||||
#undef KW
|
||||
|
||||
#define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1)
|
||||
|
||||
|
||||
// Function prototypes (alphabetical)
|
||||
static char advance(BasLexerT *lex);
|
||||
static bool atEnd(const BasLexerT *lex);
|
||||
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
|
||||
const char *basLexerKeywordAt(int32_t i);
|
||||
BasKeywordClassE basLexerKeywordClass(int32_t i);
|
||||
int32_t basLexerKeywordCount(void);
|
||||
BasTokenTypeE basLexerNext(BasLexerT *lex);
|
||||
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
|
||||
const char *basTokenName(BasTokenTypeE type);
|
||||
static BasTokenTypeE lookupKeyword(const char *text, int32_t len);
|
||||
static char peek(const BasLexerT *lex);
|
||||
static char peekNext(const BasLexerT *lex);
|
||||
static void setError(BasLexerT *lex, const char *msg);
|
||||
static void skipLineComment(BasLexerT *lex);
|
||||
static void skipWhitespace(BasLexerT *lex);
|
||||
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex);
|
||||
static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex);
|
||||
static BasTokenTypeE tokenizeNumber(BasLexerT *lex);
|
||||
static BasTokenTypeE tokenizeString(BasLexerT *lex);
|
||||
static char upperChar(char c);
|
||||
|
||||
static char advance(BasLexerT *lex) {
|
||||
if (atEnd(lex)) {
|
||||
return '\0';
|
||||
}
|
||||
|
||||
char c = lex->source[lex->pos++];
|
||||
|
||||
if (c == '\n') {
|
||||
lex->line++;
|
||||
lex->col = 1;
|
||||
} else {
|
||||
lex->col++;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
static bool atEnd(const BasLexerT *lex) {
|
||||
return lex->pos >= lex->sourceLen;
|
||||
}
|
||||
|
||||
|
||||
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen) {
|
||||
memset(lex, 0, sizeof(*lex));
|
||||
lex->source = source;
|
||||
lex->sourceLen = (sourceLen < 0) ? (int32_t)strlen(source) : sourceLen;
|
||||
lex->pos = 0;
|
||||
lex->line = 1;
|
||||
lex->col = 1;
|
||||
|
||||
// Prime the first token
|
||||
basLexerNext(lex);
|
||||
}
|
||||
|
||||
|
||||
const char *basLexerKeywordAt(int32_t i) {
|
||||
if (i < 0 || i >= (int32_t)KEYWORD_COUNT) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sKeywords[i].text;
|
||||
}
|
||||
|
||||
|
||||
BasKeywordClassE basLexerKeywordClass(int32_t i) {
|
||||
if (i < 0 || i >= (int32_t)KEYWORD_COUNT) {
|
||||
return BAS_KW_CLASS_OTHER;
|
||||
}
|
||||
|
||||
switch (sKeywords[i].type) {
|
||||
case TOK_BOOLEAN:
|
||||
case TOK_DOUBLE:
|
||||
case TOK_INTEGER:
|
||||
case TOK_LONG:
|
||||
case TOK_SINGLE:
|
||||
case TOK_STRING_KW:
|
||||
return BAS_KW_CLASS_TYPE;
|
||||
|
||||
case TOK_TRUE_KW:
|
||||
case TOK_FALSE_KW:
|
||||
case TOK_NOTHING:
|
||||
return BAS_KW_CLASS_LITERAL;
|
||||
|
||||
default:
|
||||
return BAS_KW_CLASS_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int32_t basLexerKeywordCount(void) {
|
||||
return (int32_t)KEYWORD_COUNT;
|
||||
}
|
||||
|
||||
|
||||
BasTokenTypeE basLexerNext(BasLexerT *lex) {
|
||||
skipWhitespace(lex);
|
||||
|
||||
lex->token.line = lex->line;
|
||||
lex->token.col = lex->col;
|
||||
lex->token.textLen = 0;
|
||||
lex->token.text[0] = '\0';
|
||||
|
||||
if (atEnd(lex)) {
|
||||
lex->token.type = TOK_EOF;
|
||||
return TOK_EOF;
|
||||
}
|
||||
|
||||
char c = peek(lex);
|
||||
|
||||
// Newline
|
||||
if (c == '\n') {
|
||||
advance(lex);
|
||||
lex->token.type = TOK_NEWLINE;
|
||||
lex->token.text[0] = '\n';
|
||||
lex->token.text[1] = '\0';
|
||||
lex->token.textLen = 1;
|
||||
return TOK_NEWLINE;
|
||||
}
|
||||
|
||||
// Carriage return (handle CR, CRLF)
|
||||
if (c == '\r') {
|
||||
advance(lex);
|
||||
|
||||
if (!atEnd(lex) && peek(lex) == '\n') {
|
||||
advance(lex);
|
||||
}
|
||||
|
||||
lex->token.type = TOK_NEWLINE;
|
||||
lex->token.text[0] = '\n';
|
||||
lex->token.text[1] = '\0';
|
||||
lex->token.textLen = 1;
|
||||
return TOK_NEWLINE;
|
||||
}
|
||||
|
||||
// Comment (apostrophe)
|
||||
if (c == '\'') {
|
||||
skipLineComment(lex);
|
||||
lex->token.type = TOK_NEWLINE;
|
||||
lex->token.text[0] = '\n';
|
||||
lex->token.text[1] = '\0';
|
||||
lex->token.textLen = 1;
|
||||
return TOK_NEWLINE;
|
||||
}
|
||||
|
||||
// String literal
|
||||
if (c == '"') {
|
||||
lex->token.type = tokenizeString(lex);
|
||||
return lex->token.type;
|
||||
}
|
||||
|
||||
// Number
|
||||
if (isdigit((unsigned char)c) || (c == '.' && isdigit((unsigned char)peekNext(lex)))) {
|
||||
lex->token.type = tokenizeNumber(lex);
|
||||
return lex->token.type;
|
||||
}
|
||||
|
||||
// Numeric-base literals: &H hex, &O octal, &B binary. &B is an
|
||||
// extension beyond classic QBASIC; it's convenient for bitmask
|
||||
// work in the widget/graphics code.
|
||||
if (c == '&') {
|
||||
char n = upperChar(peekNext(lex));
|
||||
|
||||
if (n == 'H' || n == 'O' || n == 'B') {
|
||||
lex->token.type = tokenizeHexLiteral(lex);
|
||||
return lex->token.type;
|
||||
}
|
||||
}
|
||||
|
||||
// Identifier or keyword
|
||||
if (isalpha((unsigned char)c) || c == '_') {
|
||||
lex->token.type = tokenizeIdentOrKeyword(lex);
|
||||
return lex->token.type;
|
||||
}
|
||||
|
||||
// Single and multi-character operators/punctuation
|
||||
advance(lex);
|
||||
|
||||
switch (c) {
|
||||
case '+':
|
||||
lex->token.type = TOK_PLUS;
|
||||
break;
|
||||
|
||||
case '-':
|
||||
lex->token.type = TOK_MINUS;
|
||||
break;
|
||||
|
||||
case '*':
|
||||
lex->token.type = TOK_STAR;
|
||||
break;
|
||||
|
||||
case '/':
|
||||
lex->token.type = TOK_SLASH;
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
lex->token.type = TOK_BACKSLASH;
|
||||
break;
|
||||
|
||||
case '^':
|
||||
lex->token.type = TOK_CARET;
|
||||
break;
|
||||
|
||||
case '&':
|
||||
lex->token.type = TOK_AMPERSAND;
|
||||
break;
|
||||
|
||||
case '(':
|
||||
lex->token.type = TOK_LPAREN;
|
||||
break;
|
||||
|
||||
case ')':
|
||||
lex->token.type = TOK_RPAREN;
|
||||
break;
|
||||
|
||||
case ',':
|
||||
lex->token.type = TOK_COMMA;
|
||||
break;
|
||||
|
||||
case ';':
|
||||
lex->token.type = TOK_SEMICOLON;
|
||||
break;
|
||||
|
||||
case ':':
|
||||
lex->token.type = TOK_COLON;
|
||||
break;
|
||||
|
||||
case '.':
|
||||
lex->token.type = TOK_DOT;
|
||||
break;
|
||||
|
||||
case '#':
|
||||
lex->token.type = TOK_HASH;
|
||||
break;
|
||||
|
||||
case '?':
|
||||
lex->token.type = TOK_PRINT;
|
||||
break;
|
||||
|
||||
case '=':
|
||||
lex->token.type = TOK_EQ;
|
||||
break;
|
||||
|
||||
case '<':
|
||||
if (!atEnd(lex) && peek(lex) == '>') {
|
||||
advance(lex);
|
||||
lex->token.type = TOK_NE;
|
||||
} else if (!atEnd(lex) && peek(lex) == '=') {
|
||||
advance(lex);
|
||||
lex->token.type = TOK_LE;
|
||||
} else {
|
||||
lex->token.type = TOK_LT;
|
||||
}
|
||||
break;
|
||||
|
||||
case '>':
|
||||
if (!atEnd(lex) && peek(lex) == '=') {
|
||||
advance(lex);
|
||||
lex->token.type = TOK_GE;
|
||||
} else {
|
||||
lex->token.type = TOK_GT;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
setError(lex, "Unexpected character");
|
||||
lex->token.type = TOK_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
// Store the operator text
|
||||
if (lex->token.type != TOK_ERROR) {
|
||||
lex->token.text[0] = c;
|
||||
lex->token.textLen = 1;
|
||||
|
||||
if (lex->token.type == TOK_NE || lex->token.type == TOK_LE || lex->token.type == TOK_GE) {
|
||||
lex->token.text[1] = lex->source[lex->pos - 1];
|
||||
lex->token.textLen = 2;
|
||||
}
|
||||
|
||||
lex->token.text[lex->token.textLen] = '\0';
|
||||
}
|
||||
|
||||
return lex->token.type;
|
||||
}
|
||||
|
||||
|
||||
BasTokenTypeE basLexerPeek(const BasLexerT *lex) {
|
||||
return lex->token.type;
|
||||
}
|
||||
|
||||
|
||||
const char *basTokenName(BasTokenTypeE type) {
|
||||
switch (type) {
|
||||
case TOK_INT_LIT: return "integer";
|
||||
case TOK_LONG_LIT: return "long";
|
||||
case TOK_FLOAT_LIT: return "float";
|
||||
case TOK_STRING_LIT: return "string";
|
||||
case TOK_IDENT: return "identifier";
|
||||
case TOK_DOT: return "'.'";
|
||||
case TOK_COMMA: return "','";
|
||||
case TOK_SEMICOLON: return "';'";
|
||||
case TOK_COLON: return "':'";
|
||||
case TOK_LPAREN: return "'('";
|
||||
case TOK_RPAREN: return "')'";
|
||||
case TOK_HASH: return "'#'";
|
||||
case TOK_PLUS: return "'+'";
|
||||
case TOK_MINUS: return "'-'";
|
||||
case TOK_STAR: return "'*'";
|
||||
case TOK_SLASH: return "'/'";
|
||||
case TOK_BACKSLASH: return "'\\'";
|
||||
case TOK_CARET: return "'^'";
|
||||
case TOK_AMPERSAND: return "'&'";
|
||||
case TOK_EQ: return "'='";
|
||||
case TOK_NE: return "'<>'";
|
||||
case TOK_LT: return "'<'";
|
||||
case TOK_GT: return "'>'";
|
||||
case TOK_LE: return "'<='";
|
||||
case TOK_GE: return "'>='";
|
||||
case TOK_NEWLINE: return "newline";
|
||||
case TOK_EOF: return "end of file";
|
||||
case TOK_ERROR: return "error";
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Keywords
|
||||
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
|
||||
if (sKeywords[i].type == type) {
|
||||
return sKeywords[i].text;
|
||||
}
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
|
||||
|
||||
static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
|
||||
// Case-insensitive keyword lookup. Short-circuits on length mismatch
|
||||
// (via cached keyword length) and on the very first character, both of
|
||||
// which reject the vast majority of entries before doing a full scan.
|
||||
char firstUp = (text[0] >= 'a' && text[0] <= 'z') ? (char)(text[0] - 32) : text[0];
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
|
||||
const KeywordEntryT *kw = &sKeywords[i];
|
||||
|
||||
if (kw->textLen != len || kw->text[0] != firstUp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool match = true;
|
||||
|
||||
for (int32_t j = 1; j < len; j++) {
|
||||
if (upperChar(text[j]) != kw->text[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
return kw->type;
|
||||
}
|
||||
}
|
||||
|
||||
return TOK_IDENT;
|
||||
}
|
||||
|
||||
|
||||
static char peek(const BasLexerT *lex) {
|
||||
if (atEnd(lex)) {
|
||||
return '\0';
|
||||
}
|
||||
|
||||
return lex->source[lex->pos];
|
||||
}
|
||||
|
||||
|
||||
static char peekNext(const BasLexerT *lex) {
|
||||
if (lex->pos + 1 >= lex->sourceLen) {
|
||||
return '\0';
|
||||
}
|
||||
|
||||
return lex->source[lex->pos + 1];
|
||||
}
|
||||
|
||||
|
||||
static void setError(BasLexerT *lex, const char *msg) {
|
||||
snprintf(lex->error, sizeof(lex->error), "Line %d, Col %d: %s", (int)lex->line, (int)lex->col, msg);
|
||||
}
|
||||
|
||||
|
||||
static void skipLineComment(BasLexerT *lex) {
|
||||
while (!atEnd(lex) && peek(lex) != '\n' && peek(lex) != '\r') {
|
||||
advance(lex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Skips spaces and tabs. Does NOT skip newlines (they are tokens).
|
||||
// Handles line continuation: underscore followed by newline joins
|
||||
// the next line to the current logical line.
|
||||
static void skipWhitespace(BasLexerT *lex) {
|
||||
while (!atEnd(lex)) {
|
||||
char c = peek(lex);
|
||||
|
||||
if (c == ' ' || c == '\t') {
|
||||
advance(lex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line continuation: _ at end of line
|
||||
if (c == '_') {
|
||||
int32_t savedPos = lex->pos;
|
||||
int32_t savedLine = lex->line;
|
||||
int32_t savedCol = lex->col;
|
||||
advance(lex);
|
||||
|
||||
// Skip spaces/tabs after underscore
|
||||
while (!atEnd(lex) && (peek(lex) == ' ' || peek(lex) == '\t')) {
|
||||
advance(lex);
|
||||
}
|
||||
|
||||
// Must be followed by newline
|
||||
if (!atEnd(lex) && (peek(lex) == '\n' || peek(lex) == '\r')) {
|
||||
advance(lex);
|
||||
|
||||
if (!atEnd(lex) && peek(lex) == '\n' && lex->source[lex->pos - 1] == '\r') {
|
||||
advance(lex);
|
||||
}
|
||||
|
||||
continue; // Continue skipping whitespace on next line
|
||||
}
|
||||
|
||||
// Not a continuation -- put back
|
||||
lex->pos = savedPos;
|
||||
lex->line = savedLine;
|
||||
lex->col = savedCol;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
|
||||
advance(lex); // skip &
|
||||
char base = upperChar(peek(lex));
|
||||
advance(lex); // skip H/O/B
|
||||
|
||||
int32_t shift;
|
||||
int32_t maxDigit;
|
||||
|
||||
if (base == 'O') {
|
||||
shift = 3;
|
||||
maxDigit = 7;
|
||||
} else if (base == 'B') {
|
||||
shift = 1;
|
||||
maxDigit = 1;
|
||||
} else {
|
||||
shift = 4;
|
||||
maxDigit = 15;
|
||||
}
|
||||
|
||||
int32_t idx = 0;
|
||||
int32_t value = 0;
|
||||
|
||||
for (;;) {
|
||||
if (atEnd(lex)) {
|
||||
break;
|
||||
}
|
||||
|
||||
char c = peek(lex);
|
||||
int32_t digit;
|
||||
|
||||
if (c >= '0' && c <= '9') {
|
||||
digit = c - '0';
|
||||
} else if (shift == 4 && c >= 'A' && c <= 'F') {
|
||||
digit = c - 'A' + 10;
|
||||
} else if (shift == 4 && c >= 'a' && c <= 'f') {
|
||||
digit = c - 'a' + 10;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if (digit > maxDigit) {
|
||||
break;
|
||||
}
|
||||
|
||||
advance(lex);
|
||||
|
||||
if (idx < BAS_MAX_TOKEN_LEN - 1) {
|
||||
lex->token.text[idx++] = c;
|
||||
}
|
||||
|
||||
value = (value << shift) | digit;
|
||||
}
|
||||
|
||||
lex->token.text[idx] = '\0';
|
||||
lex->token.textLen = idx;
|
||||
|
||||
// Check for trailing & (long suffix)
|
||||
if (!atEnd(lex) && peek(lex) == '&') {
|
||||
advance(lex);
|
||||
lex->token.longVal = (int64_t)value;
|
||||
return TOK_LONG_LIT;
|
||||
}
|
||||
|
||||
lex->token.intVal = value;
|
||||
return TOK_INT_LIT;
|
||||
}
|
||||
|
||||
|
||||
static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
|
||||
int32_t idx = 0;
|
||||
|
||||
while (!atEnd(lex) && (isalnum((unsigned char)peek(lex)) || peek(lex) == '_')) {
|
||||
char c = advance(lex);
|
||||
|
||||
if (idx < BAS_MAX_TOKEN_LEN - 1) {
|
||||
lex->token.text[idx++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
lex->token.text[idx] = '\0';
|
||||
lex->token.textLen = idx;
|
||||
|
||||
// Check for type suffix
|
||||
if (!atEnd(lex)) {
|
||||
char c = peek(lex);
|
||||
|
||||
if (c == '%' || c == '&' || c == '!' || c == '#' || c == '$') {
|
||||
advance(lex);
|
||||
lex->token.text[idx++] = c;
|
||||
lex->token.text[idx] = '\0';
|
||||
lex->token.textLen = idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a keyword
|
||||
// For suffix-bearing identifiers, only check the base (without suffix)
|
||||
int32_t baseLen = idx;
|
||||
|
||||
if (baseLen > 0) {
|
||||
char last = lex->token.text[baseLen - 1];
|
||||
|
||||
if (last == '%' || last == '&' || last == '!' || last == '#' || last == '$') {
|
||||
baseLen--;
|
||||
}
|
||||
}
|
||||
|
||||
// Try the full text first (including any type suffix). Suffix-bearing
|
||||
// keywords like CURDIR$, DIR$, INIREAD$, INPUTBOX$ are listed in the
|
||||
// keyword table with their $ and will match here. If the full text
|
||||
// isn't a keyword, fall back to the base name (without suffix).
|
||||
BasTokenTypeE kwType = lookupKeyword(lex->token.text, idx);
|
||||
bool matchedWithSuffix = (kwType != TOK_IDENT && baseLen != idx);
|
||||
|
||||
if (kwType == TOK_IDENT && baseLen != idx) {
|
||||
kwType = lookupKeyword(lex->token.text, baseLen);
|
||||
}
|
||||
|
||||
// REM is a comment -- skip to end of line
|
||||
if (kwType == TOK_REM) {
|
||||
skipLineComment(lex);
|
||||
lex->token.type = TOK_NEWLINE;
|
||||
lex->token.text[0] = '\n';
|
||||
lex->token.text[1] = '\0';
|
||||
lex->token.textLen = 1;
|
||||
return TOK_NEWLINE;
|
||||
}
|
||||
|
||||
// Accept the keyword if it's a plain keyword (no suffix on source) or
|
||||
// if it explicitly matched a $-suffixed entry in the keyword table.
|
||||
if (kwType != TOK_IDENT && (baseLen == idx || matchedWithSuffix)) {
|
||||
return kwType;
|
||||
}
|
||||
|
||||
return TOK_IDENT;
|
||||
}
|
||||
|
||||
|
||||
static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
|
||||
int32_t idx = 0;
|
||||
bool hasDecimal = false;
|
||||
bool hasExp = false;
|
||||
|
||||
// Integer part
|
||||
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
||||
if (idx < BAS_MAX_TOKEN_LEN - 1) {
|
||||
lex->token.text[idx++] = advance(lex);
|
||||
} else {
|
||||
advance(lex);
|
||||
}
|
||||
}
|
||||
|
||||
// Decimal part
|
||||
if (!atEnd(lex) && peek(lex) == '.' && isdigit((unsigned char)peekNext(lex))) {
|
||||
hasDecimal = true;
|
||||
lex->token.text[idx++] = advance(lex); // .
|
||||
|
||||
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
||||
if (idx < BAS_MAX_TOKEN_LEN - 1) {
|
||||
lex->token.text[idx++] = advance(lex);
|
||||
} else {
|
||||
advance(lex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exponent
|
||||
if (!atEnd(lex) && (upperChar(peek(lex)) == 'E' || upperChar(peek(lex)) == 'D')) {
|
||||
hasExp = true;
|
||||
lex->token.text[idx++] = advance(lex);
|
||||
|
||||
if (!atEnd(lex) && (peek(lex) == '+' || peek(lex) == '-')) {
|
||||
lex->token.text[idx++] = advance(lex);
|
||||
}
|
||||
|
||||
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
||||
if (idx < BAS_MAX_TOKEN_LEN - 1) {
|
||||
lex->token.text[idx++] = advance(lex);
|
||||
} else {
|
||||
advance(lex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lex->token.text[idx] = '\0';
|
||||
lex->token.textLen = idx;
|
||||
|
||||
// Check for type suffix
|
||||
if (!atEnd(lex)) {
|
||||
char c = peek(lex);
|
||||
|
||||
if (c == '%') {
|
||||
advance(lex);
|
||||
lex->token.intVal = (int32_t)atoi(lex->token.text);
|
||||
return TOK_INT_LIT;
|
||||
}
|
||||
|
||||
if (c == '&') {
|
||||
advance(lex);
|
||||
lex->token.longVal = (int64_t)atol(lex->token.text);
|
||||
return TOK_LONG_LIT;
|
||||
}
|
||||
|
||||
if (c == '!') {
|
||||
advance(lex);
|
||||
lex->token.dblVal = atof(lex->token.text);
|
||||
return TOK_FLOAT_LIT;
|
||||
}
|
||||
|
||||
if (c == '#') {
|
||||
advance(lex);
|
||||
lex->token.dblVal = atof(lex->token.text);
|
||||
return TOK_FLOAT_LIT;
|
||||
}
|
||||
}
|
||||
|
||||
// No suffix: determine type from content
|
||||
if (hasDecimal || hasExp) {
|
||||
lex->token.dblVal = atof(lex->token.text);
|
||||
return TOK_FLOAT_LIT;
|
||||
}
|
||||
|
||||
long val = atol(lex->token.text);
|
||||
|
||||
if (val >= INT16_MIN && val <= INT16_MAX) {
|
||||
lex->token.intVal = (int32_t)val;
|
||||
return TOK_INT_LIT;
|
||||
}
|
||||
|
||||
lex->token.longVal = (int64_t)val;
|
||||
return TOK_LONG_LIT;
|
||||
}
|
||||
|
||||
|
||||
static BasTokenTypeE tokenizeString(BasLexerT *lex) {
|
||||
advance(lex); // skip opening quote
|
||||
|
||||
int32_t idx = 0;
|
||||
|
||||
while (!atEnd(lex) && peek(lex) != '"' && peek(lex) != '\n' && peek(lex) != '\r') {
|
||||
if (idx < BAS_MAX_TOKEN_LEN - 1) {
|
||||
lex->token.text[idx++] = advance(lex);
|
||||
} else {
|
||||
advance(lex);
|
||||
}
|
||||
}
|
||||
|
||||
if (atEnd(lex) || peek(lex) != '"') {
|
||||
setError(lex, "Unterminated string literal");
|
||||
lex->token.text[idx] = '\0';
|
||||
lex->token.textLen = idx;
|
||||
return TOK_ERROR;
|
||||
}
|
||||
|
||||
advance(lex); // skip closing quote
|
||||
|
||||
lex->token.text[idx] = '\0';
|
||||
lex->token.textLen = idx;
|
||||
|
||||
return TOK_STRING_LIT;
|
||||
}
|
||||
|
||||
|
||||
static char upperChar(char c) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
return c - 32;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
293
src/apps/kpunch/dvxbasic/compiler/lexer.h
Normal file
293
src/apps/kpunch/dvxbasic/compiler/lexer.h
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// lexer.h -- DVX BASIC lexer (tokenizer)
|
||||
//
|
||||
// Converts BASIC source text into a stream of tokens. Case-insensitive
|
||||
// for keywords. Handles line continuations (_), comments (' and REM),
|
||||
// type suffixes (%, &, !, #, $), and string literals.
|
||||
//
|
||||
// Embeddable: no DVX dependencies, pure C.
|
||||
|
||||
#ifndef DVXBASIC_LEXER_H
|
||||
#define DVXBASIC_LEXER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ============================================================
|
||||
// Token types
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
// Literals
|
||||
TOK_INT_LIT, // integer literal (123, &HFF)
|
||||
TOK_LONG_LIT, // long literal (123&)
|
||||
TOK_FLOAT_LIT, // float literal (3.14, 1.5E10)
|
||||
TOK_STRING_LIT, // "string literal"
|
||||
|
||||
// Identifiers and symbols
|
||||
TOK_IDENT, // variable/function name
|
||||
TOK_DOT, // .
|
||||
TOK_COMMA, // ,
|
||||
TOK_SEMICOLON, // ;
|
||||
TOK_COLON, // :
|
||||
TOK_LPAREN, // (
|
||||
TOK_RPAREN, // )
|
||||
TOK_HASH, // # (file channel)
|
||||
|
||||
// Operators
|
||||
TOK_PLUS, // +
|
||||
TOK_MINUS, // -
|
||||
TOK_STAR, // *
|
||||
TOK_SLASH, // /
|
||||
TOK_BACKSLASH, // \ (integer divide)
|
||||
TOK_CARET, // ^
|
||||
TOK_AMPERSAND, // & (string concat or hex prefix)
|
||||
TOK_EQ, // =
|
||||
TOK_NE, // <>
|
||||
TOK_LT, // <
|
||||
TOK_GT, // >
|
||||
TOK_LE, // <=
|
||||
TOK_GE, // >=
|
||||
|
||||
// Type suffixes (attached to identifier)
|
||||
TOK_SUFFIX_INT, // %
|
||||
TOK_SUFFIX_LONG, // &
|
||||
TOK_SUFFIX_SINGLE, // !
|
||||
TOK_SUFFIX_DOUBLE, // #
|
||||
TOK_SUFFIX_STRING, // $
|
||||
|
||||
// Keywords
|
||||
TOK_AND,
|
||||
TOK_APP,
|
||||
TOK_AS,
|
||||
TOK_BASE,
|
||||
TOK_BOOLEAN,
|
||||
TOK_BYVAL,
|
||||
TOK_CALL,
|
||||
TOK_CASE,
|
||||
TOK_CLOSE,
|
||||
TOK_CONST,
|
||||
TOK_CREATECONTROL,
|
||||
TOK_CREATEFORM,
|
||||
TOK_DATA,
|
||||
TOK_DECLARE,
|
||||
TOK_DEF,
|
||||
TOK_DEFDBL,
|
||||
TOK_DEFINT,
|
||||
TOK_DEFLNG,
|
||||
TOK_DEFSNG,
|
||||
TOK_DEFSTR,
|
||||
TOK_DIM,
|
||||
TOK_DO,
|
||||
TOK_DOEVENTS,
|
||||
TOK_DOUBLE,
|
||||
TOK_ELSE,
|
||||
TOK_ELSEIF,
|
||||
TOK_END,
|
||||
TOK_EOF_KW, // EOF (keyword, not end-of-file)
|
||||
TOK_EQV,
|
||||
TOK_ERASE,
|
||||
TOK_ERR,
|
||||
TOK_ERROR_KW,
|
||||
TOK_EXPLICIT,
|
||||
TOK_EXIT,
|
||||
TOK_FALSE_KW,
|
||||
TOK_FOR,
|
||||
TOK_FUNCTION,
|
||||
TOK_GET,
|
||||
TOK_GOSUB,
|
||||
TOK_GOTO,
|
||||
TOK_HIDE,
|
||||
TOK_IF,
|
||||
TOK_IMP,
|
||||
TOK_INPUT,
|
||||
TOK_INTEGER,
|
||||
TOK_IS,
|
||||
TOK_LBOUND,
|
||||
TOK_LET,
|
||||
TOK_LINE,
|
||||
TOK_LOAD,
|
||||
TOK_LONG,
|
||||
TOK_LOOP,
|
||||
TOK_ME,
|
||||
TOK_MOD,
|
||||
TOK_INPUTBOX,
|
||||
TOK_MSGBOX,
|
||||
TOK_NEXT,
|
||||
TOK_NOT,
|
||||
TOK_NOTHING,
|
||||
TOK_ON,
|
||||
TOK_OPEN,
|
||||
TOK_OPTIONAL,
|
||||
TOK_OPTION,
|
||||
TOK_OR,
|
||||
TOK_OUTPUT,
|
||||
TOK_PRESERVE,
|
||||
TOK_PRINT,
|
||||
TOK_PUT,
|
||||
TOK_RANDOMIZE,
|
||||
TOK_READ,
|
||||
TOK_REDIM,
|
||||
TOK_REM,
|
||||
TOK_REMOVECONTROL,
|
||||
TOK_RESTORE,
|
||||
TOK_RESUME,
|
||||
TOK_RETURN,
|
||||
TOK_SEEK,
|
||||
TOK_SELECT,
|
||||
TOK_SET,
|
||||
TOK_SETEVENT,
|
||||
TOK_SHARED,
|
||||
TOK_SHELL,
|
||||
TOK_SHOW,
|
||||
TOK_SINGLE,
|
||||
TOK_SLEEP,
|
||||
TOK_INIREAD,
|
||||
TOK_INIWRITE,
|
||||
TOK_STATIC,
|
||||
TOK_STEP,
|
||||
TOK_STRING_KW,
|
||||
TOK_SUB,
|
||||
TOK_SWAP,
|
||||
TOK_THEN,
|
||||
TOK_TIMER,
|
||||
TOK_TO,
|
||||
TOK_TRUE_KW,
|
||||
TOK_TYPE,
|
||||
TOK_UBOUND,
|
||||
TOK_UNLOAD,
|
||||
TOK_UNTIL,
|
||||
TOK_WEND,
|
||||
TOK_WHILE,
|
||||
TOK_WITH,
|
||||
TOK_WRITE,
|
||||
TOK_XOR,
|
||||
|
||||
// Filesystem keywords
|
||||
TOK_CHDIR,
|
||||
TOK_CHDRIVE,
|
||||
TOK_CURDIR,
|
||||
TOK_DIR,
|
||||
TOK_FILECOPY,
|
||||
TOK_FILELEN,
|
||||
TOK_GETATTR,
|
||||
TOK_KILL,
|
||||
TOK_MKDIR,
|
||||
TOK_NAME,
|
||||
TOK_RMDIR,
|
||||
TOK_SETATTR,
|
||||
|
||||
// File modes
|
||||
TOK_APPEND,
|
||||
TOK_BINARY,
|
||||
TOK_RANDOM,
|
||||
|
||||
// Special
|
||||
TOK_NEWLINE, // end of logical line
|
||||
TOK_EOF, // end of source
|
||||
TOK_ERROR // lexer error
|
||||
} BasTokenTypeE;
|
||||
|
||||
// ============================================================
|
||||
// Token
|
||||
// ============================================================
|
||||
|
||||
#define BAS_MAX_TOKEN_LEN 256
|
||||
#define BAS_LEX_ERROR_LEN 256
|
||||
|
||||
typedef struct {
|
||||
BasTokenTypeE type;
|
||||
int32_t line; // 1-based source line number
|
||||
int32_t col; // 1-based column number
|
||||
|
||||
// Value (depends on type)
|
||||
union {
|
||||
int32_t intVal;
|
||||
int64_t longVal;
|
||||
float fltVal;
|
||||
double dblVal;
|
||||
};
|
||||
|
||||
char text[BAS_MAX_TOKEN_LEN]; // raw text of the token
|
||||
int32_t textLen;
|
||||
} BasTokenT;
|
||||
|
||||
// ============================================================
|
||||
// Lexer state
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
const char *source; // source text (not owned)
|
||||
int32_t sourceLen;
|
||||
int32_t pos; // current position in source
|
||||
int32_t line; // current line (1-based)
|
||||
int32_t col; // current column (1-based)
|
||||
BasTokenT token; // current token
|
||||
char error[BAS_LEX_ERROR_LEN];
|
||||
} BasLexerT;
|
||||
|
||||
// ============================================================
|
||||
// API
|
||||
// ============================================================
|
||||
|
||||
// Initialize lexer with source text. The source must remain valid
|
||||
// for the lifetime of the lexer.
|
||||
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
|
||||
|
||||
// Advance to the next token. Returns the token type.
|
||||
// The token is available in lex->token.
|
||||
BasTokenTypeE basLexerNext(BasLexerT *lex);
|
||||
|
||||
// Peek at the current token type without advancing.
|
||||
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
|
||||
|
||||
// Return human-readable name for a token type.
|
||||
const char *basTokenName(BasTokenTypeE type);
|
||||
|
||||
// ============================================================
|
||||
// Keyword iteration
|
||||
// ============================================================
|
||||
//
|
||||
// The lexer's internal keyword table is the one source of truth.
|
||||
// Other modules (e.g. the IDE syntax highlighter) iterate through
|
||||
// it here instead of keeping their own parallel list.
|
||||
|
||||
typedef enum {
|
||||
BAS_KW_CLASS_OTHER = 0, // control-flow / statement / operator keyword
|
||||
BAS_KW_CLASS_TYPE = 1, // INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
|
||||
BAS_KW_CLASS_LITERAL = 2 // TRUE, FALSE, NOTHING
|
||||
} BasKeywordClassE;
|
||||
|
||||
// Number of keywords in the table (excluding the trailing NULL sentinel
|
||||
// and any duplicate dollar-suffixed aliases like DIR$/DIR).
|
||||
int32_t basLexerKeywordCount(void);
|
||||
|
||||
// Return the text of keyword i (uppercase). i must be in [0, count).
|
||||
// Duplicates such as DIR and DIR$ are both returned at their own index.
|
||||
const char *basLexerKeywordAt(int32_t i);
|
||||
|
||||
// Classify the keyword at index i.
|
||||
BasKeywordClassE basLexerKeywordClass(int32_t i);
|
||||
|
||||
#endif // DVXBASIC_LEXER_H
|
||||
625
src/apps/kpunch/dvxbasic/compiler/obfuscate.c
Normal file
625
src/apps/kpunch/dvxbasic/compiler/obfuscate.c
Normal file
|
|
@ -0,0 +1,625 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// obfuscate.c -- Release build name obfuscation
|
||||
//
|
||||
// See obfuscate.h for the high-level description.
|
||||
|
||||
#include "obfuscate.h"
|
||||
#include "basEvents.h"
|
||||
#include "../runtime/values.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// ============================================================
|
||||
// Name map
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
char *orig; // original name (strdup'd, case-preserved)
|
||||
char *mapped; // new name (strdup'd, "C1" .. "Cn")
|
||||
} NameEntryT;
|
||||
|
||||
typedef struct {
|
||||
NameEntryT *entries;
|
||||
int32_t count;
|
||||
int32_t cap;
|
||||
} NameMapT;
|
||||
|
||||
|
||||
// Function prototypes (alphabetical)
|
||||
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
|
||||
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
|
||||
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map);
|
||||
static int32_t findFormEndPos(const char *text, int32_t len);
|
||||
static bool isEventSuffix(const char *suffix);
|
||||
static bool isIdentChar(int c);
|
||||
static bool isValidIdent(const char *name);
|
||||
static const char *nameMapAdd(NameMapT *m, const char *name);
|
||||
static void nameMapFree(NameMapT *m);
|
||||
static void nameMapInit(NameMapT *m);
|
||||
static const char *nameMapLookup(const NameMapT *m, const char *name);
|
||||
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize);
|
||||
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText);
|
||||
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap);
|
||||
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map);
|
||||
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map);
|
||||
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map);
|
||||
static const char *skipWhitespace(const char *p, const char *end);
|
||||
|
||||
// ============================================================
|
||||
// Top-level entry point
|
||||
// ============================================================
|
||||
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) {
|
||||
if (!mod || frmCount < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NameMapT map;
|
||||
nameMapInit(&map);
|
||||
|
||||
// Pass 1: collect all names from all .frm texts
|
||||
for (int32_t i = 0; i < frmCount; i++) {
|
||||
if (frmTexts[i] && frmLens[i] > 0) {
|
||||
collectNamesFromFrm(frmTexts[i], frmLens[i], &map);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: rewrite each .frm
|
||||
for (int32_t i = 0; i < frmCount; i++) {
|
||||
outFrms[i].data = NULL;
|
||||
outFrms[i].len = 0;
|
||||
|
||||
if (!frmTexts[i] || frmLens[i] <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t strippedLen = findFormEndPos(frmTexts[i], frmLens[i]);
|
||||
|
||||
// Allocate generous output buffer (mapped names are usually shorter
|
||||
// than originals, but allow for growth and a trailing newline).
|
||||
int32_t outCap = strippedLen + 1024;
|
||||
uint8_t *outBuf = malloc(outCap);
|
||||
|
||||
if (!outBuf) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap);
|
||||
|
||||
// Ensure trailing newline
|
||||
if (outLen > 0 && outBuf[outLen - 1] != '\n' && outLen < outCap) {
|
||||
outBuf[outLen++] = '\n';
|
||||
}
|
||||
|
||||
outFrms[i].data = outBuf;
|
||||
outFrms[i].len = outLen;
|
||||
}
|
||||
|
||||
// Pass 3: rewrite module
|
||||
rewriteModuleConstants(mod, &map);
|
||||
rewriteModuleProcs(mod, &map);
|
||||
rewriteModuleFormVars(mod, &map);
|
||||
|
||||
nameMapFree(&map);
|
||||
}
|
||||
|
||||
|
||||
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap) {
|
||||
if (!src || srcLen <= 0 || !outBuf || outCap <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t outLen = 0;
|
||||
int32_t i = 0;
|
||||
|
||||
while (i < srcLen) {
|
||||
int32_t lineStart = i;
|
||||
|
||||
while (i < srcLen && src[i] != '\n' && src[i] != '\r') {
|
||||
i++;
|
||||
}
|
||||
|
||||
int32_t lineEnd = i;
|
||||
|
||||
if (i < srcLen && src[i] == '\r') {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i < srcLen && src[i] == '\n') {
|
||||
i++;
|
||||
}
|
||||
|
||||
// Scan for first unquoted ' (comment start).
|
||||
bool inStr = false;
|
||||
int32_t commentStart = -1;
|
||||
|
||||
for (int32_t j = lineStart; j < lineEnd; j++) {
|
||||
char c = src[j];
|
||||
|
||||
if (c == '"') {
|
||||
inStr = !inStr;
|
||||
} else if (c == '\'' && !inStr) {
|
||||
commentStart = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t contentEnd = (commentStart >= 0) ? commentStart : lineEnd;
|
||||
|
||||
// Check for whole-line REM. Find first non-whitespace position.
|
||||
int32_t firstNonWs = lineStart;
|
||||
|
||||
while (firstNonWs < contentEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) {
|
||||
firstNonWs++;
|
||||
}
|
||||
|
||||
if (contentEnd - firstNonWs >= 3 &&
|
||||
strncasecmp(src + firstNonWs, "REM", 3) == 0 &&
|
||||
(contentEnd - firstNonWs == 3 ||
|
||||
src[firstNonWs + 3] == ' ' ||
|
||||
src[firstNonWs + 3] == '\t')) {
|
||||
contentEnd = firstNonWs;
|
||||
}
|
||||
|
||||
// Trim trailing whitespace.
|
||||
while (contentEnd > lineStart && (src[contentEnd - 1] == ' ' || src[contentEnd - 1] == '\t')) {
|
||||
contentEnd--;
|
||||
}
|
||||
|
||||
// Drop lines that have no non-whitespace content.
|
||||
if (contentEnd <= firstNonWs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip leading whitespace -- the form parser trims per line
|
||||
// anyway, so shipping indentation just bloats the embedded resource.
|
||||
int32_t writeLen = contentEnd - firstNonWs;
|
||||
|
||||
if (outLen + writeLen + 1 >= outCap) {
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(outBuf + outLen, src + firstNonWs, writeLen);
|
||||
outLen += writeLen;
|
||||
outBuf[outLen++] = '\n';
|
||||
}
|
||||
|
||||
return outLen;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Pass 1: collect all form/control names from .frm texts
|
||||
// ============================================================
|
||||
|
||||
// Scan a .frm text and add all "Begin <Type> <Name>" names to the map.
|
||||
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) {
|
||||
const char *p = text;
|
||||
const char *end = text + len;
|
||||
|
||||
while (p < end) {
|
||||
// Read one line
|
||||
const char *lineStart = p;
|
||||
|
||||
while (p < end && *p != '\n' && *p != '\r') {
|
||||
p++;
|
||||
}
|
||||
|
||||
const char *lineEnd = p;
|
||||
|
||||
if (p < end && *p == '\r') {
|
||||
p++;
|
||||
}
|
||||
|
||||
if (p < end && *p == '\n') {
|
||||
p++;
|
||||
}
|
||||
|
||||
// Trim leading whitespace
|
||||
const char *l = skipWhitespace(lineStart, lineEnd);
|
||||
|
||||
// Check "Begin "
|
||||
if ((lineEnd - l) < 6 || strncasecmp(l, "Begin ", 6) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
l += 6;
|
||||
l = skipWhitespace(l, lineEnd);
|
||||
|
||||
// Read type name
|
||||
char typeName[64];
|
||||
l = readToken(l, lineEnd, typeName, sizeof(typeName));
|
||||
|
||||
if (typeName[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read control name
|
||||
l = skipWhitespace(l, lineEnd);
|
||||
char ctrlName[64];
|
||||
l = readToken(l, lineEnd, ctrlName, sizeof(ctrlName));
|
||||
|
||||
if (ctrlName[0] && isValidIdent(ctrlName)) {
|
||||
nameMapAdd(map, ctrlName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Pass 2: strip BASIC code from .frm text (everything after outer End)
|
||||
// ============================================================
|
||||
|
||||
// Find the position just after the matching End of the outermost Begin Form.
|
||||
// Returns len of the stripped .frm. If no Begin Form found, returns original len.
|
||||
static int32_t findFormEndPos(const char *text, int32_t len) {
|
||||
int32_t nesting = 0;
|
||||
bool inForm = false;
|
||||
|
||||
const char *p = text;
|
||||
const char *end = text + len;
|
||||
|
||||
while (p < end) {
|
||||
const char *lineStart = p;
|
||||
|
||||
while (p < end && *p != '\n' && *p != '\r') {
|
||||
p++;
|
||||
}
|
||||
|
||||
const char *lineEnd = p;
|
||||
|
||||
if (p < end && *p == '\r') {
|
||||
p++;
|
||||
}
|
||||
|
||||
if (p < end && *p == '\n') {
|
||||
p++;
|
||||
}
|
||||
|
||||
const char *l = skipWhitespace(lineStart, lineEnd);
|
||||
|
||||
if ((lineEnd - l) >= 6 && strncasecmp(l, "Begin ", 6) == 0) {
|
||||
// Check for "Begin Form ..." to set inForm on outer open
|
||||
if (!inForm) {
|
||||
const char *r = l + 6;
|
||||
r = skipWhitespace(r, lineEnd);
|
||||
|
||||
if ((lineEnd - r) >= 5 && strncasecmp(r, "Form ", 5) == 0) {
|
||||
inForm = true;
|
||||
}
|
||||
}
|
||||
|
||||
nesting++;
|
||||
} else if ((lineEnd - l) >= 3 && strncasecmp(l, "End", 3) == 0 &&
|
||||
(lineEnd - l == 3 || l[3] == ' ' || l[3] == '\t' || l[3] == '\r')) {
|
||||
nesting--;
|
||||
|
||||
if (inForm && nesting == 0) {
|
||||
return (int32_t)(p - text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
// Check if suffix is a known event name.
|
||||
static bool isEventSuffix(const char *suffix) {
|
||||
for (int32_t i = 0; basEventSuffixes[i]; i++) {
|
||||
if (strcasecmp(suffix, basEventSuffixes[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Pass 3: rewrite .frm text with mapped names
|
||||
// ============================================================
|
||||
|
||||
// Returns true if c is a valid identifier character.
|
||||
static bool isIdentChar(int c) {
|
||||
return isalnum(c) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
// Check if name is a valid identifier (letters, digits, underscore, starts non-digit)
|
||||
static bool isValidIdent(const char *name) {
|
||||
if (!name || !*name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isalpha((unsigned char)name[0]) && name[0] != '_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const char *p = name; *p; p++) {
|
||||
if (!isalnum((unsigned char)*p) && *p != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Add a name if not already present. Returns mapped name.
|
||||
static const char *nameMapAdd(NameMapT *m, const char *name) {
|
||||
const char *existing = nameMapLookup(m, name);
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (m->count >= m->cap) {
|
||||
int32_t newCap = m->cap == 0 ? 16 : m->cap * 2;
|
||||
NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT));
|
||||
|
||||
if (!newEntries) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
m->entries = newEntries;
|
||||
m->cap = newCap;
|
||||
}
|
||||
|
||||
char mapped[16];
|
||||
snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));
|
||||
|
||||
m->entries[m->count].orig = strdup(name);
|
||||
m->entries[m->count].mapped = strdup(mapped);
|
||||
m->count++;
|
||||
|
||||
return m->entries[m->count - 1].mapped;
|
||||
}
|
||||
|
||||
|
||||
static void nameMapFree(NameMapT *m) {
|
||||
for (int32_t i = 0; i < m->count; i++) {
|
||||
free(m->entries[i].orig);
|
||||
free(m->entries[i].mapped);
|
||||
}
|
||||
|
||||
free(m->entries);
|
||||
m->entries = NULL;
|
||||
m->count = 0;
|
||||
m->cap = 0;
|
||||
}
|
||||
|
||||
|
||||
static void nameMapInit(NameMapT *m) {
|
||||
m->entries = NULL;
|
||||
m->count = 0;
|
||||
m->cap = 0;
|
||||
}
|
||||
|
||||
|
||||
// Look up an original name (case-insensitive). Returns mapped name or NULL.
|
||||
static const char *nameMapLookup(const NameMapT *m, const char *name) {
|
||||
for (int32_t i = 0; i < m->count; i++) {
|
||||
if (strcasecmp(m->entries[i].orig, name) == 0) {
|
||||
return m->entries[i].mapped;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Copy next whitespace-delimited token into buf. Returns pointer after token.
|
||||
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize) {
|
||||
int32_t len = 0;
|
||||
|
||||
while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && len < bufSize - 1) {
|
||||
buf[len++] = *p++;
|
||||
}
|
||||
|
||||
buf[len] = '\0';
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Module rewriting
|
||||
// ============================================================
|
||||
|
||||
// Replace the contents of a constant pool entry with a new string.
|
||||
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) {
|
||||
BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText));
|
||||
|
||||
if (!newStr) {
|
||||
return;
|
||||
}
|
||||
|
||||
basStringUnref(mod->constants[idx]);
|
||||
mod->constants[idx] = newStr;
|
||||
}
|
||||
|
||||
|
||||
// Scan text; for each identifier found outside of strings, if it's in
|
||||
// the map, emit the mapped name instead. Output to out (returns bytes written).
|
||||
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) {
|
||||
int32_t outLen = 0;
|
||||
int32_t i = 0;
|
||||
bool inStr = false;
|
||||
|
||||
while (i < srcLen) {
|
||||
char c = src[i];
|
||||
|
||||
if (c == '"') {
|
||||
inStr = !inStr;
|
||||
|
||||
if (outLen < outCap) {
|
||||
out[outLen++] = (uint8_t)c;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read identifier
|
||||
if (!inStr && (isalpha((unsigned char)c) || c == '_')) {
|
||||
int32_t identStart = i;
|
||||
|
||||
while (i < srcLen && isIdentChar((unsigned char)src[i])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
int32_t identLen = i - identStart;
|
||||
char ident[128];
|
||||
|
||||
if (identLen >= (int32_t)sizeof(ident)) {
|
||||
identLen = (int32_t)sizeof(ident) - 1;
|
||||
}
|
||||
|
||||
memcpy(ident, src + identStart, identLen);
|
||||
ident[identLen] = '\0';
|
||||
|
||||
const char *mapped = nameMapLookup(map, ident);
|
||||
|
||||
if (mapped) {
|
||||
int32_t mLen = (int32_t)strlen(mapped);
|
||||
|
||||
for (int32_t k = 0; k < mLen && outLen < outCap; k++) {
|
||||
out[outLen++] = (uint8_t)mapped[k];
|
||||
}
|
||||
} else {
|
||||
for (int32_t k = 0; k < identLen && outLen < outCap; k++) {
|
||||
out[outLen++] = (uint8_t)ident[k];
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (outLen < outCap) {
|
||||
out[outLen++] = (uint8_t)c;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return outLen;
|
||||
}
|
||||
|
||||
|
||||
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
|
||||
for (int32_t i = 0; i < mod->constCount; i++) {
|
||||
const BasStringT *s = mod->constants[i];
|
||||
|
||||
if (!s) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *mapped = nameMapLookup(map, s->data);
|
||||
|
||||
if (mapped) {
|
||||
replaceConstant(mod, i, mapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map) {
|
||||
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
|
||||
BasFormVarInfoT *fv = &mod->formVarInfo[i];
|
||||
const char *mapped = nameMapLookup(map, fv->formName);
|
||||
|
||||
if (mapped) {
|
||||
snprintf(fv->formName, sizeof(fv->formName), "%s", mapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
|
||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||
BasProcEntryT *proc = &mod->procs[i];
|
||||
|
||||
if (proc->name[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remap the owning form name (used at runtime to bind form-scope
|
||||
// variables). The form itself gets renamed by the same pass.
|
||||
if (proc->formName[0]) {
|
||||
const char *mappedForm = nameMapLookup(map, proc->formName);
|
||||
|
||||
if (mappedForm) {
|
||||
snprintf(proc->formName, sizeof(proc->formName), "%s", mappedForm);
|
||||
}
|
||||
}
|
||||
|
||||
// Find last underscore
|
||||
char *underscore = strrchr(proc->name, '_');
|
||||
|
||||
if (!underscore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *suffix = underscore + 1;
|
||||
|
||||
if (!isEventSuffix(suffix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split on underscore
|
||||
int32_t prefixLen = (int32_t)(underscore - proc->name);
|
||||
char prefix[BAS_MAX_PROC_NAME];
|
||||
|
||||
if (prefixLen >= (int32_t)sizeof(prefix)) {
|
||||
prefixLen = (int32_t)sizeof(prefix) - 1;
|
||||
}
|
||||
|
||||
memcpy(prefix, proc->name, prefixLen);
|
||||
prefix[prefixLen] = '\0';
|
||||
|
||||
const char *mapped = nameMapLookup(map, prefix);
|
||||
|
||||
if (mapped) {
|
||||
char newName[BAS_MAX_PROC_NAME];
|
||||
snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix);
|
||||
snprintf(proc->name, sizeof(proc->name), "%s", newName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// .frm parsing helpers
|
||||
// ============================================================
|
||||
|
||||
// Skip ASCII whitespace. Returns pointer past whitespace.
|
||||
static const char *skipWhitespace(const char *p, const char *end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t')) {
|
||||
p++;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
68
src/apps/kpunch/dvxbasic/compiler/obfuscate.h
Normal file
68
src/apps/kpunch/dvxbasic/compiler/obfuscate.h
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// obfuscate.h -- Release build name obfuscation
|
||||
//
|
||||
// Replaces form and control names with generated tokens (C1, C2, ...)
|
||||
// across the compiled module AND the raw .frm text resources. This
|
||||
// hinders casual decompilation by removing meaningful identifiers
|
||||
// from event handler names (e.g., BtnHello_Click -> C3_Click).
|
||||
|
||||
#ifndef DVXBASIC_OBFUSCATE_H
|
||||
#define DVXBASIC_OBFUSCATE_H
|
||||
|
||||
#include "../runtime/vm.h"
|
||||
|
||||
// One .frm text after obfuscation (data is newly-allocated).
|
||||
typedef struct {
|
||||
uint8_t *data;
|
||||
int32_t len;
|
||||
} BasObfFrmT;
|
||||
|
||||
// Obfuscate form/control names in the module and all .frm texts.
|
||||
//
|
||||
// Reads original names from the Begin declarations in each .frm,
|
||||
// generates C1..Cn, then rewrites:
|
||||
// - The .frm text (form/control name declarations, stripping the
|
||||
// trailing BASIC code section after the outer form closes)
|
||||
// - Module string constants matching any original name
|
||||
// - Procedure names matching <OrigName>_<Event>
|
||||
// - formVarInfo entries keyed by form name
|
||||
//
|
||||
// frmTexts / frmLens: input .frm buffers (one per form).
|
||||
// outFrms: caller-allocated array of frmCount entries; function fills
|
||||
// in .data (malloc'd, caller frees) and .len for each.
|
||||
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
|
||||
|
||||
|
||||
// Strip comments from .frm text.
|
||||
//
|
||||
// Removes both whole-line comments (lines whose first non-whitespace
|
||||
// token is `'` or `REM`) and trailing comments (everything from the
|
||||
// first unquoted `'` to end of line). Pure-whitespace lines are
|
||||
// dropped. Quoted string literals are preserved as-is.
|
||||
//
|
||||
// srcLen: input length; outBuf: caller-allocated buffer of outCap bytes.
|
||||
// Returns the number of bytes written.
|
||||
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
|
||||
|
||||
#endif // DVXBASIC_OBFUSCATE_H
|
||||
412
src/apps/kpunch/dvxbasic/compiler/opcodes.h
Normal file
412
src/apps/kpunch/dvxbasic/compiler/opcodes.h
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// opcodes.h -- DVX BASIC bytecode instruction definitions
|
||||
//
|
||||
// Stack-based p-code for the DVX BASIC virtual machine.
|
||||
// Embeddable: no DVX dependencies, pure C.
|
||||
|
||||
#ifndef DVXBASIC_OPCODES_H
|
||||
#define DVXBASIC_OPCODES_H
|
||||
|
||||
// ============================================================
|
||||
// Variable scope tags
|
||||
// Emitted in bytecode (e.g. OP_FOR scopeTag byte) and consumed
|
||||
// by the VM to choose between globals, call-frame locals, and
|
||||
// form-scoped variables. Numeric values are part of the bytecode
|
||||
// ABI -- do not reorder.
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
SCOPE_GLOBAL = 0,
|
||||
SCOPE_LOCAL = 1,
|
||||
SCOPE_FORM = 2 // per-form variable (persists while form is loaded)
|
||||
} BasScopeE;
|
||||
|
||||
// ============================================================
|
||||
// File channel modes (BasFileChannelT.mode and OP_FILE_OPEN arg).
|
||||
// Numeric values are part of the bytecode ABI -- do not reorder.
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
BAS_FILE_MODE_CLOSED = 0,
|
||||
BAS_FILE_MODE_INPUT = 1,
|
||||
BAS_FILE_MODE_OUTPUT = 2,
|
||||
BAS_FILE_MODE_APPEND = 3,
|
||||
BAS_FILE_MODE_RANDOM = 4,
|
||||
BAS_FILE_MODE_BINARY = 5
|
||||
} BasFileModeE;
|
||||
|
||||
// ============================================================
|
||||
// Data type tags (used in Value representation)
|
||||
// ============================================================
|
||||
|
||||
#define BAS_TYPE_INTEGER 0 // 16-bit signed
|
||||
#define BAS_TYPE_LONG 1 // 32-bit signed
|
||||
#define BAS_TYPE_SINGLE 2 // 32-bit float
|
||||
#define BAS_TYPE_DOUBLE 3 // 64-bit float
|
||||
#define BAS_TYPE_STRING 4 // ref-counted dynamic string
|
||||
#define BAS_TYPE_BOOLEAN 5 // True (-1) or False (0)
|
||||
#define BAS_TYPE_ARRAY 6 // ref-counted array
|
||||
#define BAS_TYPE_UDT 7 // ref-counted user-defined type
|
||||
#define BAS_TYPE_OBJECT 8 // opaque host object (form, control, etc.)
|
||||
#define BAS_TYPE_REF 9 // ByRef pointer to a BasValueT slot
|
||||
|
||||
// ============================================================
|
||||
// Stack operations
|
||||
// ============================================================
|
||||
|
||||
#define OP_NOP 0x00
|
||||
#define OP_PUSH_INT16 0x01 // [int16] push 16-bit integer
|
||||
#define OP_PUSH_INT32 0x02 // [int32] push 32-bit integer
|
||||
#define OP_PUSH_FLT32 0x03 // [float32] push 32-bit float
|
||||
#define OP_PUSH_FLT64 0x04 // [float64] push 64-bit float
|
||||
#define OP_PUSH_STR 0x05 // [uint16 idx] push string from constant pool
|
||||
#define OP_PUSH_TRUE 0x06 // push boolean True (-1)
|
||||
#define OP_PUSH_FALSE 0x07 // push boolean False (0)
|
||||
#define OP_POP 0x08 // discard top of stack
|
||||
#define OP_DUP 0x09 // duplicate top of stack
|
||||
|
||||
// ============================================================
|
||||
// Variable access
|
||||
// ============================================================
|
||||
|
||||
#define OP_LOAD_LOCAL 0x10 // [uint16 idx] push local variable
|
||||
#define OP_STORE_LOCAL 0x11 // [uint16 idx] pop to local variable
|
||||
#define OP_LOAD_GLOBAL 0x12 // [uint16 idx] push global variable
|
||||
#define OP_STORE_GLOBAL 0x13 // [uint16 idx] pop to global variable
|
||||
#define OP_LOAD_REF 0x14 // dereference top of stack (ByRef)
|
||||
#define OP_STORE_REF 0x15 // store through reference on stack
|
||||
#define OP_LOAD_ARRAY 0x16 // [uint8 dims] indices on stack, array ref below
|
||||
#define OP_STORE_ARRAY 0x17 // [uint8 dims] value, indices, array ref on stack
|
||||
#define OP_LOAD_FIELD 0x18 // [uint16 fieldIdx] load UDT field
|
||||
#define OP_STORE_FIELD 0x19 // [uint16 fieldIdx] store UDT field
|
||||
#define OP_PUSH_LOCAL_ADDR 0x1A // [uint16 idx] push address of local (for ByRef)
|
||||
#define OP_PUSH_GLOBAL_ADDR 0x1B // [uint16 idx] push address of global (for ByRef)
|
||||
#define OP_STORE_ARRAY_FIELD 0x1C // [uint8 dims, uint16 fieldIdx] value, indices, array on stack
|
||||
|
||||
// ============================================================
|
||||
// Arithmetic (integer)
|
||||
// ============================================================
|
||||
|
||||
#define OP_ADD_INT 0x20
|
||||
#define OP_SUB_INT 0x21
|
||||
#define OP_MUL_INT 0x22
|
||||
#define OP_IDIV_INT 0x23 // integer divide (\)
|
||||
#define OP_MOD_INT 0x24
|
||||
#define OP_NEG_INT 0x25
|
||||
|
||||
// ============================================================
|
||||
// Arithmetic (float)
|
||||
// ============================================================
|
||||
|
||||
#define OP_ADD_FLT 0x26
|
||||
#define OP_SUB_FLT 0x27
|
||||
#define OP_MUL_FLT 0x28
|
||||
#define OP_DIV_FLT 0x29 // float divide (/)
|
||||
#define OP_NEG_FLT 0x2A
|
||||
#define OP_POW 0x2B // exponentiation (^)
|
||||
|
||||
// ============================================================
|
||||
// String operations
|
||||
// ============================================================
|
||||
|
||||
#define OP_STR_CONCAT 0x30
|
||||
#define OP_STR_LEFT 0x31
|
||||
#define OP_STR_RIGHT 0x32
|
||||
#define OP_STR_MID 0x33 // 3 args: str, start, len
|
||||
#define OP_STR_MID2 0x34 // 2 args: str, start (to end)
|
||||
#define OP_STR_LEN 0x35
|
||||
#define OP_STR_INSTR 0x36 // 2 args: str, find
|
||||
#define OP_STR_INSTR3 0x37 // 3 args: start, str, find
|
||||
#define OP_STR_UCASE 0x38
|
||||
#define OP_STR_LCASE 0x39
|
||||
#define OP_STR_TRIM 0x3A
|
||||
#define OP_STR_LTRIM 0x3B
|
||||
#define OP_STR_RTRIM 0x3C
|
||||
#define OP_STR_CHR 0x3D
|
||||
#define OP_STR_ASC 0x3E
|
||||
#define OP_STR_SPACE 0x3F
|
||||
|
||||
// ============================================================
|
||||
// Comparison (push boolean result)
|
||||
// ============================================================
|
||||
|
||||
#define OP_CMP_EQ 0x40
|
||||
#define OP_CMP_NE 0x41
|
||||
#define OP_CMP_LT 0x42
|
||||
#define OP_CMP_GT 0x43
|
||||
#define OP_CMP_LE 0x44
|
||||
#define OP_CMP_GE 0x45
|
||||
|
||||
// ============================================================
|
||||
// Logical / bitwise
|
||||
// ============================================================
|
||||
|
||||
#define OP_AND 0x48
|
||||
#define OP_OR 0x49
|
||||
#define OP_NOT 0x4A
|
||||
#define OP_XOR 0x4B
|
||||
#define OP_EQV 0x4C
|
||||
#define OP_IMP 0x4D
|
||||
|
||||
// ============================================================
|
||||
// Control flow
|
||||
// ============================================================
|
||||
|
||||
#define OP_JMP 0x50 // [int16 offset] unconditional jump
|
||||
#define OP_JMP_TRUE 0x51 // [int16 offset] jump if TOS is true
|
||||
#define OP_JMP_FALSE 0x52 // [int16 offset] jump if TOS is false
|
||||
#define OP_CALL 0x53 // [uint16 addr] [uint8 argc] [uint8 baseSlot]
|
||||
#define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return)
|
||||
#define OP_RET 0x55 // return from subroutine
|
||||
#define OP_RET_VAL 0x56 // return from function (value on stack)
|
||||
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 scope] [int16 skipOffset] init FOR, skip body if range empty
|
||||
#define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop]
|
||||
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR)
|
||||
|
||||
// ============================================================
|
||||
// Type conversion
|
||||
// ============================================================
|
||||
|
||||
#define OP_CONV_INT_FLT 0x60 // int -> float
|
||||
#define OP_CONV_FLT_INT 0x61 // float -> int (banker's rounding)
|
||||
#define OP_CONV_INT_STR 0x62 // int -> string
|
||||
#define OP_CONV_STR_INT 0x63 // string -> int (VAL)
|
||||
#define OP_CONV_FLT_STR 0x64 // float -> string
|
||||
#define OP_CONV_STR_FLT 0x65 // string -> float (VAL)
|
||||
#define OP_CONV_INT_LONG 0x66 // int16 -> int32
|
||||
#define OP_CONV_LONG_INT 0x67 // int32 -> int16
|
||||
|
||||
// ============================================================
|
||||
// I/O
|
||||
// ============================================================
|
||||
|
||||
#define OP_PRINT 0x70 // print TOS to current output
|
||||
#define OP_PRINT_NL 0x71 // print newline
|
||||
#define OP_PRINT_TAB 0x72 // print tab (14-column zones)
|
||||
#define OP_PRINT_SPC 0x73 // [uint8 n] print n spaces
|
||||
#define OP_INPUT 0x74 // read line into string on stack
|
||||
#define OP_FILE_OPEN 0x75 // [uint8 mode] filename, channel# on stack
|
||||
#define OP_FILE_CLOSE 0x76 // channel# on stack
|
||||
#define OP_FILE_PRINT 0x77 // channel#, value on stack
|
||||
#define OP_FILE_INPUT 0x78 // channel# on stack, push string
|
||||
#define OP_FILE_EOF 0x79 // channel# on stack, push boolean
|
||||
#define OP_FILE_LINE_INPUT 0x7A // channel# on stack, push string
|
||||
|
||||
// ============================================================
|
||||
// UI / Event (used when form system is active)
|
||||
// ============================================================
|
||||
//
|
||||
// All UI opcodes are name-based: control references, property names,
|
||||
// method names, and form names are strings resolved at runtime.
|
||||
// This allows third-party widget DXEs and new properties to work
|
||||
// without recompiling the BASIC runtime.
|
||||
//
|
||||
// Stack convention:
|
||||
// LOAD_PROP: ... controlRef propNameStr -> ... value
|
||||
// STORE_PROP: ... controlRef propNameStr value -> ...
|
||||
// CALL_METHOD: ... controlRef methodNameStr [args] -> ... [result]
|
||||
// LOAD_FORM: ... formNameStr -> ... formRef
|
||||
// CREATE_CTRL: ... formRef typeNameStr nameStr -> ... controlRef
|
||||
|
||||
#define OP_LOAD_PROP 0x80 // pop propName, pop ctrlRef, push property value
|
||||
#define OP_STORE_PROP 0x81 // pop value, pop propName, pop ctrlRef, set property
|
||||
#define OP_CALL_METHOD 0x82 // [uint8 argc] pop methodName, pop ctrlRef, pop args, push result
|
||||
#define OP_LOAD_FORM 0x83 // pop formName string, push form reference
|
||||
#define OP_UNLOAD_FORM 0x84 // pop formRef, unload it
|
||||
#define OP_SHOW_FORM 0x85 // [uint8 modal] pop formRef, show it
|
||||
#define OP_HIDE_FORM 0x86 // pop formRef, hide it
|
||||
#define OP_DO_EVENTS 0x87
|
||||
#define OP_MSGBOX 0x88 // pop flags, pop message string, push result
|
||||
#define OP_INPUTBOX 0x89 // pop default, pop title, pop prompt, push result string
|
||||
#define OP_ME_REF 0x8A // push current form reference
|
||||
#define OP_CREATE_CTRL 0x8B // pop name, pop typeName, pop formRef, push controlRef
|
||||
#define OP_FIND_CTRL 0x8C // pop ctrlName, pop formRef, push controlRef
|
||||
#define OP_CTRL_REF 0x8D // [uint16 nameConstIdx] push named control on current form
|
||||
#define OP_FIND_CTRL_IDX 0x8E // pop index, pop ctrlName, pop formRef, push ctrlRef
|
||||
#define OP_LOAD_FORM_VAR 0x8F // [uint16 idx] push currentFormVars[idx]
|
||||
#define OP_STORE_FORM_VAR 0x9B // [uint16 idx] pop, store to currentFormVars[idx]
|
||||
#define OP_PUSH_FORM_ADDR 0x9C // [uint16 idx] push ¤tFormVars[idx] (ByRef)
|
||||
#define OP_CREATE_CTRL_EX 0x9D // pop parentRef, pop name, pop type, pop formRef, push ctrlRef
|
||||
#define OP_CREATE_FORM 0xF0 // pop height, pop width, pop nameStr, push formRef
|
||||
#define OP_SET_EVENT 0xF1 // pop handlerNameStr, pop eventNameStr, pop ctrlRef
|
||||
#define OP_REMOVE_CTRL 0xF2 // pop ctrlNameStr, pop formRef
|
||||
|
||||
// ============================================================
|
||||
// Array / misc
|
||||
// ============================================================
|
||||
|
||||
#define OP_DIM_ARRAY 0x90 // [uint8 dims] [uint8 type] bounds on stack
|
||||
#define OP_REDIM 0x91 // [uint8 dims] [uint8 preserve] bounds on stack
|
||||
#define OP_ERASE 0x92 // array ref on stack
|
||||
#define OP_LBOUND 0x93 // [uint8 dim] array ref on stack
|
||||
#define OP_UBOUND 0x94 // [uint8 dim] array ref on stack
|
||||
#define OP_ON_ERROR 0x95 // [int16 handler] set error handler (0 = disable)
|
||||
#define OP_RESUME 0x96 // resume after error
|
||||
#define OP_RESUME_NEXT 0x97 // resume at next statement
|
||||
#define OP_RAISE_ERR 0x98 // error number on stack
|
||||
#define OP_ERR_NUM 0x99 // push current error number
|
||||
#define OP_ERR_CLEAR 0x9A // clear error state
|
||||
|
||||
// ============================================================
|
||||
// Math built-ins (single opcode each for common functions)
|
||||
// ============================================================
|
||||
|
||||
#define OP_MATH_ABS 0xA0
|
||||
#define OP_MATH_INT 0xA1 // floor
|
||||
#define OP_MATH_FIX 0xA2 // truncate toward zero
|
||||
#define OP_MATH_SGN 0xA3
|
||||
#define OP_MATH_SQR 0xA4
|
||||
#define OP_MATH_SIN 0xA5
|
||||
#define OP_MATH_COS 0xA6
|
||||
#define OP_MATH_TAN 0xA7
|
||||
#define OP_MATH_ATN 0xA8
|
||||
#define OP_MATH_LOG 0xA9
|
||||
#define OP_MATH_EXP 0xAA
|
||||
#define OP_MATH_RND 0xAB
|
||||
#define OP_MATH_RANDOMIZE 0xAC // seed on stack (or TIMER if -1)
|
||||
#define OP_RGB 0xAD // pop b, g, r; push LONG = (r<<16)|(g<<8)|b
|
||||
#define OP_GET_RED 0xAE // pop LONG color; push (color>>16) & 0xFF
|
||||
#define OP_GET_GREEN 0xAF // pop LONG color; push (color>>8) & 0xFF
|
||||
|
||||
// ============================================================
|
||||
// Conversion built-ins
|
||||
// ============================================================
|
||||
|
||||
#define OP_STR_VAL 0xB0 // VAL(s$) -> number
|
||||
#define OP_STR_STRF 0xB1 // STR$(n) -> string
|
||||
#define OP_STR_HEX 0xB2 // HEX$(n) -> string
|
||||
#define OP_STR_STRING 0xB3 // STRING$(n, char) -> string
|
||||
#define OP_STR_OCT 0xF3 // OCT$(n) -> string
|
||||
#define OP_CONV_BOOL 0xF4 // CBOOL(n) -> -1 (true) or 0 (false)
|
||||
#define OP_PUSH_ARR_ADDR 0xF5 // [uint8 dims] pop dims indices, pop array ref, push REF to element
|
||||
|
||||
// ============================================================
|
||||
// Extended built-ins
|
||||
// ============================================================
|
||||
|
||||
#define OP_MATH_TIMER 0xB4 // push seconds since midnight as DOUBLE
|
||||
#define OP_DATE_STR 0xB5 // push DATE$ string "MM-DD-YYYY"
|
||||
#define OP_TIME_STR 0xB6 // push TIME$ string "HH:MM:SS"
|
||||
#define OP_SLEEP 0xB7 // pop seconds, sleep
|
||||
#define OP_ENVIRON 0xB8 // pop env var name, push value string
|
||||
|
||||
// ============================================================
|
||||
// DATA/READ/RESTORE
|
||||
// ============================================================
|
||||
|
||||
#define OP_READ_DATA 0xB9 // push next value from data pool
|
||||
#define OP_RESTORE 0xBA // reset data pointer to 0
|
||||
|
||||
// ============================================================
|
||||
// WRITE # (comma-delimited with quoted strings)
|
||||
// ============================================================
|
||||
|
||||
#define OP_FILE_WRITE 0xBB // pop channel + value, write in WRITE format
|
||||
#define OP_FILE_WRITE_SEP 0xBC // pop channel, write comma separator
|
||||
#define OP_FILE_WRITE_NL 0xBD // pop channel, write newline
|
||||
|
||||
// ============================================================
|
||||
// Random/Binary file I/O
|
||||
// ============================================================
|
||||
|
||||
#define OP_FILE_GET 0xBE // pop channel + recno, read record, push value
|
||||
#define OP_FILE_PUT 0xBF // pop channel + recno + value, write record
|
||||
#define OP_FILE_SEEK 0xC0 // pop channel + position, seek
|
||||
#define OP_FILE_LOF 0xC1 // pop channel, push file length
|
||||
#define OP_FILE_LOC 0xC2 // pop channel, push current position
|
||||
#define OP_FILE_FREEFILE 0xC3 // push next free channel number
|
||||
#define OP_FILE_INPUT_N 0xC4 // pop channel + n, read n chars, push string
|
||||
|
||||
// ============================================================
|
||||
// Fixed-length strings and MID$ assignment
|
||||
// ============================================================
|
||||
|
||||
#define OP_STR_FIXLEN 0xC5 // [uint16 len] pop string, pad/truncate, push
|
||||
#define OP_STR_MID_ASGN 0xC6 // pop replacement, len, start, str; push modified
|
||||
|
||||
// ============================================================
|
||||
// PRINT USING
|
||||
// ============================================================
|
||||
|
||||
#define OP_PRINT_USING 0xC7 // pop format + value, push formatted string
|
||||
|
||||
// ============================================================
|
||||
// SPC(n) and TAB(n) with stack-based argument
|
||||
// ============================================================
|
||||
|
||||
#define OP_PRINT_TAB_N 0xC8 // pop column count, print spaces to reach column
|
||||
#define OP_PRINT_SPC_N 0xC9 // pop count, print that many spaces
|
||||
#define OP_FORMAT 0xCA // pop format string + value, push formatted string
|
||||
#define OP_SHELL 0xCB // pop command string, call system(), push return value
|
||||
#define OP_COMPARE_MODE 0xCC // [uint8 mode] set string compare mode (0=binary, 1=text)
|
||||
|
||||
// ============================================================
|
||||
// External library calls (DECLARE LIBRARY)
|
||||
// ============================================================
|
||||
//
|
||||
// Calls native functions exported by dynamically loaded libraries.
|
||||
// The VM resolves library + function name on first call via a host
|
||||
// callback, caches the result, and marshals arguments through a
|
||||
// second callback. This allows BASIC programs to use any library
|
||||
// (serial, security, third-party) without recompiling the runtime.
|
||||
|
||||
#define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType]
|
||||
|
||||
#define OP_GET_BLUE 0xD0 // pop LONG color; push color & 0xFF
|
||||
|
||||
// App object
|
||||
#define OP_APP_PATH 0xDD // push App.Path string
|
||||
#define OP_APP_CONFIG 0xDE // push App.Config string
|
||||
#define OP_APP_DATA 0xDF // push App.Data string
|
||||
|
||||
// INI file operations
|
||||
#define OP_INI_READ 0xE0 // pop default, pop key, pop section, pop file, push string
|
||||
#define OP_INI_WRITE 0xE1 // pop value, pop key, pop section, pop file
|
||||
|
||||
// Filesystem operations
|
||||
#define OP_FS_KILL 0xE2 // pop filename, delete file
|
||||
#define OP_FS_NAME 0xE3 // pop newname, pop oldname, rename
|
||||
#define OP_FS_FILECOPY 0xE4 // pop dst, pop src, copy file
|
||||
#define OP_FS_MKDIR 0xE5 // pop path, create directory
|
||||
#define OP_FS_RMDIR 0xE6 // pop path, remove directory
|
||||
#define OP_FS_CHDIR 0xE7 // pop path, change directory
|
||||
#define OP_FS_CHDRIVE 0xE8 // pop drive, change drive
|
||||
#define OP_FS_CURDIR 0xE9 // push current directory string
|
||||
#define OP_FS_DIR 0xEA // pop pattern, push first matching filename
|
||||
#define OP_FS_DIR_NEXT 0xEB // push next matching filename (no args)
|
||||
#define OP_FS_FILELEN 0xEC // pop filename, push file length
|
||||
#define OP_FS_GETATTR 0xED // pop filename, push attributes integer
|
||||
#define OP_FS_SETATTR 0xEE // pop attrs, pop filename, set attributes
|
||||
|
||||
// Debug
|
||||
#define OP_LINE 0xEF // [uint16 lineNum] set current source line for debugger
|
||||
|
||||
// ============================================================
|
||||
// Halt
|
||||
// ============================================================
|
||||
|
||||
#define OP_END 0xFE // explicit END statement -- terminates program
|
||||
#define OP_HALT 0xFF // implicit end of module
|
||||
|
||||
#endif // DVXBASIC_OPCODES_H
|
||||
6449
src/apps/kpunch/dvxbasic/compiler/parser.c
Normal file
6449
src/apps/kpunch/dvxbasic/compiler/parser.c
Normal file
File diff suppressed because it is too large
Load diff
117
src/apps/kpunch/dvxbasic/compiler/parser.h
Normal file
117
src/apps/kpunch/dvxbasic/compiler/parser.h
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// parser.h -- DVX BASIC parser (recursive descent)
|
||||
//
|
||||
// Single-pass compiler: reads tokens from the lexer and emits
|
||||
// p-code directly via the code generator. No AST. Forward
|
||||
// references to SUBs/FUNCTIONs are resolved via backpatching.
|
||||
//
|
||||
// Embeddable: no DVX dependencies, pure C.
|
||||
|
||||
#ifndef DVXBASIC_PARSER_H
|
||||
#define DVXBASIC_PARSER_H
|
||||
|
||||
#include "lexer.h"
|
||||
#include "codegen.h"
|
||||
#include "symtab.h"
|
||||
#include "../runtime/vm.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ============================================================
|
||||
// Parser state
|
||||
// ============================================================
|
||||
|
||||
// Parse-error string sizes. PARSE_ERROR_LEN is the final message stored
|
||||
// on the parser; PARSE_ERR_SCRATCH is used for temporary formatting before
|
||||
// copying into it (with a "Line N: " prefix).
|
||||
#define BAS_PARSE_ERROR_LEN 1024
|
||||
#define BAS_PARSE_ERR_SCRATCH 512
|
||||
|
||||
// Optional compile-time validator for CtrlName.Member references.
|
||||
// The IDE populates this from the project's .frm files + widget DXE
|
||||
// metadata so typos die at compile time instead of at event-click
|
||||
// time. bascomp leaves it NULL (runs on the host, no widget DXEs)
|
||||
// and falls back to the runtime error net. Dynamically-created
|
||||
// controls aren't in the map, so lookupCtrlType returns NULL and
|
||||
// validation is skipped for those -- no false positives.
|
||||
typedef struct {
|
||||
// Return the widget type name for a control declared in a .frm,
|
||||
// or NULL if the control isn't statically known.
|
||||
const char *(*lookupCtrlType)(void *ctx, const char *ctrlName);
|
||||
// Return true if `methodName` is valid for widget type `wgtType`
|
||||
// (or a common method like Refresh/SetFocus). Called with the
|
||||
// type string returned by lookupCtrlType.
|
||||
bool (*isMethodValid)(void *ctx, const char *wgtType, const char *methodName);
|
||||
// Return true if `propName` is a valid property on wgtType.
|
||||
bool (*isPropValid)(void *ctx, const char *wgtType, const char *propName);
|
||||
void *ctx;
|
||||
} BasCtrlValidatorT;
|
||||
|
||||
|
||||
typedef struct {
|
||||
BasLexerT lex;
|
||||
BasCodeGenT cg;
|
||||
BasSymTabT sym;
|
||||
char error[BAS_PARSE_ERROR_LEN];
|
||||
bool hasError;
|
||||
int32_t errorLine;
|
||||
int32_t prevLine; // line of the previous token (for error reporting)
|
||||
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
|
||||
int32_t optionBase; // default array lower bound (0 or 1)
|
||||
bool optionCompareText; // true = case-insensitive string comparison
|
||||
bool optionExplicit; // true = variables must be declared with DIM
|
||||
uint8_t defType[26]; // default type per letter (A-Z), set by DEFINT etc.
|
||||
char currentProc[BAS_MAX_TOKEN_LEN]; // name of current SUB/FUNCTION
|
||||
// Per-form init block tracking
|
||||
int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none)
|
||||
int32_t formInitCodeStart; // code position where init block starts (-1 = none)
|
||||
// Optional compile-time CtrlName.Member validator (IDE-only).
|
||||
const BasCtrlValidatorT *validator;
|
||||
} BasParserT;
|
||||
|
||||
// ============================================================
|
||||
// API
|
||||
// ============================================================
|
||||
|
||||
// Initialize parser with source text.
|
||||
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
|
||||
|
||||
// Attach an optional compile-time validator for CtrlName.Member
|
||||
// references. The parser borrows the pointer -- caller owns the
|
||||
// underlying struct and must keep it alive until basParserFree.
|
||||
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v);
|
||||
|
||||
// Parse the entire source and generate p-code.
|
||||
// Returns true on success, false on error (check p->error).
|
||||
bool basParse(BasParserT *p);
|
||||
|
||||
// Build a module from the parsed code. Returns NULL on error.
|
||||
// Caller owns the module and must free with basModuleFree().
|
||||
BasModuleT *basParserBuildModule(BasParserT *p);
|
||||
|
||||
// Free parser resources.
|
||||
void basParserFree(BasParserT *p);
|
||||
|
||||
#endif // DVXBASIC_PARSER_H
|
||||
141
src/apps/kpunch/dvxbasic/compiler/strip.c
Normal file
141
src/apps/kpunch/dvxbasic/compiler/strip.c
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// strip.c -- Release build stripping
|
||||
//
|
||||
// Removes debug information from a compiled module:
|
||||
// - Clears debug variable info (names, scopes, types)
|
||||
// - Clears debug UDT definitions
|
||||
// - Mangles procedure names that aren't needed for runtime dispatch.
|
||||
// The form runtime dispatches events by name (Control_Event pattern)
|
||||
// and SetEvent looks up handlers by name at runtime, so those proc
|
||||
// names must be preserved. Everything else becomes F1, F2, F3...
|
||||
//
|
||||
// OP_LINE removal is deferred to a future version (requires
|
||||
// bytecode compaction and offset rewriting).
|
||||
|
||||
#include "strip.h"
|
||||
#include "basEvents.h"
|
||||
#include "../runtime/values.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// Events fired by name via basFormRtFireEvent* in formrt.c. Any proc
|
||||
// ending in "_<EventName>" must keep its name so the dispatcher can
|
||||
// find it. Declared in basEvents.h; defined here as the single source
|
||||
// of truth.
|
||||
const char *basEventSuffixes[] = {
|
||||
"Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate",
|
||||
"Click", "DblClick", "Change", "Timer",
|
||||
"GotFocus", "LostFocus",
|
||||
"KeyPress", "KeyDown", "KeyUp",
|
||||
"MouseDown", "MouseUp", "MouseMove",
|
||||
"Scroll", "Reposition", "Validate",
|
||||
NULL
|
||||
};
|
||||
|
||||
|
||||
// Function prototypes (alphabetical)
|
||||
void basStripModule(BasModuleT *mod);
|
||||
static bool nameEndsWithEventSuffix(const char *name);
|
||||
static bool nameInConstantPool(const BasModuleT *mod, const char *name);
|
||||
|
||||
|
||||
void basStripModule(BasModuleT *mod) {
|
||||
if (!mod) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear debug variable info
|
||||
free(mod->debugVars);
|
||||
mod->debugVars = NULL;
|
||||
mod->debugVarCount = 0;
|
||||
|
||||
// Clear debug UDT definitions
|
||||
if (mod->debugUdtDefs) {
|
||||
for (int32_t i = 0; i < mod->debugUdtDefCount; i++) {
|
||||
free(mod->debugUdtDefs[i].fields);
|
||||
}
|
||||
|
||||
free(mod->debugUdtDefs);
|
||||
mod->debugUdtDefs = NULL;
|
||||
mod->debugUdtDefCount = 0;
|
||||
}
|
||||
|
||||
// Mangle proc names. Keep names that are needed for runtime name
|
||||
// lookup: event handlers (Control_Event pattern) and any name
|
||||
// referenced as a string constant (e.g. SetEvent's target name).
|
||||
int32_t nextMangled = 1;
|
||||
|
||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||
BasProcEntryT *proc = &mod->procs[i];
|
||||
|
||||
if (proc->name[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nameEndsWithEventSuffix(proc->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nameInConstantPool(mod, proc->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
snprintf(proc->name, sizeof(proc->name), "F%ld", (long)nextMangled++);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool nameEndsWithEventSuffix(const char *name) {
|
||||
const char *underscore = strrchr(name, '_');
|
||||
|
||||
if (!underscore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *suffix = underscore + 1;
|
||||
|
||||
for (int32_t i = 0; basEventSuffixes[i]; i++) {
|
||||
if (strcasecmp(suffix, basEventSuffixes[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static bool nameInConstantPool(const BasModuleT *mod, const char *name) {
|
||||
for (int32_t i = 0; i < mod->constCount; i++) {
|
||||
const BasStringT *s = mod->constants[i];
|
||||
|
||||
if (s && strcasecmp(s->data, name) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
43
src/apps/kpunch/dvxbasic/compiler/strip.h
Normal file
43
src/apps/kpunch/dvxbasic/compiler/strip.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// strip.h -- Release build stripping
|
||||
//
|
||||
// Removes debug information from a compiled module to hinder
|
||||
// decompilation. Clears debug variable info and debug UDT
|
||||
// definitions, and mangles proc names that aren't needed for
|
||||
// runtime name-based dispatch.
|
||||
|
||||
#ifndef DVXBASIC_STRIP_H
|
||||
#define DVXBASIC_STRIP_H
|
||||
|
||||
#include "../runtime/vm.h"
|
||||
|
||||
// Strip debug info from a module for release builds:
|
||||
// - Clear debug variable info
|
||||
// - Clear debug UDT definitions
|
||||
// - Mangle proc names to F1, F2, ... except for event handlers
|
||||
// (matched by Control_Event suffix) and names referenced as
|
||||
// string constants (SetEvent dispatch targets).
|
||||
void basStripModule(BasModuleT *mod);
|
||||
|
||||
#endif // DVXBASIC_STRIP_H
|
||||
269
src/apps/kpunch/dvxbasic/compiler/symtab.c
Normal file
269
src/apps/kpunch/dvxbasic/compiler/symtab.c
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// symtab.c -- DVX BASIC symbol table implementation
|
||||
|
||||
#include "symtab.h"
|
||||
#include "thirdparty/stb_ds_wrap.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// Function prototypes (alphabetical)
|
||||
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
||||
int32_t basSymTabAllocSlot(BasSymTabT *tab);
|
||||
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
|
||||
void basSymTabEnterLocal(BasSymTabT *tab);
|
||||
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
|
||||
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
|
||||
void basSymTabInit(BasSymTabT *tab);
|
||||
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
|
||||
void basSymTabLeaveLocal(BasSymTabT *tab);
|
||||
static bool namesEqual(const char *a, const char *b);
|
||||
static uint32_t nameHashCI(const char *name);
|
||||
|
||||
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
|
||||
// Determine scope: local > form > global.
|
||||
// Only variables get SCOPE_FORM; SUBs/FUNCTIONs/CONSTs remain global.
|
||||
BasScopeE scope;
|
||||
|
||||
if (tab->inLocalScope) {
|
||||
scope = SCOPE_LOCAL;
|
||||
} else if (tab->inFormScope && kind == SYM_VARIABLE) {
|
||||
scope = SCOPE_FORM;
|
||||
} else {
|
||||
scope = SCOPE_GLOBAL;
|
||||
}
|
||||
|
||||
uint32_t h = nameHashCI(name);
|
||||
|
||||
// Check for duplicate in current scope (skip ended form symbols)
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
if (tab->symbols[i]->formScopeEnded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tab->symbols[i]->nameHash == h && tab->symbols[i]->scope == scope && namesEqual(tab->symbols[i]->name, name)) {
|
||||
return NULL; // duplicate
|
||||
}
|
||||
}
|
||||
|
||||
BasSymbolT *sym = (BasSymbolT *)calloc(1, sizeof(BasSymbolT));
|
||||
|
||||
if (!sym) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1);
|
||||
sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0';
|
||||
sym->nameHash = h;
|
||||
sym->kind = kind;
|
||||
sym->scope = scope;
|
||||
sym->dataType = dataType;
|
||||
sym->isDefined = true;
|
||||
|
||||
// Record owning form for both SCOPE_FORM vars AND SUBs/FUNCTIONs
|
||||
// declared inside BEGINFORM...ENDFORM. SUBs stay at SCOPE_GLOBAL
|
||||
// (callable from anywhere) but carry the owning form so the VM can
|
||||
// bind form-scope vars correctly when the SUB is dispatched as an
|
||||
// event handler for a different form's control.
|
||||
if (tab->inFormScope && tab->formScopeName[0] &&
|
||||
(scope == SCOPE_FORM || kind == SYM_SUB || kind == SYM_FUNCTION)) {
|
||||
strncpy(sym->formName, tab->formScopeName, BAS_MAX_SYMBOL_NAME - 1);
|
||||
sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
|
||||
}
|
||||
|
||||
arrput(tab->symbols, sym);
|
||||
tab->count = (int32_t)arrlen(tab->symbols);
|
||||
return sym;
|
||||
}
|
||||
|
||||
|
||||
int32_t basSymTabAllocSlot(BasSymTabT *tab) {
|
||||
if (tab->inLocalScope) {
|
||||
return tab->nextLocalIdx++;
|
||||
}
|
||||
|
||||
if (tab->inFormScope) {
|
||||
return tab->nextFormVarIdx++;
|
||||
}
|
||||
|
||||
return tab->nextGlobalIdx++;
|
||||
}
|
||||
|
||||
|
||||
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) {
|
||||
tab->inFormScope = true;
|
||||
strncpy(tab->formScopeName, formName, BAS_MAX_SYMBOL_NAME - 1);
|
||||
tab->formScopeName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
|
||||
tab->nextFormVarIdx = 0;
|
||||
tab->formScopeSymStart = tab->count;
|
||||
}
|
||||
|
||||
|
||||
void basSymTabEnterLocal(BasSymTabT *tab) {
|
||||
tab->inLocalScope = true;
|
||||
tab->nextLocalIdx = 0;
|
||||
}
|
||||
|
||||
|
||||
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
|
||||
uint32_t h = nameHashCI(name);
|
||||
|
||||
// Search local scope first
|
||||
if (tab->inLocalScope) {
|
||||
for (int32_t i = tab->count - 1; i >= 0; i--) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
if (s->nameHash == h && s->scope == SCOPE_LOCAL && namesEqual(s->name, name)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search form scope and global scope
|
||||
for (int32_t i = tab->count - 1; i >= 0; i--) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
if (s->formScopeEnded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s->nameHash == h && (s->scope == SCOPE_FORM || s->scope == SCOPE_GLOBAL) &&
|
||||
namesEqual(s->name, name)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) {
|
||||
uint32_t h = nameHashCI(name);
|
||||
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
if (s->formScopeEnded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s->nameHash == h && s->scope == SCOPE_GLOBAL && namesEqual(s->name, name)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
void basSymTabInit(BasSymTabT *tab) {
|
||||
memset(tab, 0, sizeof(*tab));
|
||||
}
|
||||
|
||||
|
||||
int32_t basSymTabLeaveFormScope(BasSymTabT *tab) {
|
||||
int32_t varCount = tab->nextFormVarIdx;
|
||||
|
||||
// Mark all form-scope symbols added since BEGINFORM as ended
|
||||
for (int32_t i = tab->formScopeSymStart; i < tab->count; i++) {
|
||||
if (tab->symbols[i]->scope == SCOPE_FORM) {
|
||||
tab->symbols[i]->formScopeEnded = true;
|
||||
}
|
||||
}
|
||||
|
||||
tab->inFormScope = false;
|
||||
tab->formScopeName[0] = '\0';
|
||||
tab->nextFormVarIdx = 0;
|
||||
tab->formScopeSymStart = 0;
|
||||
|
||||
return varCount;
|
||||
}
|
||||
|
||||
|
||||
void basSymTabLeaveLocal(BasSymTabT *tab) {
|
||||
// Remove all local symbols, freeing their dynamic arrays and the
|
||||
// symbol struct itself.
|
||||
int32_t newCount = 0;
|
||||
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
if (tab->symbols[i]->scope == SCOPE_LOCAL) {
|
||||
arrfree(tab->symbols[i]->patchAddrs);
|
||||
arrfree(tab->symbols[i]->fields);
|
||||
free(tab->symbols[i]);
|
||||
} else {
|
||||
if (i != newCount) {
|
||||
tab->symbols[newCount] = tab->symbols[i];
|
||||
}
|
||||
|
||||
newCount++;
|
||||
}
|
||||
}
|
||||
|
||||
arrsetlen(tab->symbols, newCount);
|
||||
tab->count = newCount;
|
||||
tab->inLocalScope = false;
|
||||
tab->nextLocalIdx = 0;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
|
||||
// Caller computes hash of search-name once; each entry stores its own
|
||||
// precomputed hash, so per-entry comparison is a fast uint32 compare
|
||||
// that nearly always rejects non-matches without calling namesEqual.
|
||||
// ============================================================
|
||||
static uint32_t nameHashCI(const char *name) {
|
||||
uint32_t h = 0x811C9DC5u;
|
||||
|
||||
while (*name) {
|
||||
char c = *name;
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c -= 32;
|
||||
}
|
||||
h ^= (uint32_t)(uint8_t)c;
|
||||
h *= 0x01000193u;
|
||||
name++;
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Case-insensitive name comparison
|
||||
// ============================================================
|
||||
static bool namesEqual(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
char ca = *a >= 'a' && *a <= 'z' ? *a - 32 : *a;
|
||||
char cb = *b >= 'a' && *b <= 'z' ? *b - 32 : *b;
|
||||
|
||||
if (ca != cb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
|
||||
return *a == *b;
|
||||
}
|
||||
166
src/apps/kpunch/dvxbasic/compiler/symtab.h
Normal file
166
src/apps/kpunch/dvxbasic/compiler/symtab.h
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// symtab.h -- DVX BASIC symbol table
|
||||
//
|
||||
// Tracks variables, constants, subroutines, functions, and labels
|
||||
// during compilation. Supports nested scopes (global + one local
|
||||
// scope per SUB/FUNCTION).
|
||||
//
|
||||
// Embeddable: no DVX dependencies, pure C.
|
||||
|
||||
#ifndef DVXBASIC_SYMTAB_H
|
||||
#define DVXBASIC_SYMTAB_H
|
||||
|
||||
#include "../compiler/opcodes.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ============================================================
|
||||
// Symbol kinds
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
SYM_VARIABLE,
|
||||
SYM_CONST,
|
||||
SYM_SUB,
|
||||
SYM_FUNCTION,
|
||||
SYM_LABEL,
|
||||
SYM_TYPE_DEF // user-defined TYPE
|
||||
} BasSymKindE;
|
||||
|
||||
// BasScopeE moved to opcodes.h (shared with runtime).
|
||||
|
||||
// ============================================================
|
||||
// Symbol entry
|
||||
// ============================================================
|
||||
|
||||
#define BAS_MAX_SYMBOL_NAME 64
|
||||
#define BAS_MAX_PARAMS 16
|
||||
|
||||
// UDT field definition
|
||||
typedef struct {
|
||||
char name[BAS_MAX_SYMBOL_NAME];
|
||||
uint8_t dataType; // BAS_TYPE_*
|
||||
int32_t udtTypeId; // if dataType == BAS_TYPE_UDT, index of the TYPE_DEF symbol
|
||||
} BasFieldDefT;
|
||||
|
||||
typedef struct {
|
||||
char name[BAS_MAX_SYMBOL_NAME];
|
||||
uint32_t nameHash; // FNV-1a over uppercase(name); fast-reject during lookup
|
||||
BasSymKindE kind;
|
||||
BasScopeE scope;
|
||||
uint8_t dataType; // BAS_TYPE_* for variables/functions
|
||||
int32_t index; // slot index (local or global)
|
||||
int32_t codeAddr; // PC address for SUB/FUNCTION/LABEL
|
||||
int32_t localCount; // number of local variables (for SUB/FUNCTION, set on leave)
|
||||
bool isDefined; // false = forward-declared
|
||||
bool isArray;
|
||||
bool isShared;
|
||||
bool isExtern; // true = external library function (DECLARE LIBRARY)
|
||||
bool formScopeEnded; // true = form scope ended, invisible to lookups
|
||||
char formName[BAS_MAX_SYMBOL_NAME]; // form name for SCOPE_FORM vars
|
||||
int32_t udtTypeId; // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol
|
||||
int32_t fixedLen; // for STRING * n: fixed length (0 = variable-length)
|
||||
uint16_t externLibIdx; // constant pool index for library name (if isExtern)
|
||||
uint16_t externFuncIdx; // constant pool index for function name (if isExtern)
|
||||
|
||||
// For SUB/FUNCTION: parameter info
|
||||
int32_t paramCount;
|
||||
int32_t requiredParams; // count of non-optional params
|
||||
uint8_t paramTypes[BAS_MAX_PARAMS];
|
||||
bool paramByVal[BAS_MAX_PARAMS];
|
||||
bool paramOptional[BAS_MAX_PARAMS]; // true = OPTIONAL parameter
|
||||
|
||||
// Forward-reference backpatch list (code addresses to patch when defined)
|
||||
int32_t *patchAddrs; // stb_ds dynamic array
|
||||
int32_t patchCount;
|
||||
|
||||
// For CONST: the constant value
|
||||
union {
|
||||
int32_t constInt;
|
||||
double constDbl;
|
||||
};
|
||||
char constStr[256];
|
||||
|
||||
// For TYPE_DEF: field definitions
|
||||
BasFieldDefT *fields; // stb_ds dynamic array
|
||||
int32_t fieldCount;
|
||||
} BasSymbolT;
|
||||
|
||||
// ============================================================
|
||||
// Symbol table
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
// Array of POINTERS to heap-allocated symbols (stb_ds array of pointers).
|
||||
// Pointers-of-pointers rather than array-of-structs because callers
|
||||
// routinely hold a BasSymbolT * across parsing operations that may
|
||||
// trigger basSymTabAdd (which grows this array). An array of structs
|
||||
// would be reallocated on growth, invalidating any held pointer.
|
||||
// Indirection via a stable heap pointer per symbol avoids that.
|
||||
BasSymbolT **symbols;
|
||||
int32_t count;
|
||||
int32_t nextGlobalIdx; // next global variable slot
|
||||
int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION)
|
||||
bool inLocalScope; // true when inside SUB/FUNCTION
|
||||
bool inFormScope; // true inside BEGINFORM...ENDFORM
|
||||
char formScopeName[BAS_MAX_SYMBOL_NAME]; // current form name
|
||||
int32_t nextFormVarIdx; // next form-level variable slot
|
||||
int32_t formScopeSymStart; // symbol count at BEGINFORM (for marking ended)
|
||||
} BasSymTabT;
|
||||
|
||||
// ============================================================
|
||||
// API
|
||||
// ============================================================
|
||||
|
||||
void basSymTabInit(BasSymTabT *tab);
|
||||
|
||||
// Add a symbol. Returns the symbol pointer, or NULL if the table is full
|
||||
// or the name already exists in the current scope.
|
||||
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
||||
|
||||
// Look up a symbol by name. Searches local scope first, then global.
|
||||
// Case-insensitive.
|
||||
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
|
||||
|
||||
// Look up a symbol in the global scope only.
|
||||
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
|
||||
|
||||
// Enter local scope (called at SUB/FUNCTION start).
|
||||
void basSymTabEnterLocal(BasSymTabT *tab);
|
||||
|
||||
// Leave local scope (called at END SUB/FUNCTION). Removes local symbols.
|
||||
void basSymTabLeaveLocal(BasSymTabT *tab);
|
||||
|
||||
// Allocate the next variable slot (global, local, or form depending on scope).
|
||||
int32_t basSymTabAllocSlot(BasSymTabT *tab);
|
||||
|
||||
// Enter form scope (called at BEGINFORM). Form-level DIMs create SCOPE_FORM variables.
|
||||
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
|
||||
|
||||
// Leave form scope (called at ENDFORM). Marks form-scope symbols as ended.
|
||||
// Returns the number of form variables allocated.
|
||||
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
|
||||
|
||||
#endif // DVXBASIC_SYMTAB_H
|
||||
420
src/apps/kpunch/dvxbasic/ctrlover.dhs
Normal file
420
src/apps/kpunch/dvxbasic/ctrlover.dhs
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
.topic ctrl.common.props
|
||||
.title Common Properties, Events, and Methods
|
||||
.toc 0 Common Properties, Events, and Methods
|
||||
.default
|
||||
.index Common Properties
|
||||
.index Common Events
|
||||
.index Common Methods
|
||||
.index Properties
|
||||
.index Events
|
||||
.index Methods
|
||||
|
||||
.h1 Common Properties, Events, and Methods
|
||||
|
||||
Every control in DVX BASIC inherits a set of common properties, events, and methods. These are handled by the form runtime before dispatching to widget-specific interface descriptors.
|
||||
|
||||
.h2 Common Properties
|
||||
|
||||
.table
|
||||
Property Type R/W Description
|
||||
---------- ------- --- -------------------------------------------
|
||||
Name String R The control's name (e.g. "Command1"). Read-only at runtime.
|
||||
Left Integer R/W X position in pixels relative to the parent container.
|
||||
Top Integer R/W Y position in pixels relative to the parent container.
|
||||
Width Integer R/W Current width in pixels. Setting this changes the minimum width constraint.
|
||||
Height Integer R/W Current height in pixels. Setting this changes the minimum height constraint.
|
||||
MinWidth Integer R/W Minimum width for layout. Alias for Width in the setter.
|
||||
MinHeight Integer R/W Minimum height for layout. Alias for Height in the setter.
|
||||
MaxWidth Integer R/W Maximum width cap (0 = no limit, stretch to fill).
|
||||
MaxHeight Integer R/W Maximum height cap (0 = no limit, stretch to fill).
|
||||
Weight Integer R/W Layout weight. 0 = fixed size, >0 = share extra space proportionally.
|
||||
Visible Boolean R/W Whether the control is visible.
|
||||
Enabled Boolean R/W Whether the control accepts user input.
|
||||
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
|
||||
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
|
||||
.endtable
|
||||
|
||||
.h2 Common Events
|
||||
|
||||
These events are wired on every control loaded from a .frm file. Controls created dynamically at runtime via code only receive Click, DblClick, Change, GotFocus, and LostFocus; the keyboard, mouse, and scroll events below require the control to be defined in the .frm file.
|
||||
|
||||
.table
|
||||
Event Parameters Description
|
||||
--------- ------------------------------------------- -------------------------------------------
|
||||
Click (none) Fires when the control is clicked.
|
||||
DblClick (none) Fires when the control is double-clicked.
|
||||
Change (none) Fires when the control's value or text changes.
|
||||
GotFocus (none) Fires when the control receives keyboard focus.
|
||||
LostFocus (none) Fires when the control loses keyboard focus.
|
||||
KeyPress KeyAscii As Integer Fires when a printable key is pressed. KeyAscii is the ASCII code.
|
||||
KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down. KeyCode is the scan code; Shift indicates modifier keys.
|
||||
KeyUp KeyCode As Integer, Shift As Integer Fires when a key is released.
|
||||
MouseDown Button As Integer, X As Integer, Y As Integer Fires when a mouse button is pressed over the control.
|
||||
MouseUp Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control.
|
||||
MouseMove Button As Integer, X As Integer, Y As Integer Fires when the mouse moves over the control.
|
||||
Scroll Delta As Integer Fires when the control is scrolled (mouse wheel or scrollbar).
|
||||
.endtable
|
||||
|
||||
.h2 Common Methods
|
||||
|
||||
.table
|
||||
Method Parameters Description
|
||||
-------- ---------- -------------------------------------------
|
||||
SetFocus (none) Gives keyboard focus to this control.
|
||||
Refresh (none) Forces the control to repaint.
|
||||
.endtable
|
||||
|
||||
.link ctrl.form Form
|
||||
.link ctrl.databinding Data Binding
|
||||
.link ctrl.frm FRM File Format
|
||||
|
||||
|
||||
.topic ctrl.databinding
|
||||
.title Data Binding
|
||||
.toc 1 Data Binding
|
||||
.index Data Binding
|
||||
.index DataSource
|
||||
.index DataField
|
||||
.index Master-Detail
|
||||
|
||||
.h1 Data Binding
|
||||
|
||||
DVX BASIC provides VB3-style data binding through three properties that can be set on most controls:
|
||||
|
||||
.table
|
||||
Property Set On Description
|
||||
---------- ----------- -------------------------------------------
|
||||
DataSource Any control Name of the Data control to bind to (e.g. "Data1").
|
||||
DataField Any control Column name from the Data control's recordset to display.
|
||||
.endtable
|
||||
|
||||
.h2 How It Works
|
||||
|
||||
.list
|
||||
.item Place a Data control on the form and set its DatabaseName and RecordSource properties.
|
||||
.item Place one or more display/edit controls (TextBox, Label, etc.) and set their DataSource to the Data control's name and DataField to a column name.
|
||||
.item When the form loads, the Data control auto-refreshes: it opens the database, runs the query, and navigates to the first record.
|
||||
.item Bound controls are updated automatically each time the Data control repositions (the Reposition event fires, and the runtime pushes the current record's field values into all bound controls).
|
||||
.item When a bound control loses focus (LostFocus), its current text is written back to the Data control's record cache, and Update is called automatically to persist changes.
|
||||
.endlist
|
||||
|
||||
.h2 Master-Detail Binding
|
||||
|
||||
For hierarchical data (e.g. orders and order items), use two Data controls:
|
||||
|
||||
.list
|
||||
.item A master Data control bound to the parent table.
|
||||
.item A detail Data control with its MasterSource set to the master's name, MasterField set to the key column in the master, and DetailField set to the foreign key column in the detail table.
|
||||
.endlist
|
||||
|
||||
When the master record changes, the detail Data control automatically re-queries using the master's current value for filtering. All controls bound to the detail are refreshed.
|
||||
|
||||
.h2 DBGrid Binding
|
||||
|
||||
Set the DBGrid's DataSource to a Data control name. The grid auto-populates columns from the query results and refreshes whenever the Data control refreshes.
|
||||
|
||||
.h2 Example
|
||||
|
||||
.code
|
||||
VERSION DVX 1.00
|
||||
Begin Form frmData
|
||||
Caption = "Data Binding Example"
|
||||
AutoSize = False
|
||||
Width = 400
|
||||
Height = 280
|
||||
Begin Data Data1
|
||||
DatabaseName = "myapp.db"
|
||||
RecordSource = "customers"
|
||||
End
|
||||
Begin Label lblName
|
||||
Caption = "Name:"
|
||||
End
|
||||
Begin TextBox txtName
|
||||
DataSource = "Data1"
|
||||
DataField = "name"
|
||||
End
|
||||
Begin Label lblEmail
|
||||
Caption = "Email:"
|
||||
End
|
||||
Begin TextBox txtEmail
|
||||
DataSource = "Data1"
|
||||
DataField = "email"
|
||||
End
|
||||
End
|
||||
|
||||
Sub Data1_Reposition ()
|
||||
Print "Current record changed"
|
||||
End Sub
|
||||
|
||||
Sub Data1_Validate (Cancel As Integer)
|
||||
If txtName.Text = "" Then
|
||||
MsgBox "Name cannot be empty!"
|
||||
Cancel = 1
|
||||
End If
|
||||
End Sub
|
||||
.endcode
|
||||
|
||||
.link ctrl.data Data
|
||||
.link ctrl.dbgrid DBGrid
|
||||
|
||||
|
||||
.topic ctrl.menus
|
||||
.title Menu System
|
||||
.toc 1 Menu System
|
||||
.index Menu
|
||||
.index Menu Bar
|
||||
.index Submenu
|
||||
.index Separator
|
||||
|
||||
.h1 Menu System
|
||||
|
||||
Menus are defined in the .frm file using Begin Menu blocks. Each menu item has a name, caption, and nesting level. Menu items fire Click events dispatched as MenuName_Click.
|
||||
|
||||
.h2 FRM Syntax
|
||||
|
||||
.code
|
||||
Begin Form Form1
|
||||
Caption = "Menu Demo"
|
||||
|
||||
Begin Menu mnuFile
|
||||
Caption = "&File"
|
||||
Begin Menu mnuOpen
|
||||
Caption = "&Open"
|
||||
End
|
||||
Begin Menu mnuSave
|
||||
Caption = "&Save"
|
||||
End
|
||||
Begin Menu mnuSep1
|
||||
Caption = "-"
|
||||
End
|
||||
Begin Menu mnuExit
|
||||
Caption = "E&xit"
|
||||
End
|
||||
End
|
||||
|
||||
Begin Menu mnuEdit
|
||||
Caption = "&Edit"
|
||||
Begin Menu mnuCopy
|
||||
Caption = "&Copy"
|
||||
End
|
||||
Begin Menu mnuPaste
|
||||
Caption = "&Paste"
|
||||
End
|
||||
End
|
||||
End
|
||||
.endcode
|
||||
|
||||
.h2 Menu Item Properties
|
||||
|
||||
.table
|
||||
Property Type Description
|
||||
-------- ------- -------------------------------------------
|
||||
Caption String The text displayed. Use & for accelerator key. Set to "-" for a separator.
|
||||
Checked Boolean Whether the menu item shows a checkmark.
|
||||
Enabled Boolean Whether the menu item is enabled (default True).
|
||||
.endtable
|
||||
|
||||
.h2 Nesting
|
||||
|
||||
Menu items are nested by placing Begin Menu blocks inside other Begin Menu blocks:
|
||||
|
||||
.list
|
||||
.item Level 0: top-level menu bar headers (e.g. "File", "Edit").
|
||||
.item Level 1: items within a top-level menu.
|
||||
.item Level 2+: submenu items.
|
||||
.endlist
|
||||
|
||||
A level-0 menu that contains children becomes a top-level menu header. A non-level-0 menu that contains children becomes a submenu.
|
||||
|
||||
.h2 Event Dispatch
|
||||
|
||||
Each clickable menu item (not headers, not separators) receives a unique numeric ID at load time. When clicked, the form's onMenu handler maps the ID to the menu item's name and fires MenuName_Click.
|
||||
|
||||
.code
|
||||
Sub mnuOpen_Click ()
|
||||
MsgBox "Open was clicked"
|
||||
End Sub
|
||||
|
||||
Sub mnuExit_Click ()
|
||||
Unload Form1
|
||||
End Sub
|
||||
.endcode
|
||||
|
||||
.link ctrl.form Form
|
||||
.link ctrl.frm FRM File Format
|
||||
|
||||
|
||||
.topic ctrl.arrays
|
||||
.title Control Arrays
|
||||
.toc 1 Control Arrays
|
||||
.index Control Arrays
|
||||
.index Index Property
|
||||
|
||||
.h1 Control Arrays
|
||||
|
||||
DVX BASIC supports VB-style control arrays. Multiple controls can share the same name, differentiated by an Index property. When an event fires on a control array element, the element's index is passed as the first parameter.
|
||||
|
||||
.h2 Defining Control Arrays in FRM
|
||||
|
||||
.code
|
||||
Begin CommandButton Command1
|
||||
Caption = "Button A"
|
||||
Index = 0
|
||||
End
|
||||
Begin CommandButton Command1
|
||||
Caption = "Button B"
|
||||
Index = 1
|
||||
End
|
||||
Begin CommandButton Command1
|
||||
Caption = "Button C"
|
||||
Index = 2
|
||||
End
|
||||
.endcode
|
||||
|
||||
.h2 Event Handler Convention
|
||||
|
||||
When a control has an Index property (>= 0), the event handler receives Index As Integer as the first parameter, before any event-specific parameters.
|
||||
|
||||
.code
|
||||
Sub Command1_Click (Index As Integer)
|
||||
Select Case Index
|
||||
Case 0
|
||||
MsgBox "Button A clicked"
|
||||
Case 1
|
||||
MsgBox "Button B clicked"
|
||||
Case 2
|
||||
MsgBox "Button C clicked"
|
||||
End Select
|
||||
End Sub
|
||||
.endcode
|
||||
|
||||
.h2 Accessing Array Elements in Code
|
||||
|
||||
Use the indexed form ControlName(Index) to access a specific element:
|
||||
|
||||
.code
|
||||
Command1(0).Caption = "New Text"
|
||||
Command1(1).Enabled = False
|
||||
.endcode
|
||||
|
||||
.note info
|
||||
Control array elements share the same event handler Sub. The runtime prepends the Index argument automatically. If you define parameters on the Sub, Index comes first, followed by the event's own parameters (e.g. KeyPress would be Sub Ctrl1_KeyPress (Index As Integer, KeyAscii As Integer)).
|
||||
.endnote
|
||||
|
||||
.link ctrl.common.props Common Properties, Events, and Methods
|
||||
.link ctrl.frm FRM File Format
|
||||
|
||||
|
||||
.topic ctrl.frm
|
||||
.title FRM File Format
|
||||
.toc 1 FRM File Format
|
||||
.index FRM
|
||||
.index .frm
|
||||
.index Form File
|
||||
|
||||
.h1 FRM File Format
|
||||
|
||||
The .frm file is a text file that describes a form's layout, controls, menus, and code. It follows a format compatible with VB3 .frm files, with DVX-specific extensions.
|
||||
|
||||
.h2 Structure
|
||||
|
||||
.code
|
||||
VERSION DVX 1.00
|
||||
Begin Form FormName
|
||||
form-level properties...
|
||||
|
||||
Begin Menu mnuFile
|
||||
Caption = "&File"
|
||||
Begin Menu mnuOpen
|
||||
Caption = "&Open"
|
||||
End
|
||||
End
|
||||
|
||||
Begin TypeName ControlName
|
||||
property = value
|
||||
...
|
||||
End
|
||||
|
||||
Begin Frame Frame1
|
||||
Caption = "Group"
|
||||
Begin TypeName ChildName
|
||||
...
|
||||
End
|
||||
End
|
||||
End
|
||||
|
||||
BASIC code follows...
|
||||
|
||||
Sub FormName_Load ()
|
||||
...
|
||||
End Sub
|
||||
.endcode
|
||||
|
||||
.h2 Rules
|
||||
|
||||
.list
|
||||
.item The VERSION line is optional. VERSION DVX 1.00 marks a native DVX form. VB forms with version <= 2.0 are accepted for import.
|
||||
.item The form block begins with Begin Form Name and ends with End.
|
||||
.item Controls are nested with Begin TypeName Name / End.
|
||||
.item Container controls (Frame, VBox, HBox, Toolbar, TabStrip, ScrollPane, Splitter, WrapBox) can have child controls nested inside them.
|
||||
.item Properties are assigned as Key = Value. String values are optionally quoted.
|
||||
.item Everything after the form's closing End is BASIC source code.
|
||||
.item Comments in the form section use ' (single quote).
|
||||
.item Blank lines are ignored in the form section.
|
||||
.endlist
|
||||
|
||||
.h2 Common FRM Properties
|
||||
|
||||
.table
|
||||
Property Applies To Description
|
||||
----------------------- --------------- -------------------------------------------
|
||||
Caption Form, controls Display text or window title.
|
||||
Text TextBox, ComboBox Initial text content.
|
||||
MinWidth / Width Controls Minimum width. Both names are accepted.
|
||||
MinHeight / Height Controls Minimum height. Both names are accepted.
|
||||
MaxWidth Controls Maximum width (0 = no cap).
|
||||
MaxHeight Controls Maximum height (0 = no cap).
|
||||
Weight Controls Layout weight for flexible sizing.
|
||||
Left Form, controls X position (used by Form when Centered=False; informational for controls).
|
||||
Top Form, controls Y position.
|
||||
Index Controls Control array index (-1 or absent = not in array).
|
||||
Visible Controls Initial visibility.
|
||||
Enabled Controls Initial enabled state.
|
||||
Layout Form "VBox" or "HBox".
|
||||
AutoSize Form Auto-fit window to content.
|
||||
Resizable Form Allow runtime resizing.
|
||||
Centered Form Center window on screen.
|
||||
DatabaseName Data SQLite database file path.
|
||||
RecordSource Data Table name or SQL query.
|
||||
DataSource Bound controls Name of the Data control.
|
||||
DataField Bound controls Column name in the recordset.
|
||||
.endtable
|
||||
|
||||
.link ctrl.form Form
|
||||
.link ctrl.common.props Common Properties, Events, and Methods
|
||||
.link ctrl.menus Menu System
|
||||
.link ctrl.arrays Control Arrays
|
||||
27
src/apps/kpunch/dvxbasic/dvxbasic.hcf
Normal file
27
src/apps/kpunch/dvxbasic/dvxbasic.hcf
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
# DVX BASIC Help configuration
|
||||
# Compiled into DVXBASIC.HLP in this directory
|
||||
output = DVXBASIC.HLP
|
||||
source = APPS/KPUNCH/DVXBASIC/*.DHS
|
||||
source = WIDGETS/*.BHS
|
||||
21
src/apps/kpunch/dvxbasic/dvxbasic.res
Normal file
21
src/apps/kpunch/dvxbasic/dvxbasic.res
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# dvxbasic.res -- Resource manifest for DVX BASIC
|
||||
icon32 icon icon32.bmp
|
||||
name text "DVX BASIC"
|
||||
author text "Scott Duensing"
|
||||
copyright text "Copyright 2026 Scott Duensing"
|
||||
publisher text "Kangaroo Punch Studios"
|
||||
description text "BASIC language IDE and runtime"
|
||||
# Toolbar icons (16x16)
|
||||
tb_open icon tb_open.bmp
|
||||
tb_save icon tb_save.bmp
|
||||
tb_run icon tb_run.bmp
|
||||
tb_stop icon tb_stop.bmp
|
||||
tb_code icon tb_code.bmp
|
||||
tb_design icon tb_design.bmp
|
||||
tb_debug icon tb_debug.bmp
|
||||
tb_stepin icon tb_stepinto.bmp
|
||||
tb_stepov icon tb_stepover.bmp
|
||||
tb_stepou icon tb_stepout.bmp
|
||||
tb_runtoc icon tb_runtocur.bmp
|
||||
# Placeholder icon (32x32)
|
||||
noicon icon noicon.bmp
|
||||
104
src/apps/kpunch/dvxbasic/form.dhs
Normal file
104
src/apps/kpunch/dvxbasic/form.dhs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
.topic ctrl.form
|
||||
.title Form
|
||||
.toc 1 Form
|
||||
.index Form
|
||||
.index Window
|
||||
.index Caption
|
||||
.index AutoSize
|
||||
.index Resizable
|
||||
.index Load
|
||||
.index Unload
|
||||
.index Show
|
||||
.index Hide
|
||||
|
||||
.h1 Form
|
||||
|
||||
VB Equivalent: Form -- DVX Widget: Window + VBox/HBox root
|
||||
|
||||
The Form is the top-level container representing a DVX window. It is declared in the .frm file with Begin Form FormName. All controls are children of the form's content box, which uses either VBox (default) or HBox layout.
|
||||
|
||||
.h2 Form Properties
|
||||
|
||||
.table
|
||||
Property Type Default Description
|
||||
---------- ------- -------------- -------------------------------------------
|
||||
Name String "Form1" The form's name, used for event dispatch and Load statement.
|
||||
Caption String (same as Name) Window title bar text.
|
||||
Width Integer 400 Window width in pixels. Setting this disables AutoSize.
|
||||
Height Integer 300 Window height in pixels. Setting this disables AutoSize.
|
||||
Left Integer 0 Initial X position. Used when Centered is False.
|
||||
Top Integer 0 Initial Y position. Used when Centered is False.
|
||||
Layout String "VBox" Content box layout: "VBox" (vertical) or "HBox" (horizontal).
|
||||
AutoSize Boolean False When True, the window shrink-wraps to fit its content.
|
||||
Resizable Boolean True Whether the user can resize the window at runtime.
|
||||
Centered Boolean True When True, the window is centered on screen. When False, Left/Top are used.
|
||||
.endtable
|
||||
|
||||
.h2 Form Events
|
||||
|
||||
.table
|
||||
Event Parameters Description
|
||||
----------- --------------------- -------------------------------------------
|
||||
Load (none) Fires after the form and all controls are created. This is the default event.
|
||||
Unload (none) Fires when the form is being closed or unloaded.
|
||||
QueryUnload Cancel As Integer Fires before Unload. Set Cancel = 1 to abort the close.
|
||||
Resize (none) Fires when the window is resized by the user.
|
||||
Activate (none) Fires when the window gains focus.
|
||||
Deactivate (none) Fires when the window loses focus.
|
||||
.endtable
|
||||
|
||||
.h2 Form Methods
|
||||
|
||||
.table
|
||||
Statement Description
|
||||
------------------ -------------------------------------------
|
||||
Load FormName Load the form (creates the window and controls, fires Load event).
|
||||
Unload FormName Unload the form (fires Unload, destroys window).
|
||||
FormName.Show Make the form visible.
|
||||
FormName.Show 1 Show as modal dialog (blocks until closed).
|
||||
FormName.Hide Hide the form without unloading it.
|
||||
.endtable
|
||||
|
||||
.h2 Example
|
||||
|
||||
.code
|
||||
Sub Form_Load ()
|
||||
Form1.Caption = "Hello World"
|
||||
Print "Form loaded!"
|
||||
End Sub
|
||||
|
||||
Sub Form_QueryUnload (Cancel As Integer)
|
||||
If MsgBox("Really quit?", 4) <> 6 Then
|
||||
Cancel = 1
|
||||
End If
|
||||
End Sub
|
||||
|
||||
Sub Form_Resize ()
|
||||
Print "Window resized"
|
||||
End Sub
|
||||
.endcode
|
||||
|
||||
.link ctrl.common.props Common Properties, Events, and Methods
|
||||
.link ctrl.frm FRM File Format
|
||||
5529
src/apps/kpunch/dvxbasic/formrt/formrt.c
Normal file
5529
src/apps/kpunch/dvxbasic/formrt/formrt.c
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue