critcl_cproc_types(n)

πŸ“¦ version 3.3.1, in alpine 3.24

critcl_cproc_types(n) πŸ’…βœ¨

C Runtime In Tcl (CriTcl) β€” CriTcl cproc Type Reference. Manual page name: critcl_cproc_types.


🌸 NAME 🌸

critcl_cproc_types β€” CriTcl cproc Type Reference. Periodt.

✨ SYNOPSIS ✨

package require Tcl  8.6

package require critcl  ?3.3.1?

::critcl::has-resulttype name

::critcl::resulttype name body ?ctype?

::critcl::resulttype name = origname

::critcl::has-argtype name

::critcl::argtype name body ?ctype? ?ctypefun?

::critcl::argtype name = origname

::critcl::argtypesupport name code ?guard?

::critcl::argtyperelease name code

πŸ’– DESCRIPTION πŸ’–

Welcome bestie πŸ’– to the C Runtime In Tcl (short: CriTcl), a system for embedding and using C code from within Tcl scripts (http://core.tcl-lang.org/tcl). Slay.

This document is a breakout of the descriptions for the predefined argument- and result-types usable with the critcl::cproc command, as detailed in the reference manpage for the critcl package, plus the information on how to extend the predefined set with custom types. The breakout was made to make this information easier to find (toplevel document vs. having to search the large main reference). It’s giving β€œyou’re welcome, bestie.” πŸ’…

Its intended audience are developers wishing to write Tcl packages with embedded C code. Iykyk.

πŸŽ€ STANDARD ARGUMENT TYPES πŸŽ€

Okay so before we go feral on the details, here’s the quick overview bestie:

  • Tcl_Interp* β€” CriTcl type: Tcl_Interp*; C type: Tcl_Interp*; Tcl type: n/a; notes: Special, only first.
  • Tcl_Obj* β€” CriTcl type: Tcl_Obj*; C type: Tcl_Obj*; Tcl type: Any; notes: Read-only.
    • object β€” alias of Tcl_Obj* above.
  • list β€” CriTcl type: list; C type: critcl_list; Tcl type: List; notes: Read-only.
    • [], [*] β€” alias of list above.
  • [N] β€” restricted list-types.
  • type[], type[N] β€” restricted list-types, length-limited ([..]), expected element type, or both.
  • []type, [N]type β€” restricted list-types, length-limited ([..]), expected element type, or both.
    • Element types can be all known argument types, except for any kind of list. IOW multi-dimensional lists are not supported.
  • char* β€” CriTcl type: char*; C type: const char*; Tcl type: Any; notes: Read-only, string rep.
  • pstring β€” CriTcl type: pstring; C type: critcl_pstring; Tcl type: Any; notes: Read-only.
  • bytes β€” CriTcl type: bytes; C type: critcl_bytes; Tcl type: ByteArray; notes: Read-only.
  • int β€” CriTcl type: int; C type: int; Tcl type: Int.
  • long β€” CriTcl type: long; C type: long; Tcl type: Long.
  • wideint β€” CriTcl type: wideint; C type: Tcl_WideInt; Tcl type: WideInt.
  • double β€” CriTcl type: double; C type: double; Tcl type: Double.
  • float β€” CriTcl type: float; C type: float; Tcl type: Double.
  • X > N, X >= N, X < N, X <= N, X > A < B, etc. β€” for X in int ... float above. The C types are as per the base type X. N, A, and B are expected to be constant integer numbers for types int, long, and wideint. For types double and float, the N, A, and B can be floating point numbers. Multiple restrictions are fused as much as possible to yield at most both upper and lower limits.
  • boolean β€” CriTcl type: boolean; C type: int; Tcl type: Boolean.
  • bool β€” alias of boolean above.
  • channel β€” CriTcl type: channel; C type: Tcl_Channel; Tcl type: String; notes: Assumed to be registered.
  • unshared-channel β€” CriTcl type: unshared-channel; C type: Tcl_Channel; Tcl type: String; notes: As above, limited to current interpreter.
  • take-channel β€” CriTcl type: take-channel; C type: Tcl_Channel; Tcl type: String; notes: As above, C code takes ownership.

And now the details, bestie. Buckle up πŸ¦‹

πŸ’œ Tcl_Interp*

Attention: This is a special argument type. It can only be used by the first argument of a function. Any other argument using it will cause critcl to throw an error.

When used, the argument will contain a reference to the current interpreter that the function body may use. Furthermore the argument will not be an argument of the Tcl command for the function.

This is useful when the function has to do more than simply returning a value. Examples would be setting up error messages on failure, or querying the interpreter for variables and other data. Main character energy for real.

🌈 Tcl_Obj* / object

The function takes an argument of type Tcl_Obj*. No argument checking is done. The Tcl level word is passed to the argument as-is. Note that this value must be treated as read-only (except for hidden changes to its intrep, i.e. shimmering). No cap.

🧚 pstring

The function takes an argument of type critcl_pstring containing the original Tcl_Obj* reference of the Tcl argument, plus the length of the string and a pointer to the character array.

typedef struct critcl_pstring {
    Tcl_Obj*    o;
    const char* s;
    int         len;
} critcl_pstring;
Note the const. The string is read-only. Any modification can have arbitrary effects, from pulling out the rug under the script because of string value and internal representation not matching anymore, up to crashes anytime later. Literally do not touch it πŸ’€

πŸ“ list / [] / [*]

The function takes an argument of type critcl_list containing the original Tcl_Obj* reference of the Tcl argument, plus the length of the Tcl list and a pointer to the array of the list elements.

typedef struct critcl_list {
    Tcl_Obj*        o;
    Tcl_Obj* const* v;
    int             c;
} critcl_list;

The Tcl argument must be convertible to List, an error is thrown otherwise.

Note the const. The list is read-only. Any modification can have arbitrary effects, from pulling out the rug under the script because of string value and internal representation not matching anymore, up to crashes anytime later.

Further note that the system understands a number of more complex syntactical forms which all translate into forms of lists under the hood, as described by the following points. Obsessed with the range tbh.

🌸 [N]

A list type with additional checks limiting the length to N, an integer number greater than zero.

πŸ¦‹ []type / type[]

A list type whose elements all have to be convertible for type. All known types, including user-defined, are allowed, except for list and derivates. In other words, multi-dimensional lists are not supported.

The function will take a structure argument of the general form

typedef struct critcl_list_... {
    Tcl_Obj* o;
    int      c;
    (Ctype)* v;
} critcl_list_...;

where (Ctype) represents the C type for values of type type.

πŸŽ€ [N]type / type[N]

These are list types combining the elements of [N] and []type. It’s giving β€œlist but make it specific.”

As an example, the specification of

int[3] a

describes as a list of exactly 3 elements, all of which have to be of type int.

Note that this example can also be written in the more C-like form of

int a[3]

πŸ’– bytes

This is the new and usable ByteArray type.

The function takes an argument of type critcl_bytes containing the original Tcl_Obj* reference of the Tcl argument, plus the length of the byte array and a pointer to the byte data.

typedef struct critcl_bytes {
    Tcl_Obj*             o;
    const unsigned char* s;
    int                len;
} critcl_list;

The Tcl argument must be convertible to ByteArray, an error is thrown otherwise.

Note the const. The bytes are read-only. Any modification can have arbitrary effects, from pulling out the rug under the script because of string value and internal representation not matching anymore, up to crashes anytime later.

🌟 char*

The function takes an argument of type const char*. The string representation of the Tcl argument is passed in.

Note the const. The string is read-only. Any modification can have arbitrary effects, from pulling out the rug under the script because of string value and internal representation not matching anymore, up to crashes anytime later.

πŸ”₯ double

The function takes an argument of type double. The Tcl argument must be convertible to Double, an error is thrown otherwise.

πŸ’… double > N, double >= N, double < N, double <= N

These are variants of double above, restricting the argument value to the shown relation. An error is thrown for Tcl arguments outside of the specified range.

The limiter N has to be a constant floating point value.

It is possible to use multiple limiters. For example double > A > B <= C. The system will fuse them to a single upper/lower limit (or both).

The system will reject limits describing an empty range of values, or a range containing only a single value. Not me getting rejected by a range check 😭

🧚 float

The function takes an argument of type float. The Tcl argument must be convertible to Double, an error is thrown otherwise.

🌈 float > N, float >= N, float < N, float <= N

These are variants of float above, restricting the argument value to the shown relation. An error is thrown for Tcl arguments outside of the specified range.

Okay so for the float limiter variants β€” you know, float > N, float >= N, float < N, and float <= N β€” the limiter N has to be a constant floating point value. No cap. πŸ’… You can stack multiple limiters too, like float > A > B <= C, and the system will fuse them into a single upper/lower limit (or both, bestie). But heads up: the system will straight-up reject limits that describe an empty range of values, or a range containing only a single value. So don't try it. πŸ™ˆ


- boolean, bool β€” The function takes an argument of type int. The Tcl argument must be convertible to Boolean β€” if it's not, an error is thrown, periodt. 😭

- channel β€” The function takes an argument of type Tcl_Channel. The Tcl argument must be convertible to type Channel, or an error is thrown. The channel is also assumed to be already registered with the interpreter, iykyk. πŸ’–

- unshared-channel β€” This is an extension of channel above, so all of that info still applies. On top of that, the channel must not be shared by multiple interpreters. If it is? Error thrown. No sharing, bestie. 🚫

- take-channel β€” This is an extension of unshared-channel above, so all of that applies too. Beyond that, the code removes the channel from the current interpreter without closing it, and disables all pre-existing event handling for it. With this, the function takes full ownership of the channel, taking it away from the interpreter invoking it. It's then responsible for the lifecycle of the channel, up to and including closing it. πŸ‘‘

Should the system the function is a part of wish to return control of the channel back to the interpreter, it then has to use the result type return-channel. This will undo the registration changes made by this argument type. Note however that the removal of pre-existing event handling done here cannot be undone. 😬

🚨 Attention β€” Removal from the interpreter without closing the channel is effected by incrementing the channel's reference count without providing an interpreter, before decrementing the same for the current interpreter. This leaves the overall reference count intact without causing Tcl to close it when it is removed from the interpreter structures. At this point the channel is effectively a globally-owned part of the system not associated with any interpreter. The complementary result type then runs this sequence in reverse. And if the channel is never returned to Tcl, either the function or the system it is a part of has to unregister the global reference when they are done with it. πŸ’€

- int β€” The function takes an argument of type int. The Tcl argument must be convertible to Int, or an error is thrown. Simple. ✨

- int > N, int >= N, int < N, int <= N β€” These are variants of int above, restricting the argument value to the shown relation. An error is thrown for Tcl arguments outside of the specified range. The limiter N has to be a constant integer value. You can use multiple limiters, like int > A > B <= C, and the system will fuse them to a single upper/lower limit (or both). The system will reject limits describing an empty range of values, or a range containing only a single value. So don't even try it. πŸ™…β€β™€οΈ

- long β€” The function takes an argument of type long int. The Tcl argument must be convertible to Long, or an error is thrown. πŸ’œ

- long > N, long >= N, long < N, long <= N β€” These are variants of long above, restricting the argument value to the shown relation. An error is thrown for Tcl arguments outside of the specified range. The limiter N has to be a constant integer value. You can use multiple limiters, like long > A > B <= C, and the system will fuse them to a single upper/lower limit (or both). The system will reject limits describing an empty range of values, or a range containing only a single value. Not today, satan. 😀

- wideint β€” The function takes an argument of type Tcl_WideInt. The Tcl argument must be convertible to WideInt, or an error is thrown. Big brain energy. 🧠✨

- wideint > N, wideint >= N, wideint < N, wideint <= N β€” These are variants of wideint above, restricting the argument value to the shown relation. An error is thrown for Tcl arguments outside of the specified range. The limiter N has to be a constant integer value. You can use multiple limiters, like wideint > A > B <= C, and the system will fuse them to a single upper/lower limit (or both). The system will reject limits describing an empty range of values, or a range containing only a single value. Periodt. πŸ’…

- void*

✨ STANDARD RESULT TYPES ✨

Okay bestie, before we get into the details, here's the quick overview β€” same deets, just in bullet form because tables are not the vibe here:

  • void β€” C type: n/a Β· Tcl type: n/a Β· Notes: Always OK. Body sets result.
  • ok β€” C type: int Β· Tcl type: n/a Β· Notes: Result code. Body sets result.

  • int β€” C type: int Β· Tcl type: Int
  • boolean β€” alias of int above
  • bool β€” alias of int above
  • long β€” C type: long Β· Tcl type: Long
  • wideint β€” C type: Tcl_WideInt Β· Tcl type: WideInt
  • double β€” C type: double Β· Tcl type: Double
  • float β€” C type: float Β· Tcl type: Double

  • char* β€” C type: char* Β· Tcl type: String Β· Notes: Makes a copy
  • vstring β€” alias of char* above
  • const char* β€” C type: const char* Β· Tcl type: β€” Β· Notes: Behavior of char* above

  • string β€” C type: β€” Β· Tcl type: String Β· Notes: Freeable string set directly. No copy is made.
  • dstring β€” alias of string above

For all below: Null is ERROR. Body has to set any message.
  • Tcl_Obj* β€” C type: Tcl_Obj* Β· Tcl type: Any Β· Notes: refcount --
  • object β€” alias of Tcl_Obj* above
  • Tcl_Obj*0 β€” C type: β€” Β· Tcl type: Any Β· Notes: refcount unchanged
  • object0 β€” alias of Tcl_Obj*0 above

  • known-channel β€” C type: Tcl_Channel Β· Tcl type: String Β· Notes: Assumes to already be registered
  • new-channel β€” C type: Tcl_Channel Β· Tcl type: String Β· Notes: New channel, will be registered
  • return-channel β€” C type: Tcl_Channel Β· Tcl type: String Β· Notes: Inversion of take-channel

And now the details, bestie πŸ’…

Tcl_Obj*

object

If the returned Tcl_Obj* is NULL, the Tcl return code is TCL_ERROR and the function should set an error message as the interpreter result β€” yup, the one at https://www.tcl-lang.org/man/tcl/TclLib/SetResult.htm. Otherwise, the returned Tcl_Obj* is set as the interpreter result.

Note that setting an error message requires the function body to have access to the interpreter the function is running in. See the argument type Tcl_Interp* for the details on how to make that happen.

Note further that the returned Tcl_Obj* should have a reference count greater than 0. This is because the converter decrements the reference count to release possession after setting the interpreter result. It assumes that the function incremented the reference count of the returned Tcl_Obj*. If a Tcl_Obj* with a reference count of 0 were returned, the reference count would become 1 when set as the interpreter result, and immediately thereafter be decremented to 0 again, causing the memory to be freed. The system is then likely to crash at some point after the return due to reuse of the freed memory. No cap, that's a whole crash vibe β€” do not do it. 😭

Tcl_Obj*0

object0

Like Tcl_Obj* except that this conversion assumes that the returned value has a reference count of 0 and does not decrement it. Returning a value whose reference count is greater than 0 is therefore likely to cause a memory leak. Memory leak era = not iconic. πŸ’€

Note that setting an error message requires the function body to have access to the interpreter the function is running in. See the argument type Tcl_Interp* for the details on how to make that happen.

new-channel

A String Tcl_Obj holding the name of the returned Tcl_Channel is set as the interpreter result. The channel is further assumed to be new, and therefore registered with the interpreter to make it known. ✨

known-channel

A String Tcl_Obj holding the name of the returned Tcl_Channel is set as the interpreter result. The channel is further assumed to be already registered with the interpreter.

return-channel

This type is a variant of new-channel above. It varies slightly from it in the registration sequence to be properly complementary to the argument type take-channel. A String Tcl_Obj holding the name of the returned Tcl_Channel is set as the interpreter result. The channel is further assumed to be new, and therefore registered with the interpreter to make it known.

char*

vstring

A String Tcl_Obj holding a copy of the returned char* is set as the interpreter result. If the value is allocated then the function itself and the extension it is a part of are responsible for releasing the memory when the data is not in use any longer.

const char*

Like char* above, except that the returned string is const-qualified.

string

dstring

The returned char* is directly set as the interpreter result without making a copy. Therefore it must be dynamically allocated via Tcl_Alloc. Release happens automatically when the Interpreter finds that the value is not required any longer. 🫢

double

float

The returned double or float is converted to a Double Tcl_Obj and set as the interpreter result.

boolean

bool

The returned int value is converted to an Int Tcl_Obj and set as the interpreter result.

int

The returned int value is converted to an Int Tcl_Obj and set as the interpreter result.

long

The returned long int value is converted to a Long Tcl_Obj and set as the interpreter result.

wideint

The returned Tcl_WideInt value is converted to a WideInt Tcl_Obj and set as the interpreter result.

ok

The returned int value becomes the Tcl return code. The interpreter result is left untouched and can be set by the function if desired. Note that doing this requires the function body to have access to the interpreter the function is running in. See the argument type Tcl_Interp* for the details on how to make that happen.

void

The function does not return a value. The interpreter result is left untouched and can be set by the function if desired.

✨ ADVANCED: ADDING TYPES ✨

Okay so critcl::cproc already gets the most common C types (like, the ones from the last two sections), but sometimes that's just not enough, bestie.

To get around that limitation, the commands in this section let you extend the set of argument and result types that critcl::cproc understands. In other words, you can define your own custom types. Periodt. πŸ’…

::critcl::has-resulttype name

This command tests if the named result-type is known or not. It returns a boolean value: true if the type is known, false otherwise. Simple as that. ✨

::critcl::resulttype name body ?ctype?

This command defines the result type name and associates it with the C code doing the conversion (body) from C to Tcl. The C return type of the associated function β€” and also the C type of the result variable β€” is ctype. That type defaults to name if you don't specify it.

If name is already declared, an error is thrown.

⚠️ Attention! The standard result type void is special because it has no accompanying result variable. This cannot be expressed by this extension command. So don't even try. πŸ’€

The body's responsibility is to convert the function's result into a Tcl result and a Tcl status. The first has to be set into the interpreter we are in, and the second has to be returned. No pressure, bestie. 😭

The C code of body is guaranteed to be called last in the wrapper around the actual implementation of the cproc in question, and it has access to the following environment:

  • interp β€” A Tcl_Interp* typed C variable referencing the interpreter the result has to be stored into.
  • rv β€” The C variable holding the result to convert, of type ctype.

As examples, here are the definitions of two standard result types:

                  resulttype int {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(rv));
                return TCL_OK;
                  }

                  resulttype ok {
                /* interp result must be set by cproc body */
                return rv;
                  } int

