-2

We use a third party Tcl parsing library to validation Tcl script for both syntax and semantic checking. The driver was written in C and defined a set of utility functions. Then it calls Tcl_CreateObjCommand so the script could call these C functions. Now we are in the process of porting the main program to go and I could not find a way to do this. Anyone know a way to call golang functions from Tcl script?

static int
create_utility_tcl_cmds(Tcl_Interp* interp)
{
    if (Tcl_CreateObjCommand(interp, "ip_v4_address",
                        ip_address, (ClientData)AF_INET, NULL) == NULL) {
        TCL_CHECKER_TCL_CMD_EVENT(0, "ip_v4_address");
        return -1;
    }

    .....
    return 0;
}
avariant
  • 2,234
  • 5
  • 25
  • 33
Tony
  • 3
  • 1
  • 1
    You can call Go functions from C code using [cgo](https://blog.golang.org/c-go-cgo), and a Google search for `go tcl` turned up this as the first result: https://github.com/nsf/gothic – Adrian May 30 '19 at 21:08
  • The new main program will be in go so option 1 is out. As for the second, I will look deeper but I daublt it will work with our third party tcl library, as it is pretty old – Tony May 30 '19 at 21:26
  • cgo is going to be the solution to interface with non-Go code no matter what (even the library I linked is a cgo wrapper). You could write your own bindings for your specific library if necessary. – Adrian May 30 '19 at 21:43
  • The callback function registered by `Tcl_CreateCommand` (which might be easier for you to deal with to start with) takes an array of C strings that are the argument words to the function. The client data is just an arbitrary pointer (`void*`) that Tcl passes back without interpretation, and can be NULL if you don't need it; in C++ bindings, it's used to hold a thunk to the object that the command is a method dispatcher for. – Donal Fellows May 31 '19 at 14:38

1 Answers1

1

Assuming you've set the relevant functions as exported and built the Go parts of your project as in

Using Go code in an existing C project

[…]

The important things to note are:

  • The package needs to be called main
  • You need to have a main function, although it can be empty.
  • You need to import the package C
  • You need special //export comments to mark the functions you want callable from C.

I can compile it as a C callable static library with the following command:

go build -buildmode=c-archive foo.go

Then the core of what remains to be done is to write the C glue function from Tcl's API to your Go code. That will involve a function something like:

static int ip_address_glue(
        ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const *objv) {
    // Need an explicit cast; ClientData is really void*
    GoInt address_family = (GoInt) clientData;

    // Check for the right number of arguments
    if (objc != 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "address");
        return TCL_ERROR;
    }

    // Convert the argument to a Go string
    GoString address;
    int len;
    address.p = Tcl_GetStringFromObj(objv[1], &len);
    address.n = len; // This bit is hiding a type mismatch

    // Do the call; I assume your Go function is called ip_address
    ip_address(address_family, address);

    // Assume the Go code doesn't fail, so no need to map the failure back to Tcl

    return TCL_OK;
}

(Credit to https://medium.com/learning-the-go-programming-language/calling-go-functions-from-other-languages-4c7d8bcc69bf for providing enough information for me to work out some of the type bindings.)

That's then the function that you register with Tcl as the callback.

Tcl_CreateObjCommand(interp, "ip_v4_address", ip_address_glue, (ClientData)AF_INET, NULL);

Theoretically, a command registration can fail. Practically, that only happens when the Tcl interpreter (or a few critical namespaces within it) is being deleted.


Mapping a failure into Tcl is going to be easiest if it is encoded at the Go level as an enumeration. Probably easiest to represent success as zero. With that, you'd then do:

GoInt failure_code = ip_address(address_family, address);

switch (failure_code) {
case 0: // Success
    return TCL_OK;
case 1: // First type of failure
    Tcl_SetResult(interp, "failure of type #1", TCL_STATIC);
    return TCL_ERROR;
// ... etc for each expected case ...
default: // Should be unreachable, yes?
    Tcl_SetObjResult(interp, Tcl_ObjPrintf("unexpected failure: %d", failure_code));
    return TCL_ERROR;
}

Passing back more complex return types with tuples of values (especially a combination of a success indicator and a “real” result value) should also be possible, but I've not got a Go development environment in order to probe how they're mapped at the C level.

Community
  • 1
  • 1
Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • It's the mapping of errors that makes this all not as smooth as it should be; `GoInt` is actually the same type as `Tcl_WideInt`, and `GoFloat64` is known as plain old `double` in the Tcl API; those are trivial to handle. The code above indicates how to handle `GoString`. But errors… Tcl's a language that conceptually uses exceptions (though not in a C++ sense!) and Go very much isn't (and I don't know if the conventions for error handling are universally followed) so there's quite an impedance mismatch there. – Donal Fellows Jun 01 '19 at 17:24
  • Also, _be aware that Tcl cares about threads!_ A Tcl interpreter is strongly bound to a single POSIX thread (it's a many-to-one relationship; a thread can have multiple interpreters) in a very deep way inside its implementation. When calling into Tcl from Go, you **need** to respect that _or_ use `Tcl_ThreadQueueEvent()` to asynchronously post an event from elsewhere, which can get a bit complicated… – Donal Fellows Jun 01 '19 at 17:27