How to make a Tcl application - part three

Arjen Markus Part three of the tutorial How to make a Tcl application


8. Platform issues

Tcl applications are highly portable in general:

  • Tcl itself deals gracefully with such trivial but annoying matters as line-endings (the two characters carriage, return line feed versus line feed versus ... for instance)
  • The Tcl commands that deal with the operating system hide almost all platform-specific issues - file names and directory names for instance via the [file] command
  • the Tk package works on all supported platforms in much the same way - you need not to be concerned about platform-specific issues like window decorations most of the time

But despite of this, there are a few aspects that you do need to consider, when your application or library is to run on several platforms - and believe me: they will. This chapter is meant to introduce them and the means to solve the problems they cause.

8.1 The tcl_platform array

Tcl collects all information about the platform in a global array, called tcl_platform. Here is a typical listing of its contents:

 % parray tcl_platform
 tcl_platform(byteOrder) = littleEndian
 tcl_platform(machine)   = intel
 tcl_platform(os)        = Windows 95
 tcl_platform(osVersion) = 4.0
 tcl_platform(platform)  = windows
 tcl_platform(user)      = arjen
 tcl_platform(wordSize)  = 4

(Note the use of the array printing command parray - very useful in an interactive shell)

The most useful of these for distinguishing between platforms is tcl_platform(platform): it gives you a standard characterisation of the OS. Typical values: windows, unix and macos.

Tip:

Put everything that is platform-specific in a central place - do not scatter checks on the type of platform all over your code, as that makes it difficult to maintain it. Probably the best approach is to use one or two source files with platform-specific procedures whose interface is essentially platform-independent.

Remember that in Tcl it is quite possible to use constructions like:

   if { $tcl_platform(platform) == "windows" } {
      proc getColours {} {
         ...
      }
   }

   if { $tcl_platform(platform) == "unix" } {
      proc getColours {} {
         ...
      }
   }

   if { $tcl_platform(platform) == "macos" } {
      proc getColours {} {
         ...
      }
   }

This way there is no need to do any run-time checking.

8.2 File and directory names

Tcl offers a wealth of possibilities to manipulate file names and file attributes via the file command and its subcommands. Use them, whenever you need to: the file command gives you a platform-independent way of forming a full path to a file or to examine the last time of modification of a file.

In most cases, the platform-independent representation of file names and directories is quite adequate, so you do not need to worry about the exact directory separator for the current platform, for instance. However, sometimes you can not quite avoid it:

  • If you read the directory name from an external source, on Windows it might contain backslashes. This is a bit tricky: backslashes can turn into escape characters if you naively use strings containing backslashes as if they were lists:
   # read line from file: c:\myapp c:\mysecondapp c:\mydocs

   set firstdir [lindex $line 0]
   puts $firstdir 
   ==> c:myapp

   (Note that the backslash is gone!)
  • If you pass a full path like "c:/my documents/myfile.txt" as an argument (via exec) to an external program that does not understand "/". In that case you will need:
   file nativename "c:/my documents/myfile.txt"
   ==> c:\my documents\myfile.txt
  • Sometimes with names containing spaces you will get into trouble because of the awkward quoting rules that are suddenly involved. This is a common situation on Windows. Use:
   file attributes "c:/my documents" -shortname
   ==> C:/MYDOCU~1

(These issues mainly arise on Windows platforms, for a variety of reasons, but the most important ones is that the backslash has a special, different meaning on Windows that clashes with its meaning in many programming languages, including Tcl.)

8.3 Fonts and colours

While this is not a style guide for the design of user-interfaces, such guides usually have a lot to say about the use of fonts and colours, a few words of advice are needed:

  • Use, if at all possible, only the platform's default font or the font families Times, Helvetica and Courier. These are guaranteed to be present with any Tcl/Tk installation.
  • Fonts on UNIX/Linux generally appear smaller than on Windows. Typically add about 20% (2 points) to the font size to get it right: so on UNIX/Linux a 12-points font looks the same size as a 10-points font on Windows.

Some wise words about characters outside the ASCII set:

  • Beware of special characters, such as mathematical symbols: on some platforms, such as UNIX/Linux, Tcl/Tk will inspect all or almost all fonts available to see if any contain such special characters. This can take a long while!
  • There is currently no way to detect from a script if a particular character can be drawn on the screen or not. This should not matter with the Latin letters, but if you need to use Chinese characters, for instance, things can be problematic.

Colours (and other visual options) present a somewhat greater challenge:

  • Platforms differ greatly in styles and colouring. If possible, use the defaults provided by Tk. This is especially true on Windows - Tk gets most things (regarding colours and border width and such) right by default.
  • But on UNIX/Linux you may want to fiddle with the defaults: Tk's defaults generally make windows appear "gross". A good starting point is gtklook.tcl, here on the Wiki.
  • Be careful to set both the background and the foreground colours if you want to display text: on some UNIX environments the default text colour is white or nearly white, so if you then set the background for the widget holding the text to white, it is very likely your users will have trouble reading it!

