A cross-platform VBScript engine extracted from Wine, packaged as a standalone C library.
sudo apt install bisoncmake -DCMAKE_BUILD_TYPE=Release -DPLATFORM=linux -DARCH=x64 -B build
cmake --build buildsudo apt install bisoncmake -DCMAKE_BUILD_TYPE=Release -DPLATFORM=linux -DARCH=aarch64 -B build
cmake --build buildbrew install bisoncmake -DCMAKE_BUILD_TYPE=Release -DPLATFORM=macos -DARCH=arm64 -B build
cmake --build buildbrew install bisoncmake -DCMAKE_BUILD_TYPE=Release -DPLATFORM=macos -DARCH=x64 -B build
cmake --build buildbrew install bisoncmake -DCMAKE_BUILD_TYPE=Release -DPLATFORM=ios -DARCH=arm64 -B build
cmake --build buildsudo apt install bisoncmake -DCMAKE_BUILD_TYPE=Release -DPLATFORM=android -DARCH=arm64-v8a -B build
cmake --build buildRequires MSYS2 with UCRT64 environment. Install dependencies:
pacman -S --noconfirm bison \
mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake \
mingw-w64-ucrt-x86_64-toolsBuild (entire build runs inside the MSYS2 UCRT64 shell):
MSYSTEM=UCRT64 /c/msys64/usr/bin/bash.exe -l -c "
cd \"$(pwd)\" &&
cmake -DCMAKE_BUILD_TYPE=Release -DPLATFORM=win-mingw -DARCH=x64 -B build &&
cmake --build build -- -j\$(nproc)
"cmake --install build --prefix /usr/localsrc/winevbs_test.c contains a complete working example. The key steps are:
#define COBJMACROS
#define INITGUID
#include "libwinevbs.h"
#include <oleauto.h>
DEFINE_GUID(CLSID_VBScript, 0xb54f3741, 0x5b07, 0x11cf,
0xa4, 0xb0, 0x0, 0xaa, 0x0, 0x4a, 0x55, 0xe8);
static void log_callback(libwinevbs_log_level_t level, const char* format, va_list args)
{
const char *prefix = level == LIBWINEVBS_LOG_ERROR ? "ERROR" :
level == LIBWINEVBS_LOG_WARN ? "WARN" :
level == LIBWINEVBS_LOG_DEBUG ? "DEBUG" : "INFO";
printf("[%s] ", prefix);
vprintf(format, args);
printf("\n");
}
libwinevbs_callbacks_t cb = { .log = log_callback };
libwinevbs_init(&cb);IActiveScriptParse *parser = NULL;
CoCreateInstance(&CLSID_VBScript, NULL, CLSCTX_INPROC_SERVER,
&IID_IActiveScriptParse, (void **)&parser);
IActiveScript *engine = NULL;
ASP_QueryInterface(parser, &IID_IActiveScript, (void **)&engine);The IActiveScriptParse COBJMACROS use the 64-bit names. Define aliases for readability:
#define ASP_QueryInterface IActiveScriptParse64_QueryInterface
#define ASP_Release IActiveScriptParse64_Release
#define ASP_InitNew IActiveScriptParse64_InitNew
#define ASP_ParseScriptText IActiveScriptParse64_ParseScriptTextThe engine requires a script site that implements IActiveScriptSite. This is a COM interface with a C vtable. At minimum you need:
- GetItemInfo — returns
IUnknown/ITypeInfofor named items you add to the engine - OnScriptError — receives compile and runtime errors
See src/winevbs_test.c for a complete minimal implementation.
IActiveScript_SetScriptSite(engine, (IActiveScriptSite *)&site);
ASP_InitNew(parser);
IActiveScript_SetScriptState(engine, SCRIPTSTATE_STARTED);To give VBScript access to your application, implement IDispatch and register as a named item:
IActiveScript_AddNamedItem(engine, L"Host",
SCRIPTITEM_ISVISIBLE | SCRIPTITEM_GLOBALMEMBERS);SCRIPTITEM_GLOBALMEMBERS makes the object's methods callable without a prefix (e.g., Print "hello" instead of Host.Print "hello").
Your IDispatch implementation maps method names to DISPIDs in GetIDsOfNames and handles calls in Invoke. See the HostObj_* functions in src/winevbs_test.c.
EXCEPINFO ei = {0};
// Evaluate an expression and get the result
VARIANT result;
VariantInit(&result);
ASP_ParseScriptText(parser, L"2 + 3 * 4",
NULL, NULL, NULL, 0, 0,
SCRIPTTEXT_ISEXPRESSION, &result, &ei);
// result.lVal == 14
// Execute statements
ASP_ParseScriptText(parser,
L"Dim i\n"
L"For i = 1 To 5\n"
L" Print CStr(i)\n"
L"Next\n",
NULL, NULL, NULL, 0, 0, 0, NULL, &ei);IActiveScript_Close(engine);
IActiveScript_Release(engine);
ASP_Release(parser);
libwinevbs_shutdown();The test program is built automatically with the library:
./build/winevbs_testExpected output:
--- Test 1: Expression ---
2 + 3 * 4 = 14
--- Test 2: Host Print ---
VBScript> Hello from VBScript!
--- Test 3: Loop + String ---
VBScript> Count: 1 squared = 1
VBScript> Count: 2 squared = 4
VBScript> Count: 3 squared = 9
VBScript> Count: 4 squared = 16
VBScript> Count: 5 squared = 25
VBScript> Upper: HELLO WORLD
Done.
- Wine VBScript engine — compiler, interpreter, lexer, and parser
- OLE Automation (oleaut32) —
VARIANT,SAFEARRAY, type coercion, andVarFormat - VBScript RegExp —
RegExp,Match, andMatchesobjects - Scripting Runtime (scrrun) —
Scripting.DictionaryandScripting.FileSystemObject - Kernelbase locale stubs — minimal locale support for string operations
Visual Pinball uses VBScript as its scripting engine for table logic. On Windows, this is handled natively, but cross-platform support (macOS, Linux, iOS, Android) required an alternative. The Wine project's VBScript engine provided a solid foundation, but needed significant bug fixes and missing feature implementations to handle the breadth of real-world VBScript used in pinball tables.
libwinevbs packages this work as a reusable C library with a clean callback-based API, decoupled from both Wine and Visual Pinball internals.
Wine source is available here. All local modifications are guarded with #ifdef __LIBWINEVBS__ / #ifndef __LIBWINEVBS__.
To compare against upstream, Wine must be built first since IDL compilation generates the header files:
# macOS
brew install llvm lld bison
git clone https://gitlab.winehq.org/wine/wine.git /path/to/wine
cd /path/to/wine
git checkout afba987dda1430dc0d9f36fc6f27cd3584bce79e
export PATH="/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/bison/bin:$PATH"
./configure --without-freetype --enable-win64
make -j$(nproc)Then diff against it:
cd /path/to/libwinevbs
./scripts/winediff.sh /path/to/wineThe sources under wine/ are a snapshot of a single upstream Wine commit. The
hash is recorded both in the deps: bump to wine X.Y commit message and in the
wine/wine-<hash>.patch filename. Every local change is guarded with
#ifdef __LIBWINEVBS__ / #ifndef __LIBWINEVBS__ so it can be carried across a
re-sync.
To move to a newer Wine release:
- Check out and build the target Wine commit (see Wine Source Base). The build is required because IDL compilation generates header files.
- Carry the libwinevbs delta onto the new sources.
./scripts/winediff.sh /path/to/winelists every local modification, plus# libwinevbs-only:for files that exist only here, so you can re-apply the#ifdef __LIBWINEVBS__blocks. - If any
.idlchanged, regenerate the IDispatch proxies with./scripts/genproxy.sh(see IDL Parser). - Regenerate the VBScript error-string table from the updated
wine/dlls/vbscript/vbscript.rcwith./scripts/genvbserrors.sh(see Error Strings). Ifwinediff.shreports changes tovbscript.rc, the table needs refreshing. - Rebuild and run the smoke test:
cmake --build build --target winevbs_test && ./build/winevbs_test. - Regenerate the reference patch and rename it to the new hash:
./scripts/winediff.sh /path/to/wine > wine/wine-<new-hash>.patch(delete the oldwine-<hash>.patch).
tools/idl-parser is a development-only Java tool that regenerates the *_proxy.c IDispatch dispatchers from the Wine .idl files. End users do not need it; it only runs when the Wine IDL definitions change and the proxy stubs need to be regenerated.
Requires Java 11+ and Gradle. To regenerate:
./scripts/genproxy.shOutput is written directly into wine/dlls/<module>/<module>_proxy.c. The list of IDLs and interfaces to process is hardcoded in tools/idl-parser/src/main/java/org/vpinball/IDLParserToC.java.
VBScript runtime and compile errors carry English descriptions (surfaced through
Err.Description and EXCEPINFO.bstrDescription). Wine normally loads these
from compiled string resources via LoadStringW, but libwinevbs does not link
the resource/locale stack, so get_vbscript_string() reads them from a static
table instead.
That table is generated from the vendored, English-only resource file
wine/dlls/vbscript/vbscript.rc (Wine keeps translations in separate po
files, so this .rc holds only English). To refresh it after bumping Wine:
./scripts/genvbserrors.shIt rewrites the region between the BEGIN/END GENERATED ERROR STRINGS markers in
wine/dlls/vbscript/vbscript_main.c. Requires python3.
Using the Wine VBScript engine source for Visual Pinball has surfaced a large number of defects. Many of these have since been fixed in upstream Wine as a direct result of this project.
- Bug 49908 — vbscript: ExecuteGlobal is not implemented 11.6
- Bug 53644 — vbscript can not compile classes with lists of private / public / dim declarations 10.1
- Bug 53670 — vbscript can not compile if expressions with reversed gte, lte, (=>, =<) 7.22
- Bug 53676 — vbscript can not exec_script - invalid number of arguments for Randomize 7.21
- Bug 53678 — vbscript can not compile CaseClausules that do not use a colon 8.0-rc1
- Bug 53766 — vbscript fails to handle SAFEARRAY assignment, access, UBounds, LBounds [Invalid]
- Bug 53767 — vbscript fails to handle ReDim when variable is not yet created 10.16
- Bug 53782 — vbscript can not compile ReDim with list of variables 7.22
- Bug 53783 — vbscript can not compile private const expressions 8.0-rc1
- Bug 53807 — vbscript fails to redim original array in function when passed byref 7.22
- Bug 53844 — invoke_vbdisp not handling let property correctly for VT_DISPATCH arguments 11.6
- Bug 53866 — vbscript fails to handle SAFEARRAY in for...each [Invalid]
- Bug 53867 — vbscript fails to retrieve property array by index 7.22
- Bug 53868 — vbscript fails to return TypeName for VT_DISPATCH 7.22
- Bug 53873 — vbscript fails to compile Else If when If is on same line 7.22
- Bug 53877 — compile_assignment assertion when assigning multidimensional array by indices 11.9
- Bug 53888 — vbscript does not allow Mid on non VT_BSTR 7.21
- Bug 53889 — does not support Get_Item call on IDispatch objects 11.5
- Bug 53962 — vbscript does not Eval implemented 11.6
- Bug 54177 — vbscript: Fix Sub first argument parentheses handling. 11.8
- Bug 54221 — vbscript: missing support for GetRef 11.6
- Bug 54234 — vbscript fails to compile when colon follows Else in If...Else 8.2
- Bug 54291 — stuck in endless for loop when UBound on Empty and On Error Resume Next 11.7
- Bug 54456 — vbscript memory leak in For Each with SafeArray as group 8.2
- Bug 54457 — vbscript memory leaks in interp_redim_preserve 8.2
- Bug 54458 — vbscript memory leaks in Global_Split 8.2
- Bug 54489 — vbscript Abs on BSTR returns invalid value 8.2
- Bug 54490 — vbscript fails to compile when statement follows ElseIf 8.2
- Bug 54493 — vbscript fails to compile concat when used without space and expression begins with H 8.2
- Bug 54731 — vbscript: stack_pop_bool doesn't support floats or ole color 9.0-rc1
- Bug 54978 — vbscript fails to compile Sub when End Sub on same line 8.12
- Bug 55037 — colon on new line after Then fails 11.5
- Bug 55042 — IDictionary::Add() fails to add entries with numerical keys that have the same hashes 8.11
- Bug 55052 — For loop where right bound is string coercion issue 9.0-rc1
- Bug 55006 — single line if else without else body fails compilation 11.5
- Bug 55093 — if boolean condition should work without braces 11.6
- Bug 55185 — vbscript round does not handle numdecimalplaces argument 9.0-rc1
- Bug 55931 — vbscript: empty MOD 100000 returns garbage instead of 0 9.0-rc1
- Bug 55969 — vbscript fails to return TypeName for Nothing 9.0-rc1
- Bug 56139 — scrrun: Dictionary does not allow storing at key Undefined 9.3
- Bug 56281 — string number converted to ascii value instead of parsed value 11.8 11.9
- Bug 56464 — vbscript: Join on array with "empty" items fails 10.2
- Bug 56480 — underscore line continue issues 11.5
- Bug 56781 — scrrun: Dictionary setting item to object fails 9.11
- Bug 56931 — Const used before declaration fails (explicit) 11.5
- Bug 57511 — For loop where loop var is not defined throws error without context 11.7
- Bug 57563 — vbscript: mid() throws when passed VT_EMPTY instead of returning empty string 10.1
- Bug 58248 — Me(Idx) fails to compile 11.5
- Bug 58026 — vbscript: Script running error when Dictionary contains array 11.6
- Bug 58051 — Dictionary direct Keys/Items access causes parse error 11.7
- Bug 58056 — directly indexing a Split returns Empty 11.6
- vbscript: Support element access on public array properties of class instances. 11.8
- vbscript: Convert VT_DISPATCH arguments to string in Eval/Execute/ExecuteGlobal. 11.8
- oleaut32: Fix Null handling in three-valued logical ops. 11.9
- vbscript: Coerce VT_EMPTY operands before Var* calls. 11.9
- vbscript: Match native cross-parse name redefinition rules. 11.9
- vbscript: Resolve UBound/LBound default member and match native errors 11.10
- vbscript: Mark class member fixed-size arrays as FADF_FIXEDSIZE 11.10
- vbscript: Parse double literals via ucrtbase _wcstod_l. 11.10
- vbscript: Allow Dim to shadow a global const from a previous compile unit. 11.11
- vbscript: Report specific compile errors for trailing tokens and missing identifiers. 11.11
- vbscript: vbscript: Handle class declaration scope in the parser. 11.11
- vbscript: Bind local variables and arguments at compile time. 11.11
- vbscript: Store global variables and functions in a single tagged tree. 11.12
- vbscript: Look up named items before builtin functions. 11.12
- vbscript: Cache host DISPIDs as entries in the unified global tree. 11.12
- vbscript: Parse a bare '&' followed by octal digits as an octal literal. 11.12
- vbscript: Raise type mismatch when comparing an array. 11.12
- scrrun/dictionary: Match native semantics for Empty and Boolean keys. 11.11
- vbscript: Use indexed lookup for global functions and variables. 11.7
Wine and the people who support the VBScript engine:
- Robert Wilhelm
- Nikolay Sivov
- Jacek Caban
Special thanks to @francisdb for providing a majority of the vpx-standalone-scripts patches and getting many of the Wine bugs fixed in upstream Wine.
This library contains code from the Wine project, licensed under the GNU Lesser General Public License v2.1 (LGPL-2.1), and the ReactOS project, licensed under the GNU General Public License v2 (GPL-2.0).