I want to operate on a file from C using the tcl C API. From within tcl, this is what I would do:
% set file [open "my_file"]
file3
% myfunc::load $file
where myfunc::load
is from a C extension:
#include <tcl/tcl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#define NS "myfunc"
static int
Load_Cmd(ClientData cdata, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) {
if (objc != 2) {
Tcl_SetObjResult(interp,
Tcl_NewStringObj("wrong # args: should be \" " NS "::open file\"",
-1));
return TCL_ERROR;
}
Tcl_Obj *o = objv[1];
char *chan_name = Tcl_GetString(o);
Tcl_Channel chan = Tcl_GetChannel(interp, chan_name, NULL);
if (chan == NULL) {
return TCL_ERROR;
}
const Tcl_ChannelType * type = Tcl_GetChannelType(chan);
if (!strcmp(type->typeName, "file")) {
return TCL_ERROR;
}
return TCL_OK;
}
int DLLEXPORT
Myfunc_Init(Tcl_Interp *interp) {
Tcl_InitStubs(interp, TCL_VERSION, 0);
Tcl_CreateNamespace(interp, NS, NULL, NULL);
Tcl_CreateObjCommand(interp, NS "::load", Load_Cmd, NULL, NULL);
Tcl_PkgProvide(interp, "myfunc", "1.0");
return TCL_OK;
}
Is there a way to get the FILE
pointer associated with the TCL channel?
I have tried the following:
FILE *data = Tcl_GetChannelInstanceData(chan);
FILE *fp = malloc(sizeof(FILE));
Tcl_GetChannelHandle(chan, TCL_READABLE, (ClientData *) fp);
FILE *fp = malloc(sizeof(FILE));
Tcl_GetOpenFile(interp, chan_name, /*forWriting=*/ 0, /*checkUsage=*/1, (ClientData *) fp);
but none of these seem to work and end in a Segmentation fault, i.e.
% myfunc::load $file
Segmentation fault (core dumped)
Tcl does not use C stdio on any platform; stdio is not very good at handling asynchronous I/O. Instead, Tcl does direct system calls wherever it can; on some platforms, this lets it avoid some really nasty bugs.
On all POSIX platforms (including both Linux and macOS) the underlying handle is actually always a file descriptor, which is of type int
. On Windows, it's potentially one of many things; often a HANDLE
of some kind, sometimes something more complex.
int fd;
if (Tcl_GetChannelHandle(chan, TCL_READABLE, (ClientData *) &fd) != TCL_OK) {
// Some sort of failure...
return TCL_ERROR;
}
On POSIX platforms only, you also can do:
FILE *file;
if (Tcl_GetOpenFile(interp, channelName, 0, 1, (ClientData *) &file) != TCL_OK) {
// Some sort of failure...
return TCL_ERROR;
}
This has a somewhat odd type signature; the final argument should be a FILE **
, but isn't to avoid binding lots of stdio directly into Tcl's API. Tcl still doesn't internally use FILE *
for channels, but in this case will wrap the file descriptor into one for you (with fdopen()
).