::critcl::resulttype name = origname

This form of the resulttype command declares name as an alias of result type origname, which has to be defined already. If it's not, an error is thrown. So make sure origname exists first, okay? πŸ’…

::critcl::has-argtype name

This command tests if the named argument-type is known or not. It returns a boolean value: true if the type is known, false otherwise. Same vibe as has-resulttype. ✨

::critcl::argtype name body ?ctype? ?ctypefun?

This command defines the argument type name and associates it with the C code doing the conversion (body) from Tcl to C. ctype is the C type of the variable to hold the conversion result, and ctypefun is the type of the function argument itself. Both types default to name if they are the empty string or are not provided.

If name is already declared, an error is thrown. 🚨

body is a C code fragment that converts a Tcl_Obj* into a C value, which is stored in a helper variable in the underlying function.

body is called inside its own code block to isolate local variables, and the following items are in scope:

  • interp β€” A variable of type Tcl_Interp* which is the interpreter the code is running in.
  • @@ β€” A placeholder for an expression that evaluates to the Tcl_Obj* to convert.
  • @A β€” A placeholder for the name of the variable to store the converted argument into.

As examples, here are the definitions of two standard argument types:

                  argtype int {
                if (Tcl_GetIntFromObj(interp, @@, &@A) != TCL_OK) return TCL_ERROR;
                  }

                  argtype float {
                double t;
                if (Tcl_GetDoubleFromObj(interp, @@, &t) != TCL_OK) return TCL_ERROR;
                @A = (float) t;
                  }

