critcl_howto_use(n)

๐Ÿ“ฆ version 3.3.1, in alpine 3.24

๐Ÿ’… NAME ๐Ÿ’…

critcl_howto_use โ€” How To Use CriTcl ๐Ÿฆ‹


๐ŸŒธ DESCRIPTION ๐ŸŒธ

okay bestie, welcome to the C Runtime In Tcl (short: CriTcl) page โ€” it's the whole system for embedding and using C code from inside Tcl [http://core.tcl-lang.org/tcl] scripts. โœจ

This document assumes you already have a working CriTcl installation. If that's missing, follow the instructions on How To Install CriTcl. ๐Ÿซถ


โœจ BASICS โœจ

To create a minimal working package, do this:

  1. Choose a directory to develop in and make it your working directory. This should not be a checkout of CriTcl itself. ๐Ÿ’…
  2. Save the following example to a file. In the following it is assumed that the file was named "example.tcl".
# -*- tcl -*-
# Critcl support, absolutely necessary.
package require critcl

# Bail out early if the compile environment is not suitable.
if {![critcl::compiling]} {
    error "Unable to build project, no proper compiler found."
}

# Information for the teapot.txt meta data file put into a generated package.
# Free form strings.
critcl::license {Andreas Kupries} {Under a BSD license}

critcl::summary {The first CriTcl-based package}

critcl::description {
    This package is the first example of a CriTcl-based package. It contains all the
    necessary and conventionally useful pieces.
}

critcl::subject example {critcl package}
critcl::subject {basic critcl}

# Minimal Tcl version the package should load into.
critcl::tcl 8.6

# Use to activate Tcl memory debugging
#critcl::debug memory
# Use to activate building and linking with symbols (for gdb, etc.)
#critcl::debug symbols

# ## #### ######### ################ #########################
## A hello world, directly printed to stdout. Bypasses Tcl's channel system.

critcl::cproc hello {} void {
    printf("hello world\n");
}

# ## #### ######### ################ #########################

# Forcing compilation, link, and loading now.
critcl::msg -nonewline { Building ...}
if {![critcl::load]} {
    error "Building and loading the project failed."
}

# Name and version the package. Just like for every kind of Tcl package.
package provide critcl-example 1
  1. Invoke the command:
critcl -keep -debug all -pkg example.tcl

This compiles the example and installs it into a lib/ sub directory of the working directory, generating output similar to this during operation:

Config:   linux-x86_64-gcc
Build:    linux-x86_64-gcc
Target:   linux-x86_64
Source:   example.tcl  (provide critcl-example 1) Building ...
Library:  example.so
 (tclStubsPtr     =>  const TclStubs *tclStubsPtr;)
 (tclPlatStubsPtr =>  const TclPlatStubs *tclPlatStubsPtr;)
Package:  lib/example
Files left in /home/aku/.critcl/pkg2567272.1644845439

The -keep option suppressed the cleanup of the generated C files, object files, compiler log, etc. that is normally done at the end of building. ๐Ÿ‘€

% ls -l /home/aku/.critcl/pkg2567272.1644845439
total 36
-rw-r--r-- 1 aku aku  1260 Feb 14 18:30 v3118_00000000000000000000000000000004.c
-rw-r--r-- 1 aku aku  2096 Feb 14 18:30 v3118_00000000000000000000000000000004_pic.o
-rw-r--r-- 1 aku aku  1728 Feb 14 18:30 v3118_00000000000000000000000000000009.c
-rw-r--r-- 1 aku aku  2448 Feb 14 18:30 v3118_00000000000000000000000000000009_pic.o
-rwxr-xr-x 1 aku aku 14424 Feb 14 18:30 v3118_00000000000000000000000000000009.so
-rw-r--r-- 1 aku aku  1725 Feb 14 18:30 v3118.log

This enables inspection of the generated C code. Simply drop the option from the command if that is not desired. ๐Ÿ’–

The option -debug, with argument all, activated Tcl's memory debugging and caused the generation of the symbol tables needed by gdb or any other debugger. The alternate arguments memory and symbols activate just one of these. โญ

  1. Now invoke an interactive tclsh and enter the commands:
  • lappend auto_path lib
  • package require critcl-example
  • info loaded
  • hello
  • exit

I.e. extend tclsh's package search path to include the location of the new package, load the package, verify that the associated shared library is present, invoke the package command, and stop the session. ๐ŸŽ€

When the package command is invoked the terminal will show hello world, followed by the prompt.

Commands: critcl::compiling, critcl::cproc, critcl::description, critcl::license, critcl::load, critcl::msg, critcl::subject, critcl::summary, critcl::tcl.

Make a copy of "example.tcl" before going through the sub-sections. Keep it as a save point to return to from the editing done in the sub-section. ๐ŸŒˆ


๐Ÿงš SIMPLE ARGUMENTS ๐Ÿงš

A function taking neither arguments nor returning results is not very useful. ๐Ÿ˜ญ

  1. We are now extending the command to take an argument.
  2. Starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with:
critcl::cproc hello {double x} void {
    /* double x; */
    printf("hello world, we have %f\n", x);
}

and that's the edit. โœจ

  1. When testing the package again, entering the simple hello will fail.

The changed command is now expecting an argument, and we gave it none.

Retry by entering:

hello 5

instead. Now the command behaves as expected and prints the provided value. ๐Ÿ’…

Further try and enter:

hello world

This will fail again. The command expected a real number and we gave it something decidedly not so. ๐Ÿ’€

These checks (argument count, argument type) are implemented in the translation layer CriTcl generates for the C function. The function body is never invoked. ๐Ÿ”ฅ


๐Ÿ“ SIMPLE RESULTS ๐Ÿ“

A function taking neither arguments nor returning results is not very useful. ๐Ÿ˜ญ

  1. We are now extending the command to return a result.
  2. Starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with:
critcl::cproc twice {double x} double {
    return 2*x;
}

and that's it. โœจ

  1. Note that the name of the command changed. Goodbye hello, hello twice. ๐Ÿ‘‹
  2. Invoke:
twice 4

and in the terminal.

An important limitation of the commands implemented so far is that they cannot fail. The types used so far (void, double) and related scalar types can return only a value of the specified type, and nothing else. They have no ability to signal an error to the Tcl script.

We will come back to this after knowing a bit more about the more complex argument and result types.

Of interest to the eager reader: CriTcl cproc Type Reference. ๐Ÿฆ‹


๐ŸŒธ RANGE-LIMITED SIMPLE ARGUMENTS ๐ŸŒธ

  1. Starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with:
critcl::cproc hello {{double > 5 < 22} x} void {
    /* double x, range 6-21; */
    printf("hello world, we have %f\n", x);
}

and that's the edit. โœจ

  1. When dealing with simple arguments whose range of legal values is limited to a single continuous interval extend the base type with the necessary relations (>, >=, <, and <=) and limiting values.

Note that the limiting values have to be proper constant numbers acceptable by the base type. Symbolic values are not accepted. ๐Ÿ™ˆ

Here the argument x of the changed function will reject all values outside of the interval 6 to 21. ๐Ÿ‘‘


๐Ÿฆ‹ STRING ARGUMENTS ๐Ÿฆ‹

Tcl prides itself on the fact that Everything Is A String. So how are string values passed into C functions? ๐Ÿค”

  1. We are now extending the command with a string argument.
  2. Starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with:
critcl::cproc hello {pstring x} void {
    /* critcl_pstring x (.s, .len, .o); */
    printf("hello world, from %s (%d bytes)\n", x.s, x.len);
}

and that's the edit. โœจ

  1. Testing hello with any kind of argument the information is printed. ๐Ÿ’–
  2. Of note here is that the command argument x is a structure. ๐Ÿซถ
  3. The example uses only two of the three fields, the pointer to the string data (.s), and the length of the string (.len). In bytes, not in characters, because Tcl's internal representation of strings uses a modified UTF-8 encoding. A character consists of between 1 and TCL_UTF_MAX bytes. ๐ŸŒˆ
  4. Attention The pointers (.s) refer into data structures internal to and managed by the Tcl interpreter. Changing them is highly likely to cause subtle and difficult to track down bugs. Any and all complex arguments must be treated as Read-Only. Never modify them.
