ctclc-api

Get a File Pointer from a Tcl_Channel using the Tcl C API


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)

Solution

  • 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()).