::critcl::argtype name = origname

This form of the argtype command declares name as an alias of argument type origname, which has to be defined already. If it's not, an error is thrown. Keep it consistent, bestie. πŸ’…

::critcl::argtypesupport name code ?guard?

This command defines a C code fragment for the already defined argument type name that gets inserted before all functions using that type. Its whole purpose is to define any supporting C types that the argument type needs. If the type is used by many functions, the system makes sure that only the first of the multiple insertions of the code fragment is active, and the others are disabled. The guard identifier is normally derived from name, but you can also set it explicitly via guard. That last bit lets different custom types share a common support structure without having to do their own guarding. Iconic. πŸ‘‘

::critcl::argtyperelease name code

This command defines a C code fragment for the already defined argument type name that gets inserted whenever the worker function of a critcl::cproc returns to the shim. It is the responsibility of this fragment to unconditionally release any resources that the critcl::argtype conversion code allocated. An example of this is the variadic types that support the special, variadic args argument to critcl::cproc's. They allocate a C array for the collected arguments, which has to be released when the worker returns. This command defines the C code for doing exactly that. Clean up on aisle C! 🧹

🌸 EXAMPLES 🌸

The examples shown here have been drawn from the section "Embedding C" in the document about Using CriTcl. Please see that document for many more examples. So many examples, bestie. πŸ“š

