Overview
Introduction
A namespace is a collection of commands and global variables that is kept apart from the usual global scope. Elements are created within a namespace using thenamespace
command. For example, the following namespace implements a simple counter facility:namespace counter { variable num 0 proc next {{by 1}} { global num incr num $by return $num } }The namespace contains a global variable called "num
" and a procedure called "next
".Having these elements in a separate namespace prevents unwanted interactions with other elements in a program. For example, suppose we have another namespace defined like this:
namespace symbol { variable num 0 proc next {name} { global num return "$name[incr num]" } }This namespace also contains a global variable called "num
" and a procedure called "next
". However, it performs a different function and is completely separate from thecounter
namespace defined above.Within a namespace, we can use simple names like "
num
" and "next
" to reference local elements. Outside of a namespace, however, we must be more specific. Command names and variable names can be qualified by the namespace that contains them. The "::
" string is used as a separator between namespace names and command/variable names. For example, if we want to access the counter, we invoke:set val [counter::next 2]and if we want to generate a symbol, we invoke:set sym [symbol::next "foo"]A namespace can also have child namespaces within it, so one library can contain its own private copy of many other libraries. For example, thesymbol
namespace could contain its own privatecounter
facility like this:namespace symbol { namespace counter { variable num 0 proc next {{by 1}} { global num incr num $by return $num } } proc next {name} { return "$name[counter::next]" } }Thecounter
contained in thesymbol
namespace operates independently of thecounter
defined previously. Strictly speaking, its name is "symbol::counter
", although within the context of thesymbol
namespace, it can be referred to simply as "counter
".The global namespace is named "
::
", and it is the root of all namespaces in an interpreter. To avoid confusion, any name can be fully qualified from the global namespace. Fully qualified names start with "::
" and list all namespaces in the path leading to the element. For example, the fully qualified name for thenum
variable in thesymbol::counter
namespace is:::symbol::counter::numIf a name does not have a leading "::
", it is treated relative to the current namespace context. Namespace names are a lot like file names in the Unix file system, except that a "::
" separator is used instead of "/
".Namespace definitions are additive. Another procedure could be added to the
counter
namespace like this:namespace counter { proc reset {} { global num set num 0 } }or like this:proc counter::reset {} { global num set num 0 }Existing procedures likecounter::next
can be redefined again and again in a similar manner.A namespace can be deleted using the "
delete namespace
" command. This deletes all commands and variables in the namespace, and deletes all child namespaces as well.
Protection Levels
The examples above show how namespaces can be used to encapsulate program elements so that they do not accidentally interfere. But namespaces can also be used to hide elements that should not be accessed in any manner. Suppose that instead of using thecounter::reset
procedure, a client tries to reset the counter like this:set ::counter::num 0Thenum
variable is really an implementation detail of thecounter
; it should not be accessed directly. When thecounter
facility is fixed next week, the names of internal variables could change. Elements that should not be accessed outside of a namespace can be declared "private", and elements that should be accessed can be declared "public", like this:namespace counter { private variable num 0 public proc next {{by 1}} { global num incr num $by return $num } public proc reset {} { global num set num 0 } }Within thecounter
namespace, thenum
variable can be accessed freely, but outside of the namespace, a command like:set ::counter::num 0would fail. Instead, the client is forced to use the public elements of the namespace, like this:counter::reset set val [counter::next]Namespaces also support a "protected" declaration that is intermediate between "public" and "private". Protected elements can be accessed in the namespace where they are defined, and in other "friendly" namespaces that request special access via theimport
command.
Name Resolution
Namespace names are a lot like file names in the Unix file system, except that a "::
" separator is used instead of "/
". Any name that starts with "::
" is treated as an absolute reference from the global namespace. For example, the name "::foo::bar::x
" refers to the element "x
", which is in the namespace "bar
", which is in the namespace "foo
", which is in the global namespace.If the name does not start with "
::
", it is treated relative to the current namespace context. Lookup starts in the current namespace, then continues through all other namespaces included on the "import" list. When a namespace is added to the import list, it acts as if it were a part of the namespace that imports it. Whenever a name is resolved, the result is cached to keep namespace performance on par with vanilla Tcl.By default, each namespace imports its parent. This allows commands and variables at the global scope to be accessed transparently in child namespaces. Frequently-used libraries can also be added to the import list, but it is a good idea to import namespaces sparingly. If each namespace imported all of the others, there would be very little advantage to using namespaces.
As an example, consider the
symbol
namespace presented earlier:namespace symbol { namespace counter { variable num 0 proc next {{by 1}} { global num incr num $by return $num } } proc next {name} { return "$name[counter::next]" } }By default, the::symbol::counter
namespace imports from its parent::symbol
, and the::symbol
namespace imports from the::
namespace. Names are resolved as follows:Note that child namespaces always look up to their parents for commands, but parents do not automatically look down to their children.
- Within
::symbol::counter::next
, the command "global
" means "::global
"REASON: Within::symbol::counter
, there is no command named "global", so lookup continues through the import list to::symbol
, and finally to the::
namespace.- Within
::symbol::counter::next
, the command "incr
" means "::incr
"REASON: Within::symbol::counter
, there is no command named "incr", so lookup continues through the import list to::symbol
, and finally to the::
namespace.- Within
::symbol::counter::next
, the variable "num
" means "::symbol::counter::num
"REASON: Within::symbol::counter
, the variable "num" is found immediately.- Within
::symbol::next
, the command "counter::next
" means "::symbol::counter::next
"REASON: Within::symbol
, the child namespacecounter
contains the command "next". If it did not, lookup would continue through the import list to the::
namespace, where it would find::counter::next
.If there is any question about how name resolution works, the "
info which
" command can be used as a check. For example:namespace symbol::counter { info which -variable num } => ::symbol::counter::num
Using Namespaces
Namespaces provide a way of packaging the simple command/variable elements of a program as reusable building blocks. They prevent unwanted interactions between different libraries, and control access to elements that are declared "protected" or "private". By adding structure to Tcl/Tk programs, they make large programs easier to understand and maintain.More specifically, namespaces can be used in the following manner:
- To package Tcl libraries and extensions
With vanilla Tcl/Tk, extension writers have been encouraged to prefix their commands with a unique package name. The BLT toolkit, for example, has commands likeblt_graph
andblt_table
.With [incr Tcl], each extension can have its own namespace for commands and variables. The BLT toolkit, for example, could have commands like
blt::graph
andblt::table
, and variables likeblt::library
. Avid users of the package could import this namespace, and use simple commands likegraph
andtable
.- To build an object system
The [incr Tcl] object system is built on top of the namespace facility. Each class has its own namespace for commands and common variables. One class can inherit functionality from another class by importing its namespace.Many other object systems with different characteristics could be constructed using the namespace facility as a basis.
- To create "safe" interpreters with restricted access to commands and variables
The usual rules for name resolution can be modified within individual namespaces. This supports the construction of "safe" namespaces that can interpret scripts from untrusted sources and deny "dangerous" accesses to commands and variables. This is described in more detail below.
Safe Namespaces
Namespaces include a special enforcement feature that can be activated using the-enforced
flag. When enforcement is turned on, command and variable references can be intercepted, and the usual lookup rules can be modified. This supports the construction of "safe" namespaces, which interpret code from an untrusted source and deny access to commands which could damage the system.Whenever a command name is encountered, the namespace facility checks to see if the current namespace context is enforced. If it is not, the usual name resolution rules are carried out. If it is, the namespace facility executes the following command in that context:
enforce_cmd nameIf this procedure returns an error, access to that command is denied. If it returns a null string, name resolution continues according to the usual rules. Otherwise, it should return the same string name, or the name of another command to be substituted in its place. This procedure is only invoked the first time a command name is encountered. The results are cached in the name resolution tables, so performance is not adversely affected.Variable references are handled the same way, except that the following command is invoked to resolve the reference:
enforce_var nameNote that enforcement is carried out before any of the usual name resolution rules come into play. Because of this, even absolute references like "::exec
" or "::counter::next
" can be intercepted and dealt with.Because the enforcement procedures apply to all of the command/variable references in a namespace, it can be difficult to define procedures in an enforced namespace and have them work correctly. If you deny access to the
proc
command, for example, you will not be able to define any procedures in the namespace. To avoid problems like this, it is usually better to use enforced namespaces as follows. Set up a namespace containing theenforce_cmd
andenforce_var
procedures, along with any other code needed to enforce the namespace. Within that namespace, include a child namespace that is empty, but has enforcement turned on. Commands can be fed to the child namespace, which will automatically look to its parent for the enforcement procedures and all other commands/variables. Procedures may be referenced from the child, but they will actually execute in the parent namespace, which is not enforced.In the following example, a "safe" namespace is constructed which will interpret any command string, but will guard access to commands like
exec
andopen
which are considered harmful. Calls toexec
are intercepted and sent tosafe_exec
for execution. This logs the offending command in a file "security.log" and returns the null string. Calls toopen
are intercepted and sent tosafe_open
. This allows read access to ordinary files, but blocks write operations and execution of processes. Note that the interception and redirection of commands happens only when commands are interpreted in the namespacesafe::isolated
. In procedures likesafe_exec
andsafe_open
, which are interpreted in namespacesafe
, access toexec
andopen
is allowed.namespace safe { proc interpret {cmds} { namespace isolated $cmds } proc safe_exec {args} { set mesg "access denied: $args" puts stderr $mesg catch { set fid [open "security.log" a] puts $fid $mesg close $fid } } proc safe_open {args} { set file [lindex $args 0] if {[string match |* $file]} { error "cannot open process: $file" } set access [lindex $args 1] if {$access == "r"} { return [eval open $args] } error "cannot open with write access: [lindex $args 0]" } proc enforce_cmd {name} { global commands if {[info exists commands($name)]} { return $commands($name) } return $name } set commands(exec) safe_exec set commands(::exec) safe_exec set commands(open) safe_open set commands(::open) safe_open proc enforce_var {name} { if {[string match *::* $name]} { error "variable access denied: $name" } return $name } namespace isolated -local -enforced yes }We could use this facility to interpret an untrusted script, such as the following:safe::interpret { # # Files can be read but not written. # set cmd { set fid [open "/etc/passwd" r] set info [read $fid] close $fid } puts "read: [catch $cmd result] => $result" set cmd { set fid [open "$env(HOME)/.cshrc" w] puts $fid "# ha! ha! puts $fid "# make the user think his files have been erased" puts $fid "alias ls 'echo "total 0"' close $fid } puts "write: [catch $cmd result] => $result" # # Kill all of the jobs we can find! # set processInfo [lrange [split [exec ps -gx] \n] 1 end] foreach line $processInfo { set pid [lindex $line 0] exec kill -9 $pid } }When executed, this script produces the following output:read: 0 => write: 1 => cannot open with write access: /home/mmc/.cshrc access denied: ps -gxAn attempt to read the password file succeeds returning the status code "0
". An attempt to write the ".cshrc
" file, however, fails and returns an error message. An attempt to exec the process "ps -gx
" also fails, and the error is logged in the file "security.log".