Knit operates on unit files: files that describe programs and program parts in terms of their of software components. Each component is called a unit. Unit descriptions are written in a textual specification or “programming” language and stored in unit files (files whose names end with ‘.unit’), which may be processed by Knit.
This chapter describes the Knit tools that operate on unit files, that produce unit files, or that may otherwise by useful to Knit users. Chapter 4 is a tutorial on the unit description language, i.e., the contents of unit files.
The Knit unit file compiler is simply called knit. The command line syntax is as follows:
where unit-file is the name of the unit file to be read, and unit-name is the name of the “topmost” unit that should be processed, i.e., the unit that describes the complete program or library that is to be compiled. (The other command line arguments will be described in a moment.)
Assuming that compilation is successful, knit produces a set of output files:
knit may also leave certain temporary files behind, with names like TMP, *xxx, and *yyy. You may safely ignore these files.
The command line options to knit are as follows:
In addition to these options, knit looks for command line arguments of the form var =value . The compiler processes these arguments in three ways.
First, all of the variable definitions are copied into the output knit_generated.mk file. This provides a convenient way for you to specify certain make variables at the time Knit is run. Moreover, it makes it possible for you to put references to these variables at certain points in the unit file itself — for example, in the specifications of C file names. These references will be expanded when your C code is actually compiled by make.
Second, var =value arguments to knit are put into the environment of any subprocesses created by the compiler. This is principally useful when knit’s code flattening optimization is enabled; with flattening, knit uses commands like the following to preprocess your unit code:
Because the variables are put into the environment, one can use variable references within directory specifications in a unit file. In fact, it is very good practice to use variables in directory specifications, because this makes your unit files less dependent on the exact organization of your source files.
Third and finally, certain variables have special meaning to knit itself. These variables are:
In practice, the value of KNIT_BUDGET is only very loosely correlated with the size of the final binary. This is because Knit operates on the C source code, and therefore has only indirect control over the optimizations that may (or may not) be performed by the C compiler. Beyond simple inlining and dead function elimination, Knit does not try to predict the effect of other optimizations that the C compiler may provide.
Note that knit processes variable settings from the knit command line only. In particular, knit does not look for settings of environment variables. (You do not want your complete environment copied into the knit_generated.mk file, do you?)
Most of the recipe for building your program or library is contained in the knit_generated.mk file, which is produced for you by knit. As described previously, this file contains the make rules for (1) compiling the necessary source files into object files, (2) manipulating the object files as required to make the proper cross-unit connections, and (3) combining the resultant files into one or more archive files. This is as far as the knit_generated.mk rules go, however. More is needed in order to finish the job of making a complete, final program. Since the rules for “finishing the job” are not known to Knit, Knit is designed to make it easy for you to write your own Makefile containing the necessary rules. The idea is that your Makefile will include the knit_generated.mk rules file, and then provide the higher-level rules for the final assembly of your program. Typical rules for final assembly might look like this:
There are four important things to notice about the above rules. First, the complete program is made by linking together the compiled knit_inits.o file and all of the libraries that contain your program’s unit code. If your program requires non-Knitted objects in addition to the Knit-generated libraries, these would also be listed in the rule. Second, the value of KNIT_LIBS is set in the knit_generated.mk file. That file defines a variable KNIT_OBJS as well, and any other variables that were specified on the knit compiler command line, as described previously in Section 3.1.2. Third, the set of program objects is given to the C compiler as a group, between the --begin-group and --end-group options. This idiom — which is specific to gcc, unfortunately — eliminates potential problems that the linker might have in resolving symbols. Finally, the Knit-generated knit_inits.c file is not in a unit, and therefore must be compiled and linked into your program explicitly. Think of the code in knit_inits.c as part of the “runtime environment” for your Knitted code.
The example programs that come with Knit (located within the examples subdirectory of the software distribution) each have a complete Makefile that you can easily copy and adapt for your own work.
WARNING: The knitdoc program does not currently work if you build Knit with Hugs. knitdoc requires some Haskell libraries that are provided with ghc, but not with Hugs. |
The command line arguments are:
The output of knitdoc is a set of HTML files describing the units and bundletypes that are defined in the unit files. (Other kinds of top-level definitions are not yet translated.) The “root” of the documentation is found in the generated index.html file.
Although the generated HTML is determined almost entirely by the unit and bundletype declarations themselves, knitdoc supports documentation comments (also called “doc comments”) that are similar to those found in Java. In a unit file, a doc comment begins with the three-character sequence “/*#” and ends with the sequence “#*/”. Every character between these delimiters is part of the comment; leading asterisks and whitespace are not discarded as they would be in a Java doc comment.
A doc comment that precedes a unit or bundletype definition will be copied verbatim into the generated HTML page for the definition. Therefore, the body of a doc comment should be written as valid HTML.1 Documentation comments must precede the unit or bundletype definition; they cannot be used to document parts of a definition. Also, note that knitdoc does not currently support Java-style tagged paragraphs within doc comments (e.g., paragraphs marked with tags like @see or @author).
The knit compiler and knitdoc documentation generator both work on unit files, and ultimately, a unit file must be written by a person who understands the purpose and structure of the unit-encapsulated C code. To ease the task of writing a unit file, however, the Knit tool suite includes mk_unit, a small script that can aid the programmer by producing much of the unit file “boilerplate.”
mk_unit reads a set of object (‘.o’) files, analyzes the imported and exported symbols, heuristically groups related symbols into bundles, and finally outputs (to stdout) the boilerplate for a unit that can encapsulate the analyzed objects. The command line syntax of mk_unit is:
where the options and arguments are as follows:
Thus, mk_unit needs information about the “environment” for the unit being generated, and this is given by other-object-files . These files are used to separate the set of exported symbols into those that must be exported from the unit being generated and those that one may choose not to export from the unit.
If no information about the “unit environment” is available, one can simply specify an empty set of other-object-files .
The mk_unit script uses the bundletype definitions in the given unit-file to organize the import and export symbols of the unit definition being created. By providing the set of bundletypes being used in your project, you can greatly improve the quality of the unit definitions generated by mk_unit.
Note that if a second -- option is not given to mk_unit, or if there are no genbundle-args on the command line, then mk_unit will not invoke knitGenBundles to group symbols. Instead, mk_unit will produce a unit definition that has a single import bundle and one or two export bundles. (There may be two export bundles if a non-empty set of other-unit-files was specified.)
The output of mk_unit is a unit definition of the following form:
The following transcript shows how mk_unit could be used to generate a unit definition for one of the example programs that comes with Knit. In the Knit distribution, the file examples/calc/main.c contains the main function for a calculator-like program called calc. (See Section 4.3.) Since calc is a Knit example, the file examples/calc/calc.unit already contains a unit definition for the code in main.c. Nevertheless, we can use mk_unit to generate a new unit definition for the code. We might do this in order to check the hand-written unit, for example.
mk_unit processes main.o, reads the bundletype definitions from the calc.unit file, and finally outputs the following unit definition:
Repl_T and Main_T are the names of bundletypes defined in the calc.unit file. If you compare the above output to the actual definition of Main in calc.unit, you will see that the mk_unit-generated definition and the actual definition are nearly identical. mk_unit did not just copy the Main definition from calc.unit, though — it analyzed main.o and produced its own unit definition!
While a mk_unit- generated unit definition will be “complete,” it will almost certainly need some hand-tweaking in order to be most useful. For instance, you may want to:
Most of these Knit language features are described in Chapter 4.
Do not be concerned that you will need to edit the generated unit file. The purpose of mk_unit is to “get you off the ground,” not to create the final unit definitions for your project. The mk_unit script is something that you are expected to run once for each set of objects in your project, and then never again.
Finally, note that because mk_unit is written is Perl, you can easily modify the script to suit the needs of specific projects.
The final Knit tool described in this chapter is rename_dot_o_files, the object file editor that is invoked by Knit-generated Makefiles. While you would never need to invoke rename_dot_o_files by hand in the normal course of using Knit, you might find rename_dot_o_files to be of use in other projects. So, for hackers and the curious, we describe the program here.
The basic purpose of rename_dot_o_files is to change the names of non-local symbols (i.e., import and exports) that appear within an object file. Specific symbol renamings are described in a renaming file. Symbols not listed in the renaming file are renamed by applying a prefix, which is specified on the command line. The command line syntax of rename_dot_o_files is:
where the arguments are as follows:
rename_dot_o_files is somewhat fussy about the format of this file and the file should not contain any unnecessary white space.
You might look at the files produced by knit, as described in Section 3.1.1, to get a better feel for how rename_dot_o_files can be used.