1616class ProcessTCL
1717{
1818 private FFI $ ffi ;
19+ private bool $ isTcl9 ;
1920 private static ?ProcessTCL $ instance = null ;
2021 private array $ callbacks = []; // registered callbacks
2122
@@ -34,47 +35,74 @@ private function __construct()
3435 // find the bundled script libraries without needing system packages.
3536 $ this ->setupLibraryPaths ($ libDir );
3637
37- $ cdef = "
38+ $ this ->ffi = $ this ->loadTclLibrary ($ libDir );
39+ }
40+
41+ /**
42+ * Returns the FFI C declarations for Tcl 8.6 (Linux/Windows).
43+ */
44+ private function getCdefTcl8 (): string
45+ {
46+ return "
3847 void* Tcl_CreateInterp(void);
3948 int Tcl_Init(void *interp);
4049 int Tcl_Eval(void *interp, const char *cmd);
4150 const char* Tcl_GetStringResult(void *interp);
4251 const char* Tcl_GetVar(void* interp, const char* varName, int flags);
4352 char* Tcl_SetVar(void* interp, const char* varName, const char* newValue, int flags);
4453 " ;
54+ }
4555
46- $ this ->ffi = $ this ->loadTclLibrary ($ cdef , $ libDir );
56+ /**
57+ * Returns the FFI C declarations for Tcl 9.0 (macOS).
58+ * Tcl 9 removed Tcl_Eval/Tcl_GetStringResult/Tcl_GetVar/Tcl_SetVar.
59+ */
60+ private function getCdefTcl9 (): string
61+ {
62+ return "
63+ void* Tcl_CreateInterp(void);
64+ int Tcl_Init(void *interp);
65+ int Tcl_EvalEx(void *interp, const char *cmd, int numBytes, int flags);
66+ void* Tcl_GetObjResult(void *interp);
67+ const char* Tcl_GetString(void *objPtr);
68+ const char* Tcl_GetVar2(void* interp, const char* name1, const char* name2, int flags);
69+ char* Tcl_SetVar2(void* interp, const char* name1, const char* name2, const char* newValue, int flags);
70+ " ;
4771 }
4872
4973 /**
5074 * Tries to load the Tcl shared library, attempting bundled first then system paths.
5175 *
5276 * @throws \RuntimeException if no loadable Tcl library is found.
5377 */
54- private function loadTclLibrary (string $ cdef , string $ libDir ): FFI
78+ private function loadTclLibrary (string $ libDir ): FFI
5579 {
80+ // Each candidate: [path, isTcl9]
5681 $ candidates = [];
5782
5883 if (PHP_OS_FAMILY === 'Windows ' ) {
59- $ candidates [] = str_replace ('/ ' , '\\' , $ libDir . 'windows/bin/tcl86t.dll ' );
84+ $ candidates [] = [ str_replace ('/ ' , '\\' , $ libDir . 'windows/bin/tcl86t.dll ' ), false ] ;
6085 } elseif (PHP_OS_FAMILY === 'Darwin ' ) {
61- $ candidates [] = $ libDir . 'libtcl9.0.dylib ' ;
62- $ candidates [] = $ libDir . 'libtcl8.6.dylib ' ;
86+ $ candidates [] = [ $ libDir . 'libtcl9.0.dylib ' , true ] ;
87+ $ candidates [] = [ $ libDir . 'libtcl8.6.dylib ' , false ] ;
6388 } else {
64- $ candidates [] = $ libDir . 'libtcl8.6.so ' ;
65- $ candidates [] = '/usr/lib/x86_64-linux-gnu/libtcl8.6.so ' ;
66- $ candidates [] = '/usr/lib/aarch64-linux-gnu/libtcl8.6.so ' ;
67- $ candidates [] = '/usr/lib64/libtcl8.6.so ' ;
68- $ candidates [] = '/usr/lib/libtcl8.6.so ' ;
69- $ candidates [] = '/usr/local/lib/libtcl8.6.so ' ;
89+ $ candidates [] = [ $ libDir . 'libtcl8.6.so ' , false ] ;
90+ $ candidates [] = [ '/usr/lib/x86_64-linux-gnu/libtcl8.6.so ' , false ] ;
91+ $ candidates [] = [ '/usr/lib/aarch64-linux-gnu/libtcl8.6.so ' , false ] ;
92+ $ candidates [] = [ '/usr/lib64/libtcl8.6.so ' , false ] ;
93+ $ candidates [] = [ '/usr/lib/libtcl8.6.so ' , false ] ;
94+ $ candidates [] = [ '/usr/local/lib/libtcl8.6.so ' , false ] ;
7095 }
7196
72- foreach ($ candidates as $ path ) {
97+ foreach ($ candidates as [ $ path, $ isTcl9 ] ) {
7398 if (!file_exists ($ path )) {
7499 continue ;
75100 }
101+ $ cdef = $ isTcl9 ? $ this ->getCdefTcl9 () : $ this ->getCdefTcl8 ();
76102 try {
77- return FFI ::cdef ($ cdef , $ path );
103+ $ ffi = FFI ::cdef ($ cdef , $ path );
104+ $ this ->isTcl9 = $ isTcl9 ;
105+ return $ ffi ;
78106 } catch (\FFI \Exception $ e ) {
79107 // Bundled binary may be incompatible with this system, try next
80108 continue ;
@@ -95,25 +123,38 @@ private function loadTclLibrary(string $cdef, string $libDir): FFI
95123 */
96124 private function setupLibraryPaths (string $ libDir ): void
97125 {
98- $ tclScriptDir = $ libDir . 'tcl8.6 ' ;
99- $ tkScriptDir = $ libDir . 'tk8.6 ' ;
126+ // macOS uses Tcl/Tk 9.0, Linux uses 8.6
127+ if (PHP_OS_FAMILY === 'Darwin ' ) {
128+ $ tclScriptDir = $ libDir . 'tcl9.0 ' ;
129+ $ tkScriptDir = $ libDir . 'tk9.0 ' ;
130+ } else {
131+ $ tclScriptDir = $ libDir . 'tcl8.6 ' ;
132+ $ tkScriptDir = $ libDir . 'tk8.6 ' ;
133+ }
100134
101135 if (is_dir ($ tclScriptDir )) {
102136 putenv ('TCL_LIBRARY= ' . $ tclScriptDir );
103137 }
104138 if (is_dir ($ tkScriptDir )) {
105139 putenv ('TK_LIBRARY= ' . $ tkScriptDir );
106- // TCLLIBPATH tells Tcl where to search for package directories (like tk8.6/)
140+ // TCLLIBPATH tells Tcl where to search for package directories (like tk9.0/ or tk8.6/)
107141 putenv ('TCLLIBPATH= ' . $ libDir );
108142 }
109143
110- // Add bundled X11 libraries to LD_LIBRARY_PATH so libtk can find them
144+ // Add bundled X11 libraries to LD_LIBRARY_PATH so libtk can find them (Linux)
111145 $ x11Dir = $ libDir . 'x11 ' ;
112146 if (is_dir ($ x11Dir )) {
113147 $ current = getenv ('LD_LIBRARY_PATH ' );
114148 $ newPath = $ current ? $ x11Dir . ': ' . $ current : $ x11Dir ;
115149 putenv ('LD_LIBRARY_PATH= ' . $ newPath );
116150 }
151+
152+ // macOS: ensure bundled dylibs (libtommath) can be found at runtime
153+ if (PHP_OS_FAMILY === 'Darwin ' ) {
154+ $ current = getenv ('DYLD_FALLBACK_LIBRARY_PATH ' );
155+ $ newPath = $ current ? $ libDir . ': ' . $ current : $ libDir ;
156+ putenv ('DYLD_FALLBACK_LIBRARY_PATH= ' . $ newPath );
157+ }
117158 }
118159
119160 /**
@@ -139,9 +180,13 @@ public static function getInstance(): self
139180 public function evalTcl (string $ command )
140181 {
141182 $ interp = $ this ->getInterp ();
142- $ result = $ this ->ffi ->Tcl_Eval ($ interp , $ command );
143- if ($ result !== 0 ) { // Check for errors
144- $ error = $ this ->ffi ->Tcl_GetStringResult ($ interp );
183+ if ($ this ->isTcl9 ) {
184+ $ result = $ this ->ffi ->Tcl_EvalEx ($ interp , $ command , -1 , 0 );
185+ } else {
186+ $ result = $ this ->ffi ->Tcl_Eval ($ interp , $ command );
187+ }
188+ if ($ result !== 0 ) {
189+ $ error = $ this ->getResult ();
145190 throw new \RuntimeException ("Tcl Error: " . $ error );
146191 }
147192 return $ this ->getResult ();
@@ -155,7 +200,12 @@ public function evalTcl(string $command)
155200 public function getResult (): string
156201 {
157202 $ interp = $ this ->getInterp ();
158- $ result = $ this ->ffi ->Tcl_GetStringResult ($ interp );
203+ if ($ this ->isTcl9 ) {
204+ $ objPtr = $ this ->ffi ->Tcl_GetObjResult ($ interp );
205+ $ result = $ this ->ffi ->Tcl_GetString ($ objPtr );
206+ } else {
207+ $ result = $ this ->ffi ->Tcl_GetStringResult ($ interp );
208+ }
159209 if (is_string ($ result )) {
160210 return $ result ;
161211 }
@@ -171,7 +221,11 @@ public function getResult(): string
171221 public function getVar (string $ varName ): string
172222 {
173223 $ interp = $ this ->getInterp ();
174- $ result = $ this ->ffi ->Tcl_GetVar ($ interp , $ varName , 0 );
224+ if ($ this ->isTcl9 ) {
225+ $ result = $ this ->ffi ->Tcl_GetVar2 ($ interp , $ varName , null , 0 );
226+ } else {
227+ $ result = $ this ->ffi ->Tcl_GetVar ($ interp , $ varName , 0 );
228+ }
175229 if (is_string ($ result )) {
176230 return $ result ;
177231 }
@@ -191,7 +245,11 @@ public function getVar(string $varName): string
191245 public function setVar (string $ varName , string $ value ): string
192246 {
193247 $ interp = $ this ->getInterp ();
194- $ result = $ this ->ffi ->Tcl_SetVar ($ interp , $ varName , $ value , 0 );
248+ if ($ this ->isTcl9 ) {
249+ $ result = $ this ->ffi ->Tcl_SetVar2 ($ interp , $ varName , null , $ value , 0 );
250+ } else {
251+ $ result = $ this ->ffi ->Tcl_SetVar ($ interp , $ varName , $ value , 0 );
252+ }
195253 if (is_string ($ result )) {
196254 return $ result ;
197255 }
0 commit comments