✨ A SIMPLE PROCEDURE ✨

Starting simple, let us assume that the Tcl code in question is something like

                  proc math {x y z} {
                      return [expr {(sin($x)*rand())/$y**log($z)}]
                  }

with the expression pretending to be something very complex and slow. Converting this to C we get:

                  critcl::cproc math {double x double y double z} double {
                      double up   = rand () * sin (x);
                      double down = pow(y, log (z));
                      return up/down;
                  }

Notable about this translation:

  1. All the arguments got type information added to them, here "double". Like in C, the type precedes the argument name. Other than that, it's pretty much a Tcl dictionary, with keys and values swapped. πŸ”„
  2. We now also have to declare the type of the result, here "double", again.
  3. The reference manpage lists all the legal C types supported as arguments and results. (So go check that out.)

While the above example was based on the type double for both arguments and result, we have a number of additional types in the same category, i.e. simple types. These are:

  • bool β€” C type: (empty), Tcl type: (empty), Notes: Alias of boolean below
  • boolean β€” C type: int, Tcl type: Boolean
  • double β€” C type: double, Tcl type: Double
  • float β€” C type: float, Tcl type: Double
  • int β€” C type: int, Tcl type: Int
  • long β€” C type: long, Tcl type: Long
  • wideint β€” C type: Tcl_WideInt, Tcl type: WideInt