bestie I mean it โ€” never modify those complex arguments. ๐Ÿ˜ญ
  1. Use the simpler type char* if and only if the length of the string is not relevant to the command, i.e. not computed, or not used by any of the functions called from the body of the command. Its value is essentially just the .s field of pstring's structure. This then looks like:
critcl::cproc hello {char* x} void {
    /* char* x; */
    printf("hello world, from %s\n", x);
}

๐Ÿ’œ STRING RESULTS ๐Ÿ’œ

Tcl prides itself on the fact that Everything Is A String. So how are string values returned from C functions? ๐Ÿค”

  1. We are now giving the command a string result.
  2. Starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with:
critcl::cproc twice {double x} char* {
    char buf [lb]40[rb];
    sprintf(buf, "%f", 2*x);
    return buf;
}

and that's the edit. โœจ

  1. Note that the name of the command changed. Goodbye hello, hello twice. ๐Ÿ‘‹
  2. Invoke:
twice 4

and in the terminal.

  1. Attention. To the translation layer the string pointer is owned by the C code. A copy is made to become the result seen by Tcl. ๐ŸŽ€

While the C code is totally allowed to allocate the string on the heap if it wants to โ€” like, go off โ€” that comes with the responsibility of freeing it too. Ghosting that responsibility? Memory leaks. And not the cute kind. ๐Ÿ’€

The type char* is the recommended vibe for static string buffers, string constants, and honestly anything like that.

  • [6] โ€” Conversely (love that word), if you want to return heap-allocated strings, the recommended move is to use the type string instead.

Replace the definition of twice with

critcl::cproc twice {double x} string {
    char* buf = Tcl_Alloc (40);
    sprintf(buf, "%f", 2*x);
    return buf;
}

Now the translation layer takes ownership of the string from the C code and hands that ownership straight to the Tcl interpreter. Which means the string gets released whenever the Tcl interpreter is done with it. The C code has zero say in the lifecycle of the string anymore โ€” and if the C code tries to release it anyway, that's a whole situation. Dangling pointers, associated memory corruption, crashes. Not the slay we're going for. ๐Ÿ˜ญ


๐ŸŽ€ LIST ARGUMENTS ๐ŸŽ€

Even though Tcl is, like, a string-oriented language, it can absolutely handle more complex structures. The first of these โ€” and it's been there with Tcl since the very beginning โ€” is lists. Sets of values indexed by a numeric value.

In C parlance? Arrays. Bestie, it's arrays. ๐Ÿฆ‹

  • [1] โ€” We're now extending the command with a list argument, periodt.
  • [2] โ€” Starting from the Basics. Edit the file example.tcl. Remove the definition of hello. Replace it with
critcl::cproc hello {list x} void {
    /* critcl_list x (.o, .v, .c); */
    printf("hello world, %d elements in (%s)\n", x.c, Tcl_GetString (x.o));
}

and

  • [3] โ€” Testing hello with literally any kind of list argument will print basic information about it. Cute, right?
  • [4] โ€” Of note here: the command argument x is a structure. Main character energy tbh.
  • [5] โ€” The example uses only two of the three fields: the pointer to the original Tcl_Obj* holding the list (.o), and the length of the list (.c) in elements.

The field .v, not used above, is the C array holding the Tcl_Obj* pointers to the list elements.

  • [6] โ€” Attention The pointers .o and .v refer into data structures internal to and managed by the Tcl interpreter. Changing them is highly likely to cause subtle and difficult to track down bugs. Any and all complex arguments must be treated as Read-Only. Never modify them. I'm so serious. ๐Ÿซถ
  • [7] โ€” As a last note, this argument type does not place any constraints on the size of the list, or on the type of the elements.

โœจ CONSTRAINED LIST ARGUMENTS โœจ

As mentioned at the end of section List Arguments, the basic list type places no constraints on the size of the list, nor on the type of the elements.

Both kinds of constraints can be done though โ€” alone or together. Glow-up time. ๐Ÿ’…

  • [1] โ€” We are now extending the command with a length-limited list.
  • [2] โ€” Starting from the Basics. Edit the file example.tcl. Remove the definition of hello. Replace it with
critcl::cproc hello {[5] x} void {
    /* critcl_list x (.o, .v, .c); */
    printf("hello world, %d elements in (%s)\n", x.c, Tcl_GetString (x.o));
}

and

  • [3] โ€” Testing the new command will show that only lists holding exactly 5 elements will be accepted.
  • [4] โ€” To accept lists of any length use [] or [*]. Both forms are actually aliases of the base type, i.e. list.
  • [5] โ€” To constrain just the type of elements โ€” for example to type int โ€” use
int[]

or

[]int
  • [6] โ€” To combine both type and length constraints use the forms
int[5]

or

[5]int
  • [7] โ€” The last, most C-like forms of these constraints place the list indicator syntax on the argument instead of the type. I.e.
int a[]

or

int a[5]

๐Ÿ”ฅ RAW TCL_OBJ* ARGUMENTS ๐Ÿ”ฅ

When the set of predefined argument types just isn't enough, the oldest way of handling the situation is falling back to the structures Tcl itself uses to manage values โ€” i.e. Tcl_Obj*. Old school, but iconic.

  • [1] โ€” Starting from the Basics. Edit the file example.tcl. Remove the definition of hello. Replace it with
critcl::cproc hello {object x} void {
    /* Tcl_Obj* x */
    int len;
    char* str = Tcl_GetStringFromObj (x, &len);
    printf("hello world, from %s (%d bytes)\n", str, len);
}

and

  • [2] โ€” Having direct access to the raw Tcl_Obj* value means all the functions of the public Tcl API for working with Tcl values become usable. The downside? All the considerations for handling them apply as well. ๐Ÿ™ˆ

In other words, the C code becomes responsible for handling the reference counts correctly, for duplicating shared Tcl_Obj* structures before modifying them, etc.

One thing the C code is allowed to do without restriction is to shimmer the internal representation of the value as needed, through the associated Tcl API functions. For example Tcl_GetWideIntFromObj and the like. It actually has to be allowed to do so, as the type checking done as part of such conversions is now the responsibility of the C code as well.

For the predefined types, this is all hidden in the translation layer generated by CriTcl.

If more than one command has to perform the same kind of checking and/or conversion, it is recommended to move the core of the code into proper C functions for proper sharing among the commands.

  • [3] โ€” This is best done by defining a custom argument type using CriTcl commands. This extends the translation layer CriTcl is able to generate. The necessary conversions, type checks, etc. are then again hidden from the bulk of the application C code.

We will come back to this. ๐Ÿ‘€


๐Ÿ’– RAW TCL_OBJ* RESULTS ๐Ÿ’–

When the set of predefined result types is not enough, the oldest way of handling the situation is falling back to the structures Tcl uses to manage values, i.e. Tcl_Obj*.

Two builtin types are provided for this, to handle different reference counting requirements.

  • [1] โ€” Starting from the Basics. Edit the file example.tcl. Remove the definition of hello. Replace it with
critcl::cproc twice {double x} object0 {
    return Tcl_NewDoubleObj(2*x);
}

and

  • [2] โ€” With object0 the translation layer assumes that the returned Tcl_Obj* value has a reference count of 0. I.e. a value which is unowned and unshared.

This value is passed directly to Tcl for its use, without any changes. Tcl increments the reference count and thus takes ownership. The value is still unshared.

It would be extremely detrimental if the translation layer had decremented the reference count before passing the value. That action would release the memory and then leave Tcl with a dangling pointer and the associated memory corruption bug to come. So, like, don't. ๐Ÿ’€

  • [3] โ€” The situation changes when the C code returns a Tcl_Obj* value with a reference count greater than 0. I.e. at least owned (by the C code), and possibly even shared. There are some object constructors and/or mutators in the public Tcl API which do that, although I do not recall their names. The example below simulates this situation by explicitly incrementing the reference count before returning the value.
  • [4] โ€” In this case use the type object (without the trailing 0).
  • [5] โ€” Edit the file example.tcl and replace the definition of twice with
