HaO 2020-04-23: This is the log of the creation of a project to embedd a program written in TCL in a DLL.
I have a program written in TCL (plus some C components). A customer wants to embedd the program in its own application and requires a DLL for a Microsoft compiler.
Additional requirements:
I am a medium level programmer and totally new to this subject.
I want to start by all the people helped within the journey:
KitDLL is binary distribution of a dll with an embedded tcl & bundles
I tried KitDLL binary from [L1 ]. When I download the binary distribution, copied the tclsh.exe beside the binary and started it, I got "<sys/stat.h> not compatible with VC". It is cross-compiled on Linux for Windows. I already tried MingW and those distributions and always ran into subtile bugs, like inexact calculations, wrong colors in Widgets etc. Thus, I decided to compile on my own and use a Microsoft compiler.
To build a static TCL lib, the makefile.vc is used. I decided to use the community edition of MS-VisualC2015. So, first choose in the start menu: "Visual Studio 2015 -> Windows Desktop Command -> VS2015 x86 Native Tools-Eingabeaufforderung". TCL8.6.10 source distribution is unzipped in: "C:\test\tcl8.6.10"
TCL is compiled with the following options:
======C > cd test\tcl8.6.10\win > nmake -f Makefile.vc release OPTS=static,staticpkg,msvcrt,nostubs,symbols > nmake -f Makefile.vc install OPTS=static,staticpkg,msvcrt,nostubs,symbols INSTALLDIR=c:\test\tcl8610
**C-VFS** C-VFS includes scripts in a C source and allows to access them on runtime. But KitDLL contains more options: * Starkits: read a file (the binary or dll) and locate a [metakit] file data base and mount it to the TCL interpreter * ZIPKit: same as Starkits, but using a zip archive instead the metakit * C-VFS: as described above All those options were added to the Starkit framework still containing many bug-fixes for TCL8.4/5 and support for weired platforms like Windows-CE. I am only interested in the C-VFS part and want to build on my own. The original build system downloads many packages, applies provided patches, compiles them and packs all scripts in the C-VFS and statically binds all binary extensions. This is far to complex for me. Download: I took the trunk from [https://kitcreator.rkeene.org/fossil/wiki?name=KitDLL] dated 2020-01-22. The folder "kitsh\buildsrc\kitsh-0.0" contains the kit programs. Change to this folder. First, the file tree with the scripts to include is set-up in the folder "C:\test\starpack.vfs". I added the tcl lib folder (excluding most encodings and time zones and tests) and the file "boot.tcl" from the upper "kitsh.0-0"-folder. A C-VFS file containing this file tree is created by (see aclocal.m4):
tclsh dir2c.tcl tcl c:/test/starpack.vfs --obsfucate > cvfs_data_tcl.c
I did not use "--obfuscate" at the beginning, which does not put the scripts in clear text into the DLL. To compile this file, I used a MSVC2015 dll project with the following additional defines: ======C HAVE_STRING_H;HAVE_STDLIB_H;CVFS_MAKE_LOADABLE,STATIC_BUILD;UNICODE;_UNICODE
First I tried using MS-VC6. The dir2c outputs C99 code, not C89. So this patch is applied: [L2 ]. When I included TCL itself, I ran into memory issues. So I changed to MS VC2015.
Then I ran into a compiler limit (C1091), that string constants may not exceed 65535 bytes. A solution is an unsigned char byte array, with the limit of size_t (0x7CFFFFFF).
This lead to the following patch of dir2c.tcl: [L3 ].
Then, the file "cvfs_data_tcl.c" is successfully compiled.
My DLL main program with the 3 exported functions is as follows:
======C #define STRICT #define DLL_BUILD #include <windows.h> #include <windowsx.h> #include <tchar.h> #include <string.h> #include <stdlib.h> #include <math.h>
#include <tcl.h> #include "tclkit.h"
#define RET_ERR_STRING -1 #define RET_WIN_ERROR -2 #define ERROR_MESSAGE_MAX 1024
// >>> local Prototypes __declspec(dllexport) void my_release(); static void TclGetError();
// The pointer to the tcl interpreter static Tcl_Interp *fg_interp=NULL;
static WCHAR fg_w_error_msgERROR_MESSAGE_MAX+1;
// >>>>> DllMain BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call) { case DLL_PROCESS_DETACH: // >>> DLL is unloaded my_release(); } return TRUE;
} __declspec(dllexport) WCHAR * my_init() {
// >> Release eventual present interpreter if (fg_interp != NULL) scanlink_release(); Tcl_FindExecutable(NULL); fg_interp = Tcl_CreateInterp(); // >> Init Tcl Kit Tclkit_Init(); // >> Source the init.tcl script if (Tcl_Init(fg_interp) != TCL_OK) { TclGetError(); return fg_w_error_msg; } return NULL;
} // >>>>> scanlink_release void __declspec(dllexport) scanlink_release() {
if ( fg_interp != NULL) Tcl_DeleteInterp(fg_interp); fg_interp = NULL;
} // >>>>> my_cmd __declspec(dllexport) WCHAR * my_cmd(WCHAR *pwCMD) {
WCHAR * pwError; Tcl_DString dCmd; int Res; if (fg_interp == NULL) { pwError = my_init(); if (pwError != NULL) return pwError; } if (Tcl_InterpActive(fg_interp)) { return 0; } // >> Call initialization routine Tcl_DStringInit(&dCmd); Tcl_WinTCharToUtf(pwCmd,-1,&dCmd); oArguments[1] = Tcl_NewStringObj(Tcl_DStringValue(&dProfileName), Tcl_DStringLength(&dProfileName)); Tcl_DStringFree(&dProfileName); // > Call directly without compilation Res = Tcl_Eval(fg_interp,dCmd); if (Res != TCL_OK) { TclGetError(); return fg_w_error_msg; } return NULL;
} // >>>>> TclGetError static void TclGetError() {
Tcl_DString dErrorMessage; Tcl_DString dErrorMessageWide; int ErrorLength; Tcl_DStringInit(&dErrorMessage); Tcl_DStringInit(&dErrorMessageWide); Tcl_DStringGetResult(fg_interp, &dErrorMessage); Tcl_WinUtfToTChar(Tcl_DStringValue(&dErrorMessage), Tcl_DStringLength(&dErrorMessage), &dErrorMessageWide); // Limit length to size of error buffer ErrorLength = min(Tcl_DStringLength(&dErrorMessageWide),ERROR_MESSAGE_MAX*2); memcpy(fg_w_error_msg,Tcl_DStringValue(&dErrorMessageWide),ErrorLength); // Be sure we have a wide end 0 somewhere. fg_w_error_msg[ERROR_MESSAGE_MAX] = 0; // >> If there is nothing in the interpreter, put an E in. if (fg_w_error_msg[0] == 0) { fg_w_error_msg[0] = 'E'; fg_w_error_msg[1] = 0; }
}
The interpreter is initialized, commands may be executed and the interpreter may be released. **c-vfs initialization** I have no idea how it works, but the C file tree is made available to the script. The Tclkit initialization is announced with the prototype file "tclkit.h":
void Tclkit_Init(void);
The initialization tcl file cvfs.tcl is prepared for embedding by:
tclsh stringify.tcl cvfs.tcl > cvfs.tcl.h
Now, the file kitint.c is ready for inclusion. I personally have removed from the file: * anything for metakit and zip file system * the bugfixes for "FindExecutable()". * anything for Tk So, the following defines are set: ======C _WIN32;KIT_STORAGE_CVFS;TCL_THREADS;TCLKIT_DLL
To compile, the internal C headers are required. So include:
By the build system,aclocal.m4 creates "kitInit-libs.h" with content for each contained binary package (excluding build, kitsh and common). In addition, an init function "_Tclkit_GenericLib_Init" is created.
We create this file manually with empty contents: ======C static void _Tclkit_GenericLib_Init(void) { }
**rechan** The rechan package is included. I don't know, if it is used by C-VFS. Just include the file "rechan.c". **vfs** The VFS package is included by default. I suppose, it is required for C-VFS. Get the root branch from: [https://core.tcl-lang.org/tclvfs/info/995426198338747b] And add file vfs.c in folder generic to the project. Fix bug [https://core.tcl-lang.org/tclvfs/tktview?name=c6829e8f45]. Required defines: HAVE_SYS_STAT_H **Linking** Now add the following libraries to the project: * c:\test\tcl8610\lib\tclstub86.lib * c:\test\tcl8610\lib\tcl86tsgx.lib * c:\test\tcl8610\lib\thread2.8.5\thread285tsgx.lib * Netapi32.lib **test** In a 2nd project targeting a console application, I have a test file like that: ======C int main(int argc, char* argv[]) { WCHAR *pwErrorMsg; pwErrorMsg = scanlink_init(); if (pwErrorMsg != NULL) { wprintf(L"%s",pwErrorMsg); return 1; } return 0; }