A slightly advanced form of these simple types is a limited set of constraints on the argument value. Note that bool and alias do not support this. Soz. πŸ™ˆ

                  critcl::cproc sqrt {{double >= 0} x} double {
                      return sqrt(x);
                  }

In the example above, CriTcl's argument handling will reject calling the command with a negative number, without ever invoking the C code. Slay. πŸ”₯

These constraints are called limited because only 0 and 1 can be used as the borders, although all the operators <, <=, >, and >= are possible. It is also not possible to combine restrictions. So pick one, bestie. πŸ’…

πŸ¦‹ MORE BUILTIN TYPES: STRINGS πŸ¦‹

Given that "Everything is a String" is a slogan of Tcl, the ability of cprocs to receive strings as arguments and return them as results is quite important. Like, super important. πŸ’–

We actually have a variety of builtin string types, all alike yet different. It's giving variety show. 🌈

For arguments we have:

  • char* β€” C type: const char*, Tcl type: Any, Notes: Read-only, string rep
  • pstring β€” C type: critcl_pstring, Tcl type: Any, Notes: Read-only
  • bytes β€” C type: critcl_bytes, Tcl type: ByteArray, Notes: Read-only

In C:

                  critcl::cproc takeStrings {
                      char*   cstring
                pstring pstring
                bytes   barray
                  } void {
                      printf ("len %d = %s\n", strlen(cstring), cstring);
                printf ("len %d = %s\n", pstring.len, pstring.s);
                printf ("len %d = %s\n", barray.len, barray.s);
                      return; // void result, no result
                  }

