Richard Suchenwirth 2007-10-13 - In the Tcl chatroom, Gerald Lester asked for the functionality to wrap Tcl code into a C function that relieves the user of explicit Tcl_Eval. After all, it's just a matter of string manipulation, I thought: the call
cfunc tcl_mul {int a int b} int {expr {$a*$b}}}
should return a string which looks about like the following:
#ifndef HAVE_TCC_INTERP Tcl_Interp* _interp_ = NULL; static Tcl_Interp* tcc_Interp(void) { if(_interp_ == NULL) { Tcl_FindExecutable(NULL /*argv[0]*/); _interp_ = Tcl_CreateInterp(); } return _interp_; } #define HAVE_TCC_INTERP #endif DLL_EXPORT int tcl_mul(int a, int b) { Tcl_Interp* interp = tcc_Interp(); Tcl_Obj* aname = Tcl_NewStringObj("a",-1); Tcl_Obj* bname = Tcl_NewStringObj("b",-1); int result = 99999; Tcl_ObjSetVar2(interp,aname,NULL,Tcl_NewIntObj(a),0); Tcl_ObjSetVar2(interp,bname,NULL,Tcl_NewIntObj(b),0); if(Tcl_Eval(interp,"expr {$a*$b}") == TCL_OK) { Tcl_GetIntFromObj(interp,Tcl_GetObjResult(interp), &result); } return result; }
Here's what I coded:
proc cfunc {cname argl rtype tclbody} { set cbody { #ifndef HAVE_TCC_INTERP Tcl_Interp* global_interp = NULL; static Tcl_Interp* tcc_Interp(void) { if(global_interp == NULL) { Tcl_FindExecutable(NULL /*argv[0]*/); global_interp = Tcl_CreateInterp(); } return global_interp; } #define HAVE_TCC_INTERP #endif } set cargs "" foreach {type var} $argl {lappend cargs [list $type $var]} append cbody "DLL_EXPORT $rtype ${cname}([join $cargs ,]) \{" \n append cbody " Tcl_Interp* interp = tcc_Interp();" \n foreach {type var} $argl { append cbody " Tcl_Obj* ${var}name =\ Tcl_NewStringObj(\"$var\",-1);\n" } append cbody " $rtype result;" \n foreach {type var} $argl { switch -- $type { int {append cbody \ " Tcl_ObjSetVar2(interp,${var}name,NULL,\ Tcl_NewIntObj($var),0);\n" } char* {append cbody \ " Tcl_ObjSetVar2(interp,${var}name,NULL,\ Tcl_NewStringObj($var,-1),0);\n" } double {append cbody \ " Tcl_ObjSetVar2(interp,${var}name,NULL,\ Tcl_NewDoubleObj($var),0);\n" } default {error "type $type not yet supported"} } } append cbody " if(Tcl_Eval(interp,\"" append cbody [string map {\" \\\" \n \\n\\\n} $tclbody] append cbody "\") == TCL_OK) \{\n" switch -- $rtype { int {append cbody \ " Tcl_GetIntFromObj(interp,\ Tcl_GetObjResult(interp), &result);\n" } char* {append cbody \ " result = Tcl_GetString(\ Tcl_GetObjResult(interp));\n" } double {append cbody \ " Tcl_GetDoubleFromObj(interp,\ Tcl_GetObjResult(interp), &result);\n" } default {error "type $rtype not yet supported"} } append cbody " \}\n return result;\n\}\n" return $cbody }
Testing with tcltcc (but the generated code should also be usable in Critcl or Odyce, with maybe a few tweaks):
set d [tcc::dll] $d ccode [cfunc tcl_mul {int a int b} int {expr {$a*$b}}] $d ccode [cfunc tcl_up {char* str} char* {string toupper "$str"}] #-- for now, the callers sit in the same DLL: $d cproc try {int a int b} int {tcl_mul(a,b);} $d cproc up {char* s} char* {tcl_up(s);} if [catch {$d write -name ping}] { puts [set tcc::dll::${d}::code] puts \n$errorInfo exit } load ping.dll puts [try 7 6]/[up hello]
Of course, it took me a few iterations until this short but sweet result came on stdout:
42/HELLO
But of course, we really need that code in other libs can call our functions. This took me a while to get right, and is Windows-specific (write a .DEF file to describe the library):
set d [tcc::dll] $d ccode [cfunc tcl_mul {int a int b} int {expr {$a*$b}}] $d ccode [cfunc tcl_up {char* str} char* {string toupper "$str"}] $d write -name ping #-- semi-manually making a .def file... set f [open ping.def w] puts $f "LIBRARY ping.dll\n\nEXPORTS\ntcl_mul\ntcl_up" close $f load ping.dll set d [tcc::dll] $d cproc try {int a int b} int {tcl_mul(a,b);} $d cproc up {char* s} char* {tcl_up(s);} $d write -name pong -libs ping load pong.dll puts [try 7 6]/[up hello]
It also required that I modified the function tcc::to_dll in tcc.tcl to do
tcc_1 add_library_path .
which may not be the most general solution... but at least it's getting forward