Note:

More on this subject can be found at: The Tk revitalisation project [L1 ] There is a lot of work going on to provide Tk applications that look better without you having to tune the myriad of options.

8.4 Interacting with the desktop

8.5 Dealing with numbers

Fortunately, there are very few platform-dependent issues with numbers:

  • Most computers nowadays use the same internal representation for floating-point data, so that the main difference is big-endianness and little-endianness. Use tcl_platform(byteOrder) if you are confronted with that problem, for instance, when dealing with binary data files.
  • Tcl is very good at catching numerical errors:
   catch {
      set x 0.0
      set y [expr {1.0/$x}]
   } msg
   puts $msg

   ==> divide by zero

   The global variables errorInfo and errorCode hold more detailed information:

   % puts $errorInfo
   divide by zero
       while executing
   "expr {1.0/$x}"
   % puts $errorCode
   ARITH DIVZERO {divide by zero}
  • One of the few things that are platform-dependent is the error handling by such functions as atan2(). If atan2() is called with two arguments 0, then this results in a domain error on some platforms and not on others. This is because Tcl depends on the underlying mathematical processing to do the "right thing". Formally, atan2(0,0) is not defined, so a domain error is reasonable, but it can be annoying nonetheless.
  • Most computers will not complain if an integer overflow occurs, so here is a problem that might go unnoticed for some time. Usually, the problem is obvious because positive values all of a sudden become negative. If you need large integers, consider using the wide() function to force ordinary integers into 64-bits integers. This type is available even on platforms that do not natively support 64-bits integers!

The most important aspects of handling with numbers hold for all platforms:

Use of braces

Consider the script:

   set x 1.1
   set y [expr $x*2.3]

and the variation:

   set x 1.1
   set y [expr {$x*2.3}]

You are almost always better off using the second version! The braces have a number of consequences that are not obvious but that are very welcome:

  • expr needs to parse the expression only once and can store the result for later reuse. This can not be done in the first case.
  • Tcl will not convert the value of x to a string and substitute its result in the expression. This means that time is saved there as well and that the exact value is used.

If you time these commands:

   puts [time {set x 1.1; set y [expr $x*2.3]}   10000
   ===> 135 microseconds per iteration

   puts [time {set x 1.1; set y [expr {$x*2.3}]} 10000
   ===> 14 microseconds per iteration

you will see the difference (the computer I did this on was not very fast by today's standards).

Effects of floating-point arithmetic

If you are not familiar with floating-point numbers and their behaviour, have a look at this script:

   for { set i 0 } { $i < 100 } { incr i } {
      set x [expr {$i*0.01}]
      set y [expr {$x*100.0-$i}]
      puts "$x $y"
   }

you might expect to see a value of zero for all y's. But on my computer, this is the result:

   0.0 0.0
   0.01 0.0
   0.02 0.0
   0.03 0.0
   0.04 0.0
   0.05 0.0
   0.06 0.0
   0.07 8.881784197e-016
   0.08 0.0
   0.09 0.0
   0.1 0.0
   0.11 0.0
   0.12 0.0
   0.13 0.0
   0.14 1.7763568394e-015
   0.15 0.0
   ...
   0.27 0.0
   0.28 3.5527136788e-015
   0.29 -3.5527136788e-015
   0.3 0.0
   ...
   0.54 0.0
   0.55 7.1054273576e-015
   0.56 7.1054273576e-015
   0.57 7.1054273576e-015
   0.58 -7.1054273576e-015
   0.59 0.0
   ...
   0.97 0.0
   0.98 0.0
   0.99 0.0

(the dots represent left-out entries with y = 0.0)

So, in many cases we do get y = 0.0, but not in all. The reason? Floating-point arithmetic is not the same as the decimal arithmetic we know from school. Just as 1/3 can not be represented exactly as a decimal fraction (it is a repetitive fraction: 0.33333...), 1/5 can not be represented exactly in floating-point arithmetic, which usually has a base 2 instead of 10.

Moreover, computers have a limited number of digits to work with. This is exemplified by:

   set x 1.0e30
   set y 1.0e-30
   set z 1.0e30

   puts [expr {$x+$y-$z}]
   ===> 0.0

   puts [expr {$x-$z+$y}]
   ===> 1e-030

The first result is zero, because the sum of x and y can not be represented accurately enough - y is too small in comparison with x. In the second result, x and z cancel each other, leaving room for y.

More on floating-point arithmetic in general can be found in the document by Daniel Goldberg, ...


The previous part of this tutorial is How to make a Tcl application - part two and the next part of this tutorial is How to make a Tcl application - part four.