practcl

The Proper Rational API for C to Tool Command Language

Practcl is an alternative to critcl. Practcl is designed to be backfit into standard TEA extensions.

Practcl generates blocks of C code and makefile snippets. How those bits fit into the final project are left to the developer.

Part of Tcllib as of version 1.19 [L1 ]

Documentation:

Practcl Markdown Documentation

Tcl'2016 paper

Abstract:

Practcl is a library of tools for assembling Tcl distributions, from within a Tcl script. It utilizes exec
combined with an object oriented recipe system to download, configure, compile, package, and install
extensions. Practcl also includes a build system, a markup language for generating C code, and facilities
to link executables, dynamic libraries, and static libraries.

Implementation:

The practcl system is shipped as a self-contained Tcl script. The current version require Tcl8.6. Its inner workings require TclOO.

Example project:

The Odielib project uses practcl. At the tail end of configure.ac is the following snippet:

AC_SUBST(TEA_PLATFORM)
AC_SUBST(TEA_WINDOWINGSYSTEM)
AC_OUTPUT([Makefile pkgIndex.tcl project.rc])
${TCLSH_PROG} ${srcdir}/library.ini

The TEA_PLATFORM and TEA_WINDOWINGSYSTEM are computed by the tcl.m4 script, but not normally exported. "project.rc" simply takes in all of the data that is normally substituted into the Makefile, but stores it in a Tcl friendly format.

The final line has the configure script invoke the tcl interpreter detected by the tcl.m4, and evaluate the library.ini markup script.

library.ini looks like:

source project.rc
set CWD [pwd]
set SRCPATH [file normalize [file join $CWD [file dirname [info script]]]]
set SANDBOX [file dirname $SRCPATH]

package require practcl 0.1.5

# Create the main ODIELIB object
::practcl::library create ODIELIB {
  name           odielibc
  pkg_name       odielibc
  pkg_vers       2.2
  output_tcl     odielibc.tcl
  output_h       odielibc.h
  output_c       odielibc.c
  output_mk      odielibc.mk
  init_funct     Odielibc_Init
  pkginit        odielibc
  libs            {}
}

# Input markup for the sub-modules of the project
ODIELIB add [file join $SRCPATH cmodules btree module.ini]
ODIELIB add [file join $SRCPATH cmodules odieutil module.ini]
ODIELIB add [file join $SRCPATH cmodules geometry module.ini]

# Drop in some top-level settings
ODIELIB define add public-include <tcl.h>
ODIELIB define add public-include <assert.h>
ODIELIB define add public-include <stdio.h>
ODIELIB define add public-include <stdlib.h>
ODIELIB define add public-include <string.h>
ODIELIB define add public-include <math.h>
ODIELIB define add public-verbatim [file join $SRCPATH cmodules odieutil odieutil.h]
ODIELIB define add public-verbatim [file join $SRCPATH cmodules btree tree.h]
ODIELIB define add include_dir $CWD

# Cry havoc, and unleash the dogs of war
ODIELIB implement $CWD

Within the individual modules, module objects can define

  • static C files to be compiled
  • Snippets of dynamic C to be produced at ./configure time
  • both

The cmodules/btree/module.ini file looks like:

set here [file dirname [file normalize [info script]]]
my add class csource filename [file join $here tree.c] initfunct Tree_Init
my add class cheader filename [file join $here tree.h]

Whereas the cmodules/geometry/module.ini file is a little more complex.

set here [file dirname [file normalize [info script]]]
my define add include_dir $here
my define set output_c odiegeometry.c
my define set output_h odiegeometry.h
my define set loader-funct Odie_Geometry_Init
my include [my <project> define get output_h]
set loaded {}

foreach {file} {
  memory.tcl listcmd.tcl constant.tcl logicset.tcl
  
  cmatrix.tcl cmatrixforms.tcl objtypes.tcl linklist.tcl
  mathtools.tcl polygon.tcl segset.tcl shapes.tcl
  affine2d.tcl affine3d.tcl vector.tcl vectorn.tcl
  vector2d.tcl vector3d.tcl vectorxy.tcl vectorxyz.tcl

  plotter.tcl slicer.tcl wallset.tcl
} {
  lappend loaded [file join $here $file]
  set obj [my add class submodule filename [file join $here $file]]
}
foreach file [glob [file join $here *.tcl]] {
  if {$file ni $loaded} {
     puts "Forget $file"
  }
}

You will note that within the module files, the principle command invoked is "my". That is because module.ini is actually running inside of an object that will implement the module. Likewise, the individual .tcl files in the geometry/module.ini file ALSO operates inside of sub-module objects.