tcl.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. // Copyright 2024 The tcl9.0-go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Package tcl9.0 is an idiomatic Go wrapper for [libtcl9.0].
  5. //
  6. // [libtcl9.0]: https://pkg.go.dev/modernc.org/libtcl9.0
  7. package tcl9_0 // import "modernc.org/tcl9.0"
  8. import (
  9. "fmt"
  10. "os"
  11. "path/filepath"
  12. "sync"
  13. "sync/atomic"
  14. "unsafe"
  15. "github.com/evilsocket/islazy/zip"
  16. "modernc.org/libc"
  17. lib "modernc.org/libtcl9.0"
  18. tcllib "modernc.org/libtcl9.0/library"
  19. )
  20. var (
  21. token atomic.Uintptr
  22. objectMu sync.Mutex
  23. objects = map[uintptr]interface{}{}
  24. )
  25. func addObject(o interface{}) uintptr {
  26. t := token.Add(1)
  27. objectMu.Lock()
  28. objects[t] = o
  29. objectMu.Unlock()
  30. return t
  31. }
  32. func getObject(t uintptr) interface{} {
  33. objectMu.Lock()
  34. o := objects[t]
  35. if o == nil {
  36. panic(todo("%#x", t))
  37. }
  38. objectMu.Unlock()
  39. return o
  40. }
  41. func removeObject(t uintptr) {
  42. objectMu.Lock()
  43. if _, ok := objects[t]; !ok {
  44. panic(todo("%#x"))
  45. }
  46. delete(objects, t)
  47. objectMu.Unlock()
  48. }
  49. // EvalFlag is a type used to parameterize Interp.Eval().
  50. type EvalFlag int32
  51. const (
  52. // If this flag bit is set, the script is not compiled to bytecodes; instead it
  53. // is executed directly as is done by Tcl_EvalEx. The TCL_EVAL_DIRECT flag is
  54. // useful in situations where the contents of a value are going to change
  55. // immediately, so the bytecodes will not be reused in a future execution. In
  56. // this case, it is faster to execute the script directly.
  57. EvalDirect = lib.TCL_EVAL_DIRECT
  58. // If this flag is set, the script is evaluated in the global namespace instead
  59. // of the current namespace and its variable context consists of global
  60. // variables only (it ignores any Tcl procedures that are active).
  61. EvalGlobal = lib.TCL_EVAL_GLOBAL
  62. )
  63. var (
  64. onceStdlib sync.Once
  65. onceStdlibErr error
  66. stdlib string
  67. )
  68. // Stdlib returns the path to the Tcl standard library or an error, if any. It
  69. // once creates a temporary directory where the standard library is written.
  70. // Subsequent calls to Stdlib share the same temporary directory.
  71. //
  72. // Stdlib is safe for concurrent access by multiple goroutines.
  73. func Stdlib() (string, error) {
  74. onceStdlib.Do(func() {
  75. dir, err := os.MkdirTemp("", "tcl-library-")
  76. defer func() { onceStdlibErr = err }()
  77. if err != nil {
  78. return
  79. }
  80. fn := filepath.Join(dir, "library.zip")
  81. if err = os.WriteFile(fn, []byte(tcllib.Zip), 0600); err != nil {
  82. return
  83. }
  84. if _, err = zip.Unzip(fn, dir); err != nil {
  85. return
  86. }
  87. stdlib = filepath.Join(dir, "library")
  88. })
  89. return stdlib, onceStdlibErr
  90. }
  91. // MustStdlib is like Stdlib but panics on error.
  92. func MustStdlib() (r string) {
  93. r, err := Stdlib()
  94. if err != nil {
  95. panic(err)
  96. }
  97. return r
  98. }
  99. // Interp represents a Tcl interpreter instance.
  100. type Interp struct {
  101. tls *libc.TLS
  102. interp uintptr
  103. }
  104. // NewInterp returns a newly created Interp or an error, if any.
  105. //
  106. // The 'tclvars' argument is used to set any interpreter variables before
  107. // initializing it.
  108. func NewInterp(tclvars map[string]string) (r *Interp, err error) {
  109. tls := libc.NewTLS()
  110. interp := lib.XTcl_CreateInterp(tls)
  111. if interp == 0 {
  112. tls.Close()
  113. return nil, fmt.Errorf("failed to create a Tcl interpreter")
  114. }
  115. r = &Interp{tls, interp}
  116. for k, v := range tclvars {
  117. cmd := fmt.Sprintf(`set {%s} {%s};`, k, v)
  118. if _, err := r.Eval(fmt.Sprintf(cmd), EvalGlobal); err != nil {
  119. r.Close()
  120. return nil, fmt.Errorf("eval failed: %s", cmd)
  121. }
  122. }
  123. if rc := lib.XTcl_Init(tls, interp); rc != lib.TCL_OK {
  124. r.Close()
  125. return nil, fmt.Errorf("failed to initialize the Tcl interpreter")
  126. }
  127. return r, nil
  128. }
  129. // TLS returns the thread local storage of the underlying interpreter. It is
  130. // used when calling libtcl directly
  131. func (in *Interp) TLS() *libc.TLS { return in.tls }
  132. // Handle returns the handle of the underlying interpreter. It is used when
  133. // calling libtcl directly:
  134. func (in *Interp) Handle() uintptr { return in.interp }
  135. // Close invalidates the interpreter and releases all its associated resources.
  136. func (in *Interp) Close() error {
  137. lib.XTcl_DeleteInterp(in.tls, in.interp)
  138. in.tls.Close()
  139. *in = Interp{}
  140. return nil
  141. }
  142. // Eval evaluates script and returns the interpreter's result and an error, if any.
  143. func (in *Interp) Eval(script string, flag EvalFlag) (r string, err error) {
  144. s, err := libc.CString(script)
  145. if err != nil {
  146. return "", err
  147. }
  148. defer libc.Xfree(in.tls, s)
  149. lib.XTcl_Preserve(in.tls, in.interp)
  150. defer lib.XTcl_Release(in.tls, in.interp)
  151. if rc := lib.XTcl_EvalEx(in.tls, in.interp, s, lib.TTcl_Size(len(script)), int32(flag)); rc != lib.TCL_OK {
  152. err = fmt.Errorf("return code: %d", rc)
  153. }
  154. return in.StringResult(), err
  155. }
  156. // StringResult returns the interpreter result as a string.
  157. func (in *Interp) StringResult() (r string) {
  158. return libc.GoString(lib.XTcl_GetString(in.tls, lib.XTcl_GetObjResult(in.tls, in.interp)))
  159. }
  160. // SetResult sets the result of the interpreter.
  161. func (in *Interp) SetResult(s string) error {
  162. cs, err := libc.CString(s)
  163. if err != nil {
  164. return err
  165. }
  166. defer libc.Xfree(in.tls, cs)
  167. lib.XTcl_SetObjResult(in.tls, in.interp, lib.XTcl_NewStringObj(in.tls, cs, lib.TTcl_Size(len(s))))
  168. return nil
  169. }
  170. // CmdProc is a Tcl command implemented in Go.
  171. type CmdProc func(clientData interface{}, in *Interp, args []string) int
  172. // DeleteProc is a function called when CmdProc is deleted.
  173. type DeleteProc func(clientData interface{})
  174. type cmdProc struct {
  175. clientData interface{}
  176. del DeleteProc
  177. f CmdProc
  178. in *Interp
  179. }
  180. func fp(f interface{}) uintptr {
  181. type iface [2]uintptr
  182. return (*iface)(unsafe.Pointer(&f))[1]
  183. }
  184. var (
  185. runCmdP = fp(runCmd)
  186. delCmdP = fp(delCmd)
  187. )
  188. func runCmd(tls *libc.TLS, clientData, in uintptr, argc int32, argv uintptr) int32 {
  189. cmd := getObject(clientData).(*cmdProc)
  190. var a []string
  191. for i := int32(0); i < argc; i++ {
  192. p := *(*uintptr)(unsafe.Pointer(argv))
  193. argv += unsafe.Sizeof(argv)
  194. a = append(a, libc.GoString(p))
  195. }
  196. return int32(cmd.f(cmd.clientData, cmd.in, a))
  197. }
  198. func delCmd(tls *libc.TLS, clientData uintptr) {
  199. cmd := getObject(clientData).(*cmdProc)
  200. if cmd.del != nil {
  201. cmd.del(cmd.clientData)
  202. }
  203. removeObject(clientData)
  204. }
  205. // RegisterCommand creates a new Tcl command.
  206. func (in *Interp) RegisterCommand(name string, proc CmdProc, clientData interface{}, del DeleteProc) (err error) {
  207. nm, err := libc.CString(name)
  208. if err != nil {
  209. return err
  210. }
  211. lib.XTcl_Preserve(in.tls, in.interp)
  212. defer func() {
  213. libc.Xfree(in.tls, nm)
  214. lib.XTcl_Release(in.tls, in.interp)
  215. }()
  216. p := &cmdProc{f: proc, clientData: clientData, del: del, in: in}
  217. h := addObject(p)
  218. cmd := lib.XTcl_CreateCommand(in.tls, in.interp, nm, runCmdP, h, delCmdP)
  219. if cmd == 0 {
  220. return fmt.Errorf("failed to create command: %s", name)
  221. }
  222. return nil
  223. }