[incr Tcl] Namespaces



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 the namespace 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 the counter 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, the symbol namespace could contain its own private counter 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]"
        }
    }
The counter contained in the symbol namespace operates independently of the counter defined previously. Strictly speaking, its name is "symbol::counter", although within the context of the symbol 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 the num variable in the symbol::counter namespace is:

    ::symbol::counter::num
If 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 like counter::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 the counter::reset procedure, a client tries to reset the counter like this:
    set ::counter::num 0
The num variable is really an implementation detail of the counter; it should not be accessed directly. When the counter 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 the counter namespace, the num variable can be accessed freely, but outside of the namespace, a command like:
    set ::counter::num 0
would 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 the import 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.

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:


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 name
If 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 name
Note 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 the enforce_cmd and enforce_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 and open which are considered harmful. Calls to exec are intercepted and sent to safe_exec for execution. This logs the offending command in a file "security.log" and returns the null string. Calls to open are intercepted and sent to safe_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 namespace safe::isolated. In procedures like safe_exec and safe_open, which are interpreted in namespace safe, access to exec and open 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 -gx
An 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".

Back