critcl::cproc twice {double x} object {
    Tcl_Obj* result = Tcl_NewDoubleObj(2*x);
    Tcl_IncrRefCount (result);
    return result;
}

and

  • [6] โ€” After handing the value to Tcl, with the associated incremented reference count, the translation layer decrements the reference count, invalidating the C code's ownership and leaving the final reference count the same.

Note, the order matters. If the value has only one reference then decrementing it before Tcl increments it would again release the value, and again leave Tcl with a dangling pointer.

Also, not decrementing the reference count at all causes the inverse problem to the memory corruption issues of before: memory leaks. ๐Ÿ˜ญ

  • [7] โ€” Note that both types transfer ownership of the value. Their difference is just in the reference count of the value coming out of the function, and the (non-)actions having to be (not) taken to effect said transfer without causing memory issues.

๐ŸŒˆ ERRORS & MESSAGES ๐ŸŒˆ

  • [1] โ€” Starting from the Basics. Edit the file example.tcl. Remove the definition of hello. Replace it with
critcl::cproc sqrt {
    Tcl_Interp* interp
    double      x
} object0 {
    if (x < 0) {
        Tcl_SetObjResult (interp, Tcl_ObjPrintf ("Expected double >=0, but got \"%d\"", x));
        Tcl_SetErrorCode (interp, "EXAMPLE", "BAD", "DOMAIN", NULL);
        return NULL;
    }
    return Tcl_NewDoubleObj(sqrt(x));
}

and

  • [2] โ€” In standard C-based packages, commands signal errors by returning TCL_ERROR, placing the error message as the interpreter result, and maybe providing an error code via Tcl_SetErrorCode.
  • [3] โ€” When using critcl::cproc this is limited and hidden.
  • [4] โ€” The simple and string types for results do not allow failure. The value is returned to the translation layer, converted into the interpreter result, and then reported as success (TCL_OK).
  • [5] โ€” The object types on the other hand do allow for failure. Return a NULL value to signal failure to the translation layer, which then reports this to the interpreter via the standard TCL_ERROR.
  • [6] โ€” Attention Setting the desired error message and code into the interpreter is still the responsibility of the function body. No one's doing that for you, bestie. ๐Ÿซถ

๐Ÿงš TCL_INTERP* ACCESS ๐Ÿงš

  • [1] โ€” Reread the example in the previous section.
  • [2] โ€” Note the type Tcl_Interp* used for the first argument.
  • [3] โ€” This type is special. โœจ
  • [4] โ€” An argument of this type has to be the first argument of a function. Non-negotiable, it's giving rules.
  • [5] โ€” Using it tells CriTcl that the function needs access to the Tcl interpreter calling it. It then arranges for that to happen in the generated C code. King behavior honestly. ๐Ÿ‘‘
  • [6] โ€” this special argument is not visible at the script level, bestie. The Tcl side never sees it.
  • [7] โ€” and it's not an argument of the Tcl command for the function either.
  • [8] โ€” in our example the sqrt command is called with a single argument.
  • [9] โ€” the name of the argument is totally up to you, no cap. It's the type that's important, and the type is what triggers the special behaviour. Her preferred names are ip and interp.

๐Ÿงฌ BINARY DATA ARGUMENTS ๐Ÿงฌ

  • [1] โ€” starting from the Basics, edit the file "example.tcl". Remove the definition of hello. Replace it with
critcl::cproc hello {bytes x} void {
    /* critcl_bytes x (.s, .len, .o); */
    printf("hello world, with %d bytes \n data: ", x.len);
    for (i = 0; i < x.len; i++) {
        printf(" %02x", x.s[i]);
        if (i % 16 == 15) printf ("\ndata: ");
    }
    if (i % 16 != 0) printf ("\n");
}

and

  • [2] โ€” to deal with strings holding binary data, use the type bytes. It makes sure the function sees the proper binary data and not however Tcl is encoding it internally โ€” which is literally what the string types would give you. no encoding chaos, we love to see it ๐Ÿ’…

๐Ÿ”ฎ CONSTANT BINARY DATA RESULTS ๐Ÿ”ฎ

  • [1] โ€” use the command critcl::cdata to create a command that takes no arguments and returns a constant ByteArray value.
# P5 3 3 255 \n ...
critcl::cdata cross3x3pgm {
    80 52 32 51 32 51 32 50 53 53 10
    0   255 0
    255 255 255
    0   255 0
    }

a whole little PGM file living in your source, iconic ๐ŸŽ€


โญ TCL RUNTIME VERSION โญ

  • [1] โ€” see and reread the basic package for the introduction of the commands referenced below. ๐Ÿ‘‘
  • [2] โ€” use the command critcl::tcl to tell CriTcl the minimal version of Tcl the package is to be used with.

This determines which Tcl headers all files are compiled against, and what version of the public Tcl API is available to the C code.

Currently 8.4, 8.5 and 8.6 are supported. that's the whole roster, bestie

If not specified, 8.4 is assumed.


๐ŸŒˆ ADDITIONAL TCL CODE ๐ŸŒˆ

  • [1] โ€” starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with
critcl::cproc greetings::hello {} void {
    printf("hello world\n");
}

critcl::cproc greetings::hi {} void {
    printf("hi you\n");
}

and

  • [2] โ€” the command hello is now available as greetings::hello, and a second command greetings::hi was added. two for the price of... well, two โœจ
  • [3] โ€” Tcl has automatically created the namespace greetings. Not you. Tcl. main character energy
  • [4] โ€” create a file "example-policy.tcl" and enter
namespace eval greetings {
    namespace export hello hi
    namespace ensemble create
}

into it.

  • [5] โ€” edit "example.tcl". Add the code
critcl::tsources example-policy.tcl

and

  • [6] โ€” the added Tcl code makes greetings available as an ensemble command.

The commands in the namespace have been registered as methods of the ensemble.

They can now be invoked as

greetings hello
greetings hi
  • [7] โ€” the Tcl builtin command string is an ensemble as well, as is clock. iykyk ๐Ÿ’œ

New commands: critcl::tsources


๐Ÿ› DEBUGGING SUPPORT ๐Ÿ›

  • [1] โ€” see and reread the basic package for the introduction of the commands referenced below.
  • [2] โ€” use the command critcl::debug to activate various features supporting debugging.
critcl::debug memory  ;# Activate Tcl memory debugging (-DTCL_MEM_DEBUG)
critcl::debug symbols ;# Activate building and linking with debugger symbols (-g)
critcl::debug all     ;# Shorthand for both `memory` and `symbols`.

bugs fear her ๐Ÿ”ฅ


๐Ÿ“ฆ INSTALL THE PACKAGE ๐Ÿ“ฆ

  • [1] โ€” starting from the Basics.
  • [2] โ€” use an interactive tclsh seesion to determine the value of info library.

For the purpose of this HowTo assume that this path is "/home/aku/opt/ActiveTcl/lib/tcl8.6"

  • [3] โ€” invoke the critcl application in a terminal, using
critcl -libdir /home/aku/opt/ActiveTcl/lib/tcl8.6 -pkg example.tcl
  • [4] โ€” the package is now build and installed into the chosen directory.
% find /home/aku/opt/ActiveTcl/lib/tcl8.6/example/
 /home/aku/opt/ActiveTcl/lib/tcl8.6/example/
 /home/aku/opt/ActiveTcl/lib/tcl8.6/example/pkgIndex.tcl
 /home/aku/opt/ActiveTcl/lib/tcl8.6/example/critcl-rt.tcl
 /home/aku/opt/ActiveTcl/lib/tcl8.6/example/license.terms
 /home/aku/opt/ActiveTcl/lib/tcl8.6/example/linux-x86_64
 /home/aku/opt/ActiveTcl/lib/tcl8.6/example/linux-x86_64/example.so
 /home/aku/opt/ActiveTcl/lib/tcl8.6/example/teapot.txt

look at that directory tree, she ATE ๐Ÿ’…๐ŸŒธ


๐Ÿงš USING EXTERNAL LIBRARIES ๐Ÿงš

To create a minimal package wrapping an external library

  • [1] โ€” choose a directory to develop in and make it the working directory. This should not be a checkout of CriTcl itself.
  • [2] โ€” save the following example to a file. In the following it is assumed that the file was named "example.tcl".