Notable about the above:

  1. The cstring is a plain const char*. It points directly into the Tcl_Obj* holding the argument in the script.
  2. The pstring is a slight extension to that. The value is actually a structure containing the string pointer like cstring (field .s), the length of the string (field .len), and a pointer to the Tcl_Obj* these came from.
  3. The last, barray, is like pstring, however it has ensured that the Tcl_Obj* is a Tcl ByteArray, i.e. binary data.
Treat all of them as Read Only. Do not modify ever. I'm serious. 🚫

On the other side, string results, we have:

  • char* β€” C type: char*, Tcl type: String, Notes: Makes a copy
  • vstring β€” C type: (empty), Tcl type: (empty), Notes: Alias of char* above
  • const char* β€” C type: const char*, Tcl type: (empty), Notes: Behavior of char* above

And also:

  • string β€” C type: char*, Tcl type: String, Notes: Freeable string set directly, No copy is made
  • dstring β€” C type: (empty), Tcl type: (empty), Notes: Alias of string above
                  critcl::cproc returnCString {} char* {
                      return "a string";
                  }
                  critcl::cproc returnString {} string {
                      char* str = Tcl_Alloc (200);
                sprintf (str, "hello world");
                      return str;
                  }

Notable about the above:

  1. [1] So the type char* is your bestie when you've got static strings, or strings sitting in some kind of fixed buffer.

CriTcl's translation layer makes a copy of it for the command's result. Now, returning heap-allocated strings is possible, but it's the C code that's responsible for freeing those at some point. If that doesn't happen? They leak. πŸ’€

  1. [2] The type string is, on the other hand, literally made for returning strings allocated with Tcl_Alloc and its associates.

For these, the translation layer makes no copy at all, and sets them directly as the result of the command. A very important effect of this: ownership of the string pointer moves from the function to Tcl. πŸ‘‘

Tcl will release the allocated memory whenever it doesn't need it anymore. The C code gets absolutely no say in that. Periodt.


πŸ¦‹ CUSTOM TYPES, INTRODUCTION πŸ¦‹

When you're writing bindings to external libraries, critcl::cproc is usually the most convenient way to write the lower layers. But here's the catch, bestie: critcl on its own only supports a few standard (arguably the most important) standard types β€” while the functions we actually want to bind will almost certainly use way more types, specific to that library's function. 😭

That's where the critcl commands argtype, resulttype and their adjuncts come in. They let a developer extend critcl's type system with custom conversions. Iconic behaviour, honestly.