# -*- tcl -*-
# Critcl support, absolutely necessary.
package require critcl

# Bail out early if the compile environment is not suitable.
if {![critcl::compiling]} {
    error "Unable to build project, no proper compiler found."
}

# Information for the teapot.txt meta data file put into a generated package.
# Free form strings.
critcl::license {Andreas Kupries} {Under a BSD license}

critcl::summary {The second CriTcl-based package}

critcl::description {
    This package is the second example of a CriTcl-based package. It contains all the
    necessary and conventionally useful pieces for wrapping an external library.
}

critcl::subject {external library usage} example {critcl package}
critcl::subject {wrapping external library}

# Minimal Tcl version the package should load into.
critcl::tcl 8.6

# Locations for headers and shared library of the library to wrap.
# Required only for non-standard locations, i.e. where CC is not searching by default.
critcl::cheaders   -I/usr/include
critcl::clibraries -L/usr/lib/x86_64-linux-gnu
critcl::clibraries -lzstd

# Import library API, i.e. headers.
critcl::include zstd.h

# ## #### ######### ################ #########################
## (De)compression using Zstd
## Data to (de)compress is passed in and returned as Tcl byte arrays.

critcl::cproc compress {
    Tcl_Interp* ip
    bytes       data
    int         {level ZSTD_CLEVEL_DEFAULT}
} object0 {
    /* critcl_bytes data; (.s, .len, .o) */
    Tcl_Obj* error_message;

    int max = ZSTD_maxCLevel();
    if ((level < 1) || (level > max)) {
        error_message = Tcl_ObjPrintf ("level must be integer between 1 and %d", max);
        goto err;
    }

    size_t dest_sz  = ZSTD_compressBound (data.len);
    void*  dest_buf = Tcl_Alloc(dest_sz);

    if (!dest_buf) {
        error_message = Tcl_NewStringObj ("can't allocate memory to compress data", -1);
        goto err;
    }

    size_t compressed_size = ZSTD_compress (dest_buf, dest_sz,
              data.s,   data.len,
              level);
    if (ZSTD_isError (compressed_size)) {
        Tcl_Free(dest_buf);
        error_message = Tcl_ObjPrintf ("zstd encoding error: %s",
                       ZSTD_getErrorName (compressed_size));
        goto err;
    }

    Tcl_Obj* compressed = Tcl_NewByteArrayObj (dest_buf, compressed_size);
    Tcl_Free (dest_buf);

    return compressed;
err:
    Tcl_SetObjResult (ip, error_message);
    return 0;
}

critcl::cproc decompress {
    Tcl_Interp*  ip
    bytes        data
} object0 {
    Tcl_Obj* error_message;

    size_t dest_sz = ZSTD_getDecompressedSize (data.s, data.len);
    if (dest_sz == 0) {
        error_message = Tcl_NewStringObj("invalid data", -1);
        goto err;
    }

    void* dest_buf = Tcl_Alloc (dest_sz);
    if (!dest_buf) {
        error_message = Tcl_NewStringObj("failed to allocate decompression buffer", -1);
        goto err;
    }

    size_t decompressed_size = ZSTD_decompress (dest_buf, dest_sz,
          data.s,   data.len);
    if (decompressed_size != dest_sz) {
        Tcl_Free (dest_buf);
        error_message = Tcl_ObjPrintf("zstd decoding error: %s",
                ZSTD_getErrorName (decompressed_size));
        goto err;
    }

    Tcl_Obj* decompressed = Tcl_NewByteArrayObj (dest_buf, dest_sz);
    Tcl_Free (dest_buf);

    return decompressed;

err:
    Tcl_SetObjResult (ip, error_message);
    return 0;
}

# ## #### ######### ################ #########################

# Forcing compilation, link, and loading now.
critcl::msg -nonewline { Building ...}
if {![critcl::load]} {
    error "Building and loading the project failed."
}

# Name and version the package. Just like for every kind of Tcl package.
package provide critcl-example 1
  • [3] โ€” build the package. See the Basics, if necessary.
  • [4] โ€” load the package and invoke the commands.
โš ๏ธ Attention. The commands take and return binary data. This may look very bad in the terminal. like, do not panic, it's supposed to look like that ๐Ÿ˜ญ๐Ÿ’€
  • [5] โ€” to test the commands enter
set a [compress {hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhello wwwwwwwworld}]

decompress $a

in. go on, type it, it's giving power user โญ

New commands: critcl::cheaders, critcl::clibraries, critcl::include.


๐Ÿ“ DEFAULT VALUES FOR ARGUMENTS ๐Ÿ“

  • [1] โ€” reread the example of the main section. Note specifically the line
int {level ZSTD_CLEVEL_DEFAULT}
  • [2] โ€” this line demonstrates that critcl::cproc arguments are allowed to have default values, in the same vein as proc arguments, and using the same syntax. same vibe, same syntax, we stan consistency ๐Ÿซถ
  • [3] โ€” Attention: default values have to be legal C rvalues and match the C type of the argument.

They are literally pasted into the generated C code. literally literally ๐Ÿ‘‘


Okay so real talk, bestie: they bypass any argument validation done in the generated translation layer. ๐Ÿ˜ณ That means it is possible to use a value an invoker of the command cannot use from Tcl. No cap.

[4] This kind of in-band signaling of a default versus a regular argument is however not necessary. Look at

critcl::cproc default_or_not {int {x 0}} void {
    if !has_x {
        printf("called with default\n");
        return
    }
    printf("called with %d\n", x);
      }

Any argument x with a default causes CriTcl to create a hidden argument has_x, of type int (boolean). This argument is set to 1 when x was filled from defaults, and 0 else. ๐Ÿ’…

โœจ CUSTOM ARGUMENT VALIDATION โœจ

[1] Starting from the base wrapper. Edit the file example.tcl. Replace the entire compress function with this absolute unit:

critcl::argtype zstd_compression_level {
    /* argtype: `int` */
    if (Tcl_GetIntFromObj (interp, @@, &@A) != TCL_OK) return TCL_ERROR;
    /* additional validation */
    int max = ZSTD_maxCLevel();
    if ((@A < 1) || (@A > max)) {
        Tcl_SetObjResult (interp,
            Tcl_ObjPrintf ("zstd compression level must be integer between 1 and %d", max));
        return TCL_ERROR;
    }
    /* @@: current objv[] element
    ** @A: name of argument variable for transfer to C function
    ** interp: predefined variable, access to current interp - error messages, etc.
    */
} int int ;# C types of transfer variable and function argument.

critcl::cproc compress {
    Tcl_Interp*            ip
    bytes                  data
    zstd_compression_level {level ZSTD_CLEVEL_DEFAULT}
} object0 {
    /* critcl_bytes data; (.s, .len, .o) */
    /* int level; validated to be in range 1...ZSTD_maxCLevel() */

    Tcl_Obj* error_message;

    size_t dest_sz  = ZSTD_compressBound (data.len);
    void*  dest_buf = Tcl_Alloc(dest_sz);

    if (!dest_buf) {
        error_message = Tcl_NewStringObj ("can't allocate memory to compress data", -1);
        goto err;
    }

    size_t compressed_size = ZSTD_compress (dest_buf, dest_sz,
                                            data.s,   data.len,
                                            level);
    if (ZSTD_isError (compressed_size)) {
        Tcl_Free(dest_buf);
        error_message = Tcl_ObjPrintf ("zstd encoding error: %s",
                                       ZSTD_getErrorName (compressed_size));
        goto err;
    }

    Tcl_Obj* compressed = Tcl_NewByteArrayObj (dest_buf, compressed_size);
    Tcl_Free (dest_buf);

    return compressed;
err:
    Tcl_SetObjResult (ip, error_message);
    return 0;
}

And hereโ€™s the tea: in the original example the level argument of the function was validated in the function itself. This may detract from the functionality of interest itself, especially if there are lots of arguments requiring validation. If the same kind of argument is used in multiple places this causes code duplication in the functions as well. ๐Ÿ˜ญ

Use a custom argument type as defined by the modification to move this kind of validation out of the function, and enhance readability. โœจ