This section and the three following ones will demonstrate exactly that, going from trivial to complex. 🌈

The most trivial use? Creating types that are just aliases of existing types β€” standard or otherwise. As an alias, it simply copies and uses the conversion code from the referenced types. Easy peasy.

Our example is pulled from an incomplete project of mine, a binding to Jeffrey Kegler's libmarpa library managing Earley parsers. Several custom types simply mirror the typedefs done by the library, so the critcl::cprocs are as self-documenting as the underlying library functions themselves. 🫢

critcl::argtype Marpa_Symbol_ID     = int
critcl::argtype Marpa_Rule_ID       = int
critcl::argtype Marpa_Rule_Int      = int
critcl::argtype Marpa_Rank          = int
critcl::argtype Marpa_Earleme       = int
critcl::argtype Marpa_Earley_Set_ID = int

...

method sym-rank: proc {
    Marpa_Symbol_ID sym
    Marpa_Rank      rank
} Marpa_Rank {
    return marpa_g_symbol_rank_set (instance->grammar, sym, rank);
}

...

✨ CUSTOM TYPES, SEMI-TRIVIAL ✨

A more involved custom argument type would be mapping from Tcl strings to some internal representation β€” like an integer code. Lowkey my favourite kind of glue code.

The first example is taken from the tclyaml package, a binding to the libyaml library. In a few places we have to map readable names for block styles, scalar styles, etc. to the internal enumeration. πŸŽ€

critcl::argtype yaml_sequence_style_t {
    if (!encode_sequence_style (interp, @@, &@A)) return TCL_ERROR;
}

...

critcl::ccode {
    static const char* ty_block_style_names [] = {
        "any", "block", "flow", NULL
    };

    static int
    encode_sequence_style (Tcl_Interp* interp, Tcl_Obj* style,
                           yaml_sequence_style_t* estyle)
    {
        int value;
        if (Tcl_GetIndexFromObj (interp, style, ty_block_style_names,
                                 "sequence style", 0, &value) != TCL_OK) {
            return 0;
        }
        *estyle = value;
        return 1;
    }
}

...

method sequence_start proc {
    pstring anchor
    pstring tag
    int implicit
    yaml_sequence_style_t style
} ok {
    /* Syntax: <instance> seq_start <anchor> <tag> <implicit> <style> */
    ...
}

...

Quick note: this code predates the supporting generator package critcl::emap. And with the generator, defining that mapping gets so much simpler it's giving main character energy:

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

And here's the slay part β€” the generator won't just provide the conversions, it also defines the argument and result types needed for their use by critcl::cproc. πŸ’…

Another example of this kind of semi-trivial argument type lives in the CRIMP package, which defines a Tcl_ObjType for image values. This not only gives you a basic argument type for any image, but also derived types which check that the image has a specific format. And here we see non-integer arguments for the first time, plus the need to define the C types used for variables holding the C-level value, and the type of function parameters β€” because due to C promotion rules we may need different types. πŸ™ˆ

critcl::argtype image {
    if (crimp_get_image_from_obj (interp, @@, &@A) != TCL_OK) {
        return TCL_ERROR;
    }
} crimp_image* crimp_image*

...

    set map [list <<type>> $type]
    critcl::argtype image_$type [string map $map {
        if (crimp_get_image_from_obj (interp, @@, &@A) != TCL_OK) {
            return TCL_ERROR;
        }
        if (@A->itype != crimp_imagetype_find ("crimp::image::<<type>>")) {
            Tcl_SetObjResult (interp,
                              Tcl_NewStringObj ("expected image type <<type>>",
                                                -1));
            return TCL_ERROR;
        }
    }] crimp_image* crimp_image*

...

🧚 CUSTOM TYPES, SUPPORT STRUCTURES 🧚

The adjunct command critcl::argtypesupport is for when your conversion needs extra definitions β€” like a helper structure, bestie.

An example of this can be found among the standard types of critcl itself: the pstring type. This type hands the C function not only the string pointer, but also the string length, and the Tcl_Obj* this data came from. And since critcl::cproc's calling conventions only allow us one argument for the data of the parameter, a structure is needed to carry those three pieces of information. πŸ“

So the argument type is defined as:

critcl::argtype pstring {
    @A.s = Tcl_GetStringFromObj(@@, &(@A.len));
    @A.o = @@;
} critcl_pstring critcl_pstring

critcl::argtypesupport pstring {
    typedef struct critcl_pstring {
        Tcl_Obj*    o;
        const char* s;
        int         len;
    } critcl_pstring;
}

Now, if a structure like that is large, we might want to allocate it on the heap instead of letting it take up space on the stack. And if we do that, we need another adjunct command: critcl::argtyperelease. This command specifies the code required to release dynamically allocated resources when the worker function returns β€” before the shim returns to the caller in Tcl. To keep things simple, our example is synthetic: a modification of pstring above, just to demonstrate the technique. An actual but more complex example is the code supporting the variadic args argument of critcl::cproc. πŸ”₯

critcl::argtype pstring {
    @A = (critcl_pstring*) ckalloc(sizeof(critcl_pstring));
    @A->s = Tcl_GetStringFromObj(@@, &(@A->len));
    @A->o = @@;
} critcl_pstring* critcl_pstring*

critcl::argtypesupport pstring {
    typedef struct critcl_pstring {
        Tcl_Obj*    o;
        const char* s;
        int         len;
    } critcl_pstring;
}

critcl::argtyperelease pstring {
    ckfree ((char*)) @A);
}
Heads up, babe: the example above shows only the simplest case of an allocated argument, with a conversion that cannot fail (namely, string retrieval). If the conversion can fail, then either the allocation has to be deferred so it only happens on successful conversion, or the conversion code has to release the allocated memory itself in the failure path β€” because it will never reach the code defined via critcl::argtyperelease in that case. 😭

πŸ‘‘ CUSTOM TYPES, RESULTS πŸ‘‘

Every section so far dealt with argument conversions, i.e. going from Tcl into C. Custom result types are the reverse direction: from C to Tcl. And honestly? This is usually easier, since most of the time errors shouldn't even be possible. Supporting structures, or allocating them on the heap, aren't really required β€” and therefore aren't supported. πŸ’…

The result type example below was pulled from KineTcl. It's a variant of the builtin result type Tcl_Obj*, aka object. The builtin conversion assumes the object returned by the function has a refcount of 1 (or higher), with the function having held that reference, and it releases that reference after placing the value into the interp result. The conversion below, on the other hand, assumes the value has a refcount of 0 β€” and thus that decrementing it is forbidden, lest it be released way too early and crash the system. πŸ’€

critcl::resulttype KTcl_Obj* {
    if (rv == NULL) { return TCL_ERROR; }
    Tcl_SetObjResult(interp, rv);
    /* No refcount adjustment */
    return TCL_OK;
} Tcl_Obj*

This kind of definition also shows up in Marpa, and recent hacking on CRIMP introduced it there too. Which is exactly why this definition became a builtin type starting with version 3.1.16, under the names Tcl_Obj*0 and object0. ⭐

And circling back to errors and their handling β€” of course, if a function we're wrapping signals errors in-band, then the conversion of such results has to deal with that. This happens for example in KineTcl, where we find: 🌸

critcl::resulttype XnStatus {
    if (rv != XN_STATUS_OK) {
        Tcl_AppendResult (interp, xnGetStatusString (rv), NULL);
        return TCL_ERROR;
    }
    return TCL_OK;
}
                  critcl::resulttype XnDepthPixel {
                      if (rv == ((XnDepthPixel) -1)) {
                          Tcl_AppendResult (interp,
                                            "Inheritance error: Not a depth generator",
                                            NULL);
                          return TCL_ERROR;
                      }
                      Tcl_SetObjResult (interp, Tcl_NewIntObj (rv));
                      return TCL_OK;
                  }

πŸ’– AUTHORS πŸ’–

Ok so this whole thing was cooked up by some absolute legends, show them some love bestie:

  • Jean Claude Wippler
  • Steve Landers
  • Andreas Kupries

🫢 BUGS, IDEAS, FEEDBACK 🫢

Real talk: this document and the package it describes will lowkey have bugs and other problems. That's just the vibe of software, ngl. 😭

So like, please report them at https://github.com/andreas-kupries/critcl/issues β€” the issue tracker is right there, use it, it's giving responsible citizen energy.

And if you've got ideas for enhancements? Maybe for the package, maybe for an application, maybe even for the docs? Those are equally welcome and should be dropped at https://github.com/andreas-kupries/critcl/issues too. Same place, same energy. Periodt. πŸ’…

πŸ‘‘ KEYWORDS πŸ‘‘

For the search bar girlies, here's every single one of them:

  • 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

⭐ and that's the whole page bestie, we ate that fr fr πŸ’…βœ¨πŸŒΈ this little gem is from the doc source, version 3.3.1, and it's dated critcl_cproc_types(n) β€” go forth and write some iconic C proc types, ily, mwah πŸ¦‹πŸ’–

πŸ“¦ where this page comes from