Code duplication however is only partially addressed. While there is no duplication in the visible definitions the C code of the new argument type is replicated for each use of the type. ๐Ÿซ 

[2] Now replace the argtype definition with

critcl::code {
    int GetCompressionLevel (Tcl_Interp* interp, Tcl_Obj* obj, int* level)
    {
        if (Tcl_GetIntFromObj (interp, obj, level) != TCL_OK) return TCL_ERROR;

        int max = ZSTD_maxCLevel();
        if ((*level < 1) || (*level > max)) {
            Tcl_SetObjResult (interp,
                Tcl_ObjPrintf ("zstd compression level must be integer between 1 and %d", max));
            return TCL_ERROR;
        }
        return TCL_OK;
    }
}

critcl::argtype zstd_compression_level {
    if (GetCompressionLevel (@@, &@A) != TCL_OK) return TCL_ERROR;
} int int

and now only the calls to the new validation function are replicated. The function itself exists only once. Slay. ๐Ÿ’…

๐ŸŽ€ SEPARATING LOCAL C SOURCES ๐ŸŽ€

[1] Starting from the end of the previous section. Edit the file example.tcl.

[2] Save the contents of the critcl::ccode block into a file example.c and then replace the entire block with

critcl::csources example.c

critcl::ccode {
extern int GetCompressionLevel (Tcl_Interp* interp, Tcl_Obj* obj, int* level);
}

When mixing C and Tcl code the different kind of indentation rules for these languages may come into strong conflict. Further, very large blocks of C code may reduce overall readability. ๐Ÿ˜ตโ€๐Ÿ’ซ

[3] The examples fixes this by moving the code block into a local C file and then registering this file with CriTcl. When building the package CriTcl arranges to build all such registered C files as well.

[4] Attention. The C code is now in a separate compilation unit. The example declares the exported function so that the cprocs are again able to see and use it. โš ๏ธ

[5] Now go a step further. Save the declaration into a file example.h, and then use

critcl::include example.h

to

critcl::ccode {
    #include "example.h"
}

[6] As an alternative solution, start from the beginning of the section and move the entire original critcl::ccode block into a file example-check.tcl.

Then replace it with

critcl::source example-check.tcl
Attention: Tcl's builtin command source is not suitable for importing the separate file due to how CriTcl uses the information from info script to key various internal datastructures. ๐Ÿšจ

๐ŸŒˆ VERY SIMPLE RESULTS ๐ŸŒˆ

[1] Starting from the end of the validation section. Edit the file example.tcl. Add the code below, just before the compress command.

critcl::cconst version   char* ZSTD_VERSION_STRING
critcl::cconst min-level int   1
critcl::cconst max-level int   ZSTD_maxCLevel()

and

[2] These declarations create three additional commands, each returning the specified value. A fixed string, an integer, and a function call returning an integer. ๐Ÿ‘‘

[3] Attention The values have to be legal C rvalues and match the C type of the result. They are literally pasted into the generated C code.

[4] When using critcl::cconst CriTcl is aware that the result of the function does not depend on any parameters and is computed in a single C expression.

This enables it to do away with the internal helper function it would need and generate if critcl::cproc had been used instead. For example

critcl::cproc version {} char* {
    return ZSTD_VERSION_STRING;
}

๐Ÿฆ‹ STRUCTURE ARGUMENTS ๐Ÿฆ‹

[1] For all that this is a part of how to Use External Libraries, for the demonstration only the basics are needed.

[2] Starting from the Basics. Edit the file example.tcl. Remove the definition of hello. Replace it with

critcl::ccode {
    typedef struct vec2 {
        double x;
        double y;
    } vec2;

    typedef vec2* vec2ptr;

    int
    GetVecFromObj (Tcl_Interp* interp, Tcl_Obj* obj, vec2ptr* vec)
    {
        int len;
        if (Tcl_ListObjLength (interp, obj, &len) != TCL_OK) return TCL_ERROR;
        if (len != 2) {
       Tcl_SetObjResult (interp, Tcl_ObjPrintf ("Expected 2 elements, got %d", len));
       return TCL_ERROR;
        }

        Tcl_Obj* lv[2];
        if (Tcl_ListObjGetElements (interp, obj, &lv) != TCL_OK) return TCL_ERROR;

        double x, y;
        if (Tcl_GetDoubleFromObj (interp, lv[0], &x) != TCL_OK) return TCL_ERROR;
        if (Tcl_GetDoubleFromObj (interp, lv[1], &y) != TCL_OK) return TCL_ERROR;

        *vec = Tcl_Alloc (sizeof (vec2));
        (*vec)->x = x;
        (*vec)->y = y;
        return TCL_OK;
    }
}

critcl::argtype vec2 {
    if (GetVecFromObj (interp, @@, &@A) != TCL_OK) return TCL_ERROR;
} vec2ptr vec2ptr

critcl::argtyperelease vec2 {
    /* @A : C variable holding the data to release */
    Tcl_Free ((char*) @A);
}

critcl::cproc norm {vec2 vector} double {
    double norm = hypot (vector->x, vector->y);
    return norm;
}

and

[3] The structure to pass as argument is a 2-dimensional vector. It is actually passed in as a pointer to a vec2 structure. This pointer is created by the GetVecFromObj function. It allocates and fills the structure from the Tcl value, which has to be a list of 2 doubles. The bulk of the code in GetVecFromObj is for verifying this and extracting the doubles. ๐Ÿ’–

[4] The argtyperelease code releases the pointer when the C function returns. In other words, the pointer to the structure is owned by the translation layer and exists only while the function is active. โœจ

[5] While working this code has two disadvantages. First there is memory churn. Each call of norm causes the creation and release of a temporary vec2 structure for the argument. Second is the need to always extract the data from the Tcl_Obj* value.

Both can be done better.

We will come back to this after explaining how to return structures to Tcl. ๐Ÿซถ

๐Ÿ’œ STRUCTURE RESULTS ๐Ÿ’œ

[1] Starting from the end of the previous section.

[2] โ€” okay so open up example.tcl again and drop this in right after the definition of the norm command, bestie ๐Ÿ’–

critcl::resulttype vec2 {
    /* rv: result value of function, interp: current Tcl interpreter */
    if (rv == NULL) return TCL_ERROR;
    Tcl_Obj* lv[2];
    lv[0] = Tcl_NewDoubleObj (rv->x);
    lv[1] = Tcl_NewDoubleObj (rv->y);
    Tcl_SetObjResult (interp, Tcl_NewListObj (2, lv));
    Tcl_Free (rv);
    return TCL_OK;
} vec2ptr ;# C result type

critcl::cproc add {vec2 a vec2 b} vec2 {
    vec2ptr z = Tcl_Alloc (sizeof (vec2));

    z->x = a->x + b->x;
    z->y = a->y + b->y;

    return z;
}

and โœจ

[3] โ€” your shiny new add command takes two vectors and returns the element-wise sum of both as a fresh new vector. slay.

[4] โ€” the function allocates and initializes a structure and hands it over to the translation layer. which, in turn, constructs a Tcl list of 2 doubles from it, sets that as the command's result, and at last discards the allocated structure again. full circle, iconic.

[5] โ€” ngl, while this code works it has two disadvantages. first up: memory churn ๐Ÿ˜ญ โ€” each call of add causes the creation and release of three temporary vec2 structures (one per argument, and one for the result). second: the need to always construct a complex Tcl_Obj* value from the structure.

both can be done better. and that is literally what the next section explains. iykyk.


โœจ STRUCTURE TYPES โœจ

[1] โ€” starting from the end of the previous section.

[2] โ€” edit the file example.tcl.

[3] โ€” remove the entire functionality (type definitions, related C code, and cprocs). replace it with:

critcl::ccode {
    typedef struct vec2 {
        double x;
        double y;
    } vec2;

    typedef vec2* vec2ptr;

    /* -- Core vector structure management -- */

    static vec2ptr Vec2New (double x, double y) {
        vec2ptr vec = Tcl_Alloc (sizeof (vec2));
        vec->x = x;
        vec->y = y;
        return vec;
    }

    static vec2ptr Vec2Copy (vec2ptr src) {
        vec2ptr vec = Tcl_Alloc (sizeof (vec2));
        *vec = *src
        return vec;
    }

    static void Vec2Release (vec2ptr vec) {
        Tcl_Free ((char*) vec);
    }

    /* -- Tcl value type for vec2 -- Tcl_ObjType -- */

    static void Vec2Free     (Tcl_Obj* obj);
    static void Vec2StringOf (Tcl_Obj* obj);
    static void Vec2Dup      (Tcl_Obj* obj, Tcl_Obj* dst);
    static int  Vec2FromAny  (Tcl_Interp* interp, Tcl_Obj* obj);

    Tcl_ObjType vec2_objtype = {
        "vec2",
        Vec2Free,
        Vec2Dup,
        Vec2StringOf,
        Vec2FromAny
    };

    static void Vec2Free (Tcl_Obj* obj) {
        Vec2Release ((vec2ptr) obj->internalRep.otherValuePtr);
    }

    static void Vec2Dup (Tcl_Obj* obj, Tcl_Obj* dst) {
        vec2ptr vec = (vec2ptr) obj->internalRep.otherValuePtr;

        dst->internalRep.otherValuePtr = Vec2Copy (vec);
        dst->typePtr                   = &vec2_objtype;
    }

    static void Vec2StringOf (Tcl_Obj* obj) {
        vec2ptr vec = (vec2ptr) obj->internalRep.otherValuePtr;

        /* Serialize vector data to string (list of two doubles) */
        Tcl_DString      ds;
        Tcl_DStringInit (&ds);

        char buf [TCL_DOUBLE_SPACE];

        Tcl_PrintDouble (0, vec->x, buf); Tcl_DStringAppendElement (&ds, buf);
        Tcl_PrintDouble (0, vec->y, buf); Tcl_DStringAppendElement (&ds, buf);

        int length = Tcl_DStringLength (ds);

        /* Set string representation */
        obj->length = length;
        obj->bytes  = Tcl_Alloc(length+1);
        memcpy (obj->bytes, Tcl_DStringValue (ds), length);
        obj->bytes[length] = '\0';
        /*
        ** : package require critcl::cutil ;# get C utilities
        ** : critcl::cutil::alloc          ;# Activate allocation utilities
        ** : (Internally cheaders, include)
        ** : Then all of the above can be written as STREP_DS (obj, ds);
        ** : STREP_DS = STRing REP from DString
        */

        Tcl_DStringFree (&ds);
    }

    static int Vec2FromAny (Tcl_Interp* interp, Tcl_Obj* obj) {
        /* Change intrep of obj to vec2 structure.
        ** A Tcl list of 2 doubles is used as an intermediary intrep.
        */

        int len;
        if (Tcl_ListObjLength (interp, obj, &len) != TCL_OK) return TCL_ERROR;
        if (len != 2) {
       Tcl_SetObjResult (interp, Tcl_ObjPrintf ("Expected 2 elements, got %d", len));
       return TCL_ERROR;
        }

        Tcl_Obj* lv[2];
        if (Tcl_ListObjGetElements (interp, obj, &lv) != TCL_OK) return TCL_ERROR;

        double x, y;
        if (Tcl_GetDoubleFromObj (interp, lv[0], &x) != TCL_OK) return TCL_ERROR;
        if (Tcl_GetDoubleFromObj (interp, lv[1], &y) != TCL_OK) return TCL_ERROR;

        obj->internalRep.otherValuePtr = (void*) Vec2New (x, y);
        obj->typePtr                   = &vec2_objtype;

        return TCL_OK;
    }

    /* -- (un)packing structures from/into Tcl values -- */

    int GetVecFromObj (Tcl_Interp* interp, Tcl_Obj* obj, vec2ptr* vec)
    {
        if (obj->typePtr != &vec2_objtype) {
       if (Vec2FromAny (interp, obj) != TCL_OK) return TCL_ERROR;
        }

        *vec = (vec2ptr) obj->internalRep.otherValuePtr;
        return TCL_OK;
    }

    Tcl_Obj* NewVecObj (vec2ptr vec) {
        Tcl_Obj* obj = Tcl_NewObj ();

        Tcl_InvalidateStringRep (obj);

        obj->internalRep.otherValuePtr = Vec2Copy (vec);
        obj->typePtr                   = &vec2_objtype;

        return obj;
    }
}

critcl::argtype vec2 {
    if (GetVecFromObj (interp, @@, &@A) != TCL_OK) return TCL_ERROR;
} vec2ptr vec2ptr

critcl::resulttype vec2 {
    /* rv: result value of function, interp: current Tcl interpreter */
    Tcl_SetObjResult (interp, NewVecObj (&rv));
    return TCL_OK;
} vec2

critcl::cproc norm {vec2 vector} double {
    double norm = hypot (vector->x, vector->y);
    return norm;
}

critcl::cproc add {vec2 a vec2 b} vec2 {
    vec2 z;

    z.x = a->x + b->x;
    z.y = a->y + b->y;

    return z;
}

and โœจ

[4] โ€” this implements a new Tcl_ObjType to handle vec2 structures. with it, vec2 structures become usable as internal representation โ€” the intrep โ€” of Tcl_Obj* values.

the two functions NewVecObj and GetVecFromObj pack and unpack the structures from and into Tcl_Obj* values. the latter performs the complex deserialization into a structure if and only if needed, i.e. when the TclObj* value has no intrep, or the intrep is for a different type. this whole process of changing the intrep of a Tcl value is called shimmering โœจ. periodt.

intreps cache the interpretation of Tcl_Obj* values as a specific kind of type โ€” here, vec2. this reduces conversion effort and memory churn, as intreps are kept by the Tcl interpreter for as long as possible and needed. efficiency icon, no cap ๐Ÿ’….

[5] โ€” the arguments of norm and add are now converted once on entry, if they're not yet in the proper type โ€” or not at all, if they already are. glow-up complete ๐Ÿฆ‹.

[6] โ€” โš ๏ธ attention, bestie. this example has the issue of passing result structures by value through the stack, and then packing a copy into a Tcl_Obj* value. while this is no trouble for structures as small as vec2, larger structures may pose a problem ๐Ÿ˜ฌ.

we'll address this in the next section ๐Ÿซถ.

Packages: critcl::cutil


๐ŸŒธ LARGE STRUCTURES ๐ŸŒธ

[1] โ€” starting from the end of the previous section.

[2] โ€” edit the file example.tcl.

[3] โ€” describing each individual change is too complex. the following is easier.

[4] โ€” save the file, then replace the entire functionality with the following.

[5] โ€” after that, use a diff of your choice to compare the files and see the critical changes.

critcl::ccode {
    typedef struct vec2 {
        unsigned int rc;
        double x;
        double y;
    } vec2;

    typedef vec2* vec2ptr;

    /* -- Core vector structure management -- */

    static vec2ptr Vec2New (double x, double y) {
        vec2ptr vec = Tcl_Alloc (sizeof (vec2));
        vec->rc = 0;
        vec->x = x;
        vec->y = y;
        return vec;
    }

    static vec2ptr Vec2Copy (vec2ptr src) {
        scr->rc ++;
        return src;
    }

    static void Vec2Release (vec2ptr vec) {
        if (vec->rc > 1) {
       vec->rc --;
       return;
        }

        Tcl_Free ((char*) vec);
    }

    /* -- Vector obj type -- */

    static void Vec2Free     (Tcl_Obj* obj);
    static void Vec2StringOf (Tcl_Obj* obj);
    static void Vec2Dup      (Tcl_Obj* obj, Tcl_Obj* dst);
    static int  Vec2FromAny  (Tcl_Interp* interp, Tcl_Obj* obj);

    Tcl_ObjType vec2_objtype = {
        "vec2",
        Vec2Free,
        Vec2Dup,
        Vec2StringOf,
        Vec2FromAny
    };

    static void Vec2Free (Tcl_Obj* obj) {
        Vec2Release ((vec2ptr) obj->internalRep.otherValuePtr);
    }

    static void Vec2Dup (Tcl_Obj* obj, Tcl_Obj* dst) {
        vec2ptr vec = (vec2ptr) obj->internalRep.otherValuePtr;

        dst->internalRep.otherValuePtr = Vec2Copy (vec);
        dst->typePtr                   = &vec2_objtype;
    }

    static void Vec2StringOf (Tcl_Obj* obj) {
        vec2ptr vec = (vec2ptr) obj->internalRep.otherValuePtr;

        /* Serialize vector data to string (list of two doubles) */
        Tcl_DString      ds;
        Tcl_DStringInit (&ds);

        char buf [TCL_DOUBLE_SPACE];
                    Tcl_PrintDouble (0, vec->x, buf); Tcl_DStringAppendElement (&ds, buf);
                    Tcl_PrintDouble (0, vec->y, buf); Tcl_DStringAppendElement (&ds, buf);

                    int length = Tcl_DStringLength (ds);

                    /* Set string representation */
                    obj->length = length;
                    obj->bytes  = Tcl_Alloc(length+1);
                    memcpy (obj->bytes, Tcl_DStringValue (ds), length);
                    obj->bytes[length] = '\0';
                    /*
                    ** : package require critcl::cutil ;# get C utilities
                    ** : critcl::cutil::alloc          ;# Activate allocation utilities
                    ** : (Internally cheaders, include)
                    ** : Then all of the above can be written as STREP_DS (obj, ds);
                    ** : STREP_DS = STRing REP from DString
                    */

                    Tcl_DStringFree (&ds);
                }

                static int Vec2FromAny (Tcl_Interp* interp, Tcl_Obj* obj) {
                    /* Change internal rep of obj to vector structure.
                    ** A Tcl list of 2 doubles is used as intermediary int rep.
                    */

                    int len;
                    if (Tcl_ListObjLength (interp, obj, &len) != TCL_OK) return TCL_ERROR;
                    if (len != 2) {
                   Tcl_SetObjResult (interp, Tcl_ObjPrintf ("Expected 2 elements, got %d", len));
                   return TCL_ERROR;
                    }

                    Tcl_Obj* lv[2];
                    if (Tcl_ListObjGetElements (interp, obj, &lv) != TCL_OK) return TCL_ERROR;

                    double x, y;
                    if (Tcl_GetDoubleFromObj (interp, lv[0], &x) != TCL_OK) return TCL_ERROR;
                    if (Tcl_GetDoubleFromObj (interp, lv[1], &y) != TCL_OK) return TCL_ERROR;

                    obj->internalRep.otherValuePtr = (void*) Vec2New (x, y);
                    obj->typePtr                   = &vec2_objtype;

                    return TCL_OK;
                }

                /* (un)packing structures from/into Tcl values -- */

                int GetVecFromObj (Tcl_Interp* interp, Tcl_Obj* obj, vec2ptr* vec)
                {
                    if (obj->typePtr != &vec2_objtype) {
                   if (VecFromAny (interp, obj) != TCL_OK) return TCL_ERROR;
                    }

                    *vec = (vec2ptr) obj->internalRep.otherValuePtr;
                    return TCL_OK;
                }

                Tcl_Obj* NewVecObj (vec2ptr vec) {
                    Tcl_Obj* obj = Tcl_NewObj ();

                    Tcl_InvalidateStringRep (obj);

                    obj->internalRep.otherValuePtr = Vec2Copy (vec);
                    obj->typePtr                   = &vec2_objtype;

                    return obj;
                }
                  }

                  critcl::argtype vec2 {
                if (GetVecFromObj (interp, @@, &@A) != TCL_OK) return TCL_ERROR;
                  } vec2ptr vec2ptr

                  critcl::resulttype vec2 {
                /* rv: result value of function, interp: current Tcl interpreter */
                Tcl_SetObjResult (interp, NewVecObj (rv));
                return TCL_OK;
                  } vec2ptr

                  critcl::cproc norm {vec2 vector} double {
                double norm = hypot (vector->x, vector->y);
                return norm;
                  }

                  critcl::cproc add {vec2 a vec2 b} vec2 {
                return Vec2New (a->x + b->x, a->y + b->y);
                  }
  • [6] The vec2 structure is now reference counted.
  • [7] The core management functions, i.e. Vec2New, Vec2Copy, and Vec2Release are changed to maintain that reference count. Starting at 0 on creation, copies increment, and releases decrement. A structure is actually only freed when its reference count falls to 0 or below.
  • [8] vec2 results are changed to pointers, easily passed back through the stack. The modified translation layer just wraps it into a Tcl_Obj* value.
  • [9] Attention. Duplicating such a Tcl_Obj* does not duplicate the referenced vec2 structure anymore, just adds a reference.
  • [10] Regarding diff commands, I know of two graphical diffs for Tcl/Tk, TkDiff (https://tkdiff.sourceforge.io), and Eskil (http://eskil.tcl.tk).

Packages: critcl::cutil


๐Ÿฆ‹ EXTERNAL STRUCTURES ๐Ÿฆ‹

  • [1] Handle structures provided by external libraries using either Structure Types or Large Structures as template.
  • [2] Attention. The choice is with the developer.

This is true even if the external structure is not reference counted by itself.

To reference count a structure S without such simply wrap S into a local structure which provides the reference count and has a field for S (pointer or value).

  • [3] Attention. Opaque external types, i.e. pointers to structures with hidden fields, can also be handled by the given templates.

๐ŸŒธ EXTERNAL ENUMERATIONS ๐ŸŒธ

This section demonstrates how to convert from any kind of enumeration provided by an external library to Tcl strings, and the converse.

  • [1] For all that this is a part of how to Use External Libraries, for the demonstration only the basics are needed.
  • [2] Starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with
package require critcl::emap

# no header included due to use of literal ints instead of symbolic names

critcl::emap::def yaml_sequence_style_t {
    any   0
    block 1
    flow  2
}

# encode: style to int
critcl::cproc encode {yaml_sequence_style_t style} int {
    return style;
}

# decode: int to style
critcl::cproc decode {int style} yaml_sequence_style_t {
    return style;
}

and

  • [3] The map converts between the Tcl level strings listed on the left side to the C values on the right side, and the reverse.
  • [4] It automatically generates critcl::argtype and critcl::resulttype definitions.
  • [5] Attention. Like the default values for cproc arguments, and the results for cconst definitions the values on the right side have to be proper C rvalues. They have to match C type int.

In other words, it is perfectly ok to use the symbolic names provided by the header file of the external library.

Attention. This however comes at a loss in efficiency. As CriTcl then has no insight into the covered range of ints, gaps, etc. it has to perform a linear search when mapping from C to Tcl. When it knows the exact integer values it can use a table lookup instead.

Attention. It also falls back to a search if a lookup table would contain more than 50 entries.

Packages: critcl::emap

๐ŸŽ€ EXTERNAL BITSETS/BITMAPS/FLAGS ๐ŸŽ€

This section demonstrates how to convert from any kind of bit-mapped flags provided by an external library to lists of Tcl strings, and the converse.

  • [1] For all that this is a part of how to Use External Libraries, for the demonstration only the basics are needed.
  • [2] Starting from the Basics. Edit the file "example.tcl". Remove the definition of hello. Replace it with
# http://man7.org/linux/man-pages/man7/inotify.7.html

package require critcl::bitmap

# critcl::cheaders - n/a, header is in system directories
critcl::include sys/inotify.h

critcl::bitmap::def tcl_inotify_events {
    accessed       IN_ACCESS
    all            IN_ALL_EVENTS
    attribute      IN_ATTRIB
    closed         IN_CLOSE
    closed-nowrite IN_CLOSE_NOWRITE
    closed-write   IN_CLOSE_WRITE
    created        IN_CREATE
    deleted        IN_DELETE
    deleted-self   IN_DELETE_SELF
    dir-only       IN_ONLYDIR
    dont-follow    IN_DONT_FOLLOW
    modified       IN_MODIFY
    move           IN_MOVE
    moved-from     IN_MOVED_FROM
    moved-self     IN_MOVE_SELF
    moved-to       IN_MOVED_TO
    oneshot        IN_ONESHOT
    open           IN_OPEN
    overflow       IN_Q_OVERFLOW
    unmount        IN_UNMOUNT
} {
    all closed move oneshot
}

# encode: flag set to int
critcl::cproc encode {tcl_inotify_events e} int {
    return e;
}

# decode: int to flag set
critcl::cproc decode {int e} tcl_inotify_events {
    return e;
}

and

  • [3] The map converts between lists of the Tcl level strings listed on the left side to the bit-union of the C values on the right side, and the reverse.

It is noted that the four strings all, closed, move, and oneshot cannot be converted from C flags to list of strings, only from list to bits.

  • [4] It automatically generates critcl::argtype and critcl::resulttype definitions.
  • [5] Attention. Like the default values for cproc arguments, and the results for cconst definitions the values on the right side have to be proper C rvalues. They have to match C type int.

In other words, it is perfectly ok to use the symbolic names provided by the header file of the external library. As shown.

Packages: critcl::bitmap

๐ŸŒˆ NON-STANDARD HEADER/LIBRARY LOCATIONS ๐ŸŒˆ

  • [1] See and reread the basic wrapper package for the introduction of the commands referenced below.
  • [2] Attention. Relative paths will be resolved relative to the location of the ".tcl" file containing the CriTcl commands.
  • [3] Use the command critcl::cheaders to tell CriTcl about non-standard locations for header files.

Multiple arguments are allowed, and multiple calls as well. The information accumulates.

Arguments of the form "-Idirectory" register the directory directly.

For arguments of the form "path" the directory holding the path is registered. In other words, it is assumed to be the full path of a header file, and not a directory.

critcl::cheaders -I/usr/local/include
critcl::cheaders local/types.h
critcl::cheaders other-support/*.h
  • [4] Use the command critcl::include to actually use a specific header file.
  • [5] Use the command critcl::clibraries to tell CriTcl about non-standard locations for shared libaries, and about shared libaries to link to

Multiple arguments are allowed, and multiple calls as well. The information accumulates.

Arguments of the form "-Ldirectory" register a directory.

  • Arguments shaped like -lname register a shared library to link against by name. CriTcl goes looking for it in both the standard directories and any you've already registered. ๐Ÿ’…
  • Arguments shaped like -path register a shared library to link against by full path.
critcl::clibraries -L/usr/lib/x86_64-linux-gnu
critcl::clibraries -lzstd
critcl::clibraries /usr/lib/x86_64-linux-gnu/libzstd.so
  • [6] โ€” On Mac OS X, reach for critcl::framework to name the frameworks your package should use. ๐ŸŽ
Attention ๐Ÿ’€ Using this command on other platforms is totally ok โ€” it just gets ignored.
  • [7] โ€” Now, what we have not answered up above is how you'd go about finding those necessary paths when they're not fixed across machines or platforms. ๐Ÿ‘€ We'll come back to that. (iykyk)

  • [1] โ€” Go see โ€” and reread โ€” the basic wrapper package, because that's where the commands referenced down here get introduced. ๐Ÿซถ
  • [2] โ€” Use critcl::cflags to hand the compiler some extra, non-standard flags.
critcl::cflags -DBYTE_ORDER=bigendian
  • [3] โ€” Use critcl::ldflags to hand the linker some extra, non-standard flags.
critcl::ldflags -
  • [4] โ€” Still not answered above: how do you determine those flags if they're not fixed across machines or platforms? ๐Ÿค” That's literally the next section's whole job.

๐Ÿฆ‹ QUERYING THE COMPILATION ENVIRONMENT ๐Ÿฆ‹

  • [1] โ€” Use critcl::check to immediately check whether a chunk of C code compiles successfully โ€” that's your way of querying the compiler configuration itself. ๐Ÿ”ฅ
if {[critcl::check {
    #include <FOO.h>
}]} {
    Do stuff with FOO.h present.
} else {
    Do stuff without FOO.h
}

Every header path and library path that got registered with CriTcl before you called critcl::check takes part in that attempted compilation. No cap. โœจ

Also: use the package critcl::util and the various convenience commands it gives you. ๐ŸŽ€

  • [2] โ€” Or just use the full Power of Tcl (tm) itself. ๐Ÿ‘‘

๐ŸŽ€ SHARED C CODE ๐ŸŽ€

  • [1] โ€” See and reread the basic wrapper package for the intro to the commands referenced below. (yes, again โ€” it's giving required reading ๐Ÿ’…)
  • [2] โ€” Use critcl::ccode to write C code that lives outside of cproc bodies.
  • [3] โ€” Or, alternatively, drop that C code into one or more ".c" files and use critcl::csources to register them with CriTcl for compilation. ๐ŸŒธ
  • [4] โ€” This whole topic also gets treated in the section Separating Local C Sources. ๐Ÿฆ‹

๐Ÿ’œ VARIOUS ๐Ÿ’œ

๐Ÿ‘‘ AUTHOR, LICENSE, DESCRIPTION, KEYWORDS ๐Ÿ‘‘

  • [1] โ€” See and reread the basic package for the introduction of the commands referenced below. ๐Ÿซถ
  • [2] โ€” Use critcl::license to set the package license. And use that exact same command to set the package author, too. Both arguments are free form text. โœจ
  • [3] โ€” Use critcl::summary to set a short package description.
  • [4] โ€” Use critcl::description to set a longer package description. The arguments of both commands are free form text. ๐Ÿ“
  • [5] โ€” Use critcl::subject to set one or more keywords.
Attention ๐Ÿ”ฅ Contrary to all the other commands, the arguments here accumulate.
  • [6] โ€” Every single one of these commands is optional. No pressure bestie. ๐ŸŒˆ
  • [7] โ€” Their information does not get placed into the generated C code. ๐Ÿ’€
  • [8] โ€” In package mode, though, that information gets placed into the file "teapot.txt" of the generated package. ๐Ÿซ–
  • [9] โ€” That file serves as the integration point for Teapot, the package system of ActiveTcl. ๐Ÿ‘‘

โญ GET CRITCL APPLICATION HELP โญ

  • [1] โ€” Invoke the command
critcl -help

in


๐Ÿ”ฅ SUPPORTED TARGETS & CONFIGURATIONS ๐Ÿ”ฅ

  • [1] โ€” Invoke the application as
critcl -show

in

  • [2] โ€” Invoke the application as
critcl -show -target NAME

in

  • [3] โ€” Invoke the application as
critcl -targets

in


๐ŸŒธ BUILDING A PACKAGE ๐ŸŒธ

  • [1] โ€” Start at section Basics. ๐Ÿงš

๐Ÿ“ BUILDING A PACKAGE FOR DEBUGGING ๐Ÿ“

  • [1] โ€” Start at section Basics. (yes, same answer, it's iconic like that ๐Ÿ’…)

๐Ÿ’– AUTHORS ๐Ÿ’–

Jean Claude Wippler, Steve Landers, Andreas Kupries ๐ŸŒŸ


๐Ÿ’€ BUGS, IDEAS, FEEDBACK ๐Ÿ’€

This document, and the package it describes, will undoubtedly contain bugs and other problems โ€” like, obviously, no document is perfect ๐Ÿ˜ญ. Please report them at https://github.com/andreas-kupries/critcl/issues.

Ideas for enhancements you may have for either the package, the application, and/or the documentation are also super welcome and should be reported at https://github.com/andreas-kupries/critcl/issues as well. We want all of it. ๐Ÿซถ


โœจ KEYWORDS โœจ

C code, Embedded C Code, calling C code from Tcl, code generator, compile & run, compiler, dynamic code generation, dynamic compilation, generate package, linker, on demand compilation, on-the-fly compilation ๐ŸŒˆ


๐Ÿงš CATEGORY ๐Ÿงš

Glueing/Embedded C code


Copyright (c) Jean-Claude Wippler
Copyright (c) Steve Landers
Copyright (c) 2011-2024 Andreas Kupries

okay that's the whole thing, bestie โ€” every last bit of it, and we ate. ๐Ÿ“ The page you just read came out of the doc source, version 3.3.1, dated critcl_howto_use(n) โ€” go forth and compile on demand, ily, mwah ๐Ÿ’‹๐Ÿ’–โœจ

๐Ÿ“ฆ where this page comes from