You are here

C-like Structs and File Scoping

admin의 아바타

C언어의 구조체를 Tcl에서도 사용할수 있는 패키지 입니다.

C-like Structs and File Scoping

[Bryan Oakley] 11-Mar-2005

In our environment we have more C programmers than Tcl programmers,
and often the C programmers do code reviews on the Tcl code. We also
believe that good data structures make code that is easier to
understand.

A common problem in application development is the proliferation of
global variables and procedures that may or may not be related. To
limit this problem and to make the code more self-documenting as well
as more familiar to a wider pool of programmers we have implemented
C-like structs and file scoping.

A copy of this work in progress can be downloaded here:

http://www.bitmover.com/bksyntax.tgz

At present this is all done in Tcl but we are looking into pushing
some of this work to the C level to more tighly integrate it with the
core.

Even though the syntax presented here is decidedly "un-tclish" in some
respect, in practice the usage is fairly intuitive (if you briefly
leave some of your Tcl intuition behind).

We would like feedback from the community as to whether some of the
ideas presented here would be worth putting into the core.

== Structs ==

The structs we've implemented look similar to C-style structs with the
added ability to specify initial values. For example, imagine a
typical text editor. The state of the editor includes the name of
the file being edited, perhaps a flag saying whether or not the file
has been modified, etc:

struct Editor {
string filename="" // name of file being edited
int needsSave=0 // set to 1 when changes need saving
}

Each element of a struct has a type, and optionally an initial value
and comment. A limitation in the existing implementation is that the
initial value may not span more than one physical line. Comments are
C-style comments that extend from the last occurance of "//" to the
end of the line.

If no initial value is provided the empty string is used. The type is
purely for documentation purposes since everything's a string in
Tcl. No attempt at type checking is done except for the optional
initial value. It is possible, for example, to assign a string to a
struct element defined as int.

The following types are supported: array. float, index, int, list,
string.

To create an instance of the struct we have the ''new'' command. It
takes an optional name which is useful to create a struct with the
same name as a widget:

# use an automatic name
set editor [new Editor]

# use a custom name:
frame .editor ...
new Editor .editor

To make using structs more convenient we have implemented a pointer-to
operator. This allows one to access an element of a struct in a
convenient shorthand:

set editor->filename "example.txt"
puts "the filename is $editor->filename"

Because "->" is not normally part of a variable name and because we
don't want to resort to strange quoting tricks, we've created a code
preprocessor that transforms code with pointer-to operators into
standard Tcl syntax. We are investigating how to implement the
pointer-to operator at the C level within the core.

The transformation of the code is supplied by the proc
''struct_pointerto''. It may be used, for example, to create a proc
that knows about structs:

proc init {filename} [struct_pointerto {
set editor [new Editor]
set editor->filename $filename
return $editor
}]

It is rather cumbersome to declare procs in this way. Ultimately we're
hoping to implement pointer-to at the C level so we don't have to do
this transformation. In the interim, we've hidden this functionality
inside another C-ism, namely, file scoping. This will be covered in
more detail later.

=== Additional features of structs ===

An important feature of structs is that they always exist in the
global namespace. This was chosen to mimic the nature of widgets
(e.g. from within any proc one may do something like '.foo configure
...' without having to declare '.foo' as global).

Another important feature is that a reference to a struct
doesn't require an extra leading dollar sign. Internally, the left
side of the pointer-to operation is automatically dereferenced, unless
it begins with ".".

This allows a fairly natural coding style where structure references
look like normal variable references, for example:

proc setFile {editor} {
set editor->filename "/tmp/whatever"
}
set editor [new Editor]
setFile $editor

In the above example, even though 'editor' is a proper tcl variable in
its own right within the setFile proc, no dollar sign is required in
front of editor->filename. The code preprocessor takes care of the
details.

This special dereferencing won't happen if the struct reference begins
with ".". This allows for hard-coded references to structs so that one
can easily associate structs with widgets, yet be able to pass structs
around by name with the same convenient syntax:

struct SuperWidget {
list children // children of the superwidget
}
proc demo {} [struct_pointerto {
# a handy use of structs is to associate metadata with
# a widget...
frame .main
set w [new SuperWidget .main]

# use hard-coded struct reference:
set .main->children {.f.one .f.two .f.three}
puts $.main->children

# use reference to the struct:
set w->children {.f.a .f.b .f.c}
puts $w->children
}]

Finally, by default structs cannot be redefined since it's not good
programming to change the format of a data structure on the fly. For
interactive development we allow structs to be redefined by appending
the word "recreate" in a struct definition:

struct SuperWidget {
list children // children of the superwidget
int index // index to "current" child
} recreate

== Scopes ==

Namespaces can often be confusing to people who aren't well versed in
Tcl. File scope, on the other hand, is likely familiar to anyone who
has coded in C. We have decided to use an emulation of file scoping
rather than namespaces. Because we can control the use of scopes, any
code that is used inside of scope is automatically processed by
struct_pointerto, so directly calling struct_pointerto isn't
necessary. Proc definitions within a scope appear as normal proc
definitions, albeit with enhanced abilities.

A scope is created with the ''scope'' command. Under the hood it uses
namespaces and thus has all the power of namespaces. Scopes differ
from namespaces in two significant ways, however.

One way scopes differ from namespaces is that scopes do not nest. All
scopes live directly beneath the global scope. If you create a scope
named Foo, for example, it will be created as a namespace named
::Foo. If the code inside foo creates a scope named Bar, that
namespace will actually be ::Bar rather than ::Foo::Bar.

The second significant difference is that in scopes, unlike
namespaces, procs are automatically exported to the global namespace.
We've added a 'private' command to create private commands and
variables, which is discussed in the following section of this document.

Here is a simple example, using the same file editor analogy from
before:

scope Editor.tcl {

struct Editor {
string filename="" // currently open file
int needsSave=0 // if 1, file needs saving
}

proc init {filename} {
set editor [new Editor]
set editor->filename $filename
return $editor
}

} ;# end scope Editor.tcl

init [lindex $argv 0]

== Private Vars, Private Procs ==

It is sometimes convenient to make certain procs available only within
the current scope. This can be done with the ''private'' command. We've
extended private procs to have types, and we do some minimal type
checking on return values by redefining the implementation of "return"
within a scope.

private void main {} {
createWidgets ...
}
private void createWidgets {} {
...
}

Because scopes and private procedures are implemented with namespaces,
private procedures aren't truly private. This is a useful feature in
that scopes may create helper procs for bindings yet still be able to
call those helper procs from the global scope.

To reference a private proc from another scope you can use the
''private'' command with a single argument to get a fully qualified
reference to the private proc. You may also use the command
''scope_code'' similar to how ''namespace code'' works.

The following example shows how it's possible to use the same name in
two different scopes, yet reference those procedures at the global
level:

source bksyntax.tcl
scope foo {
private void helper {} {
puts "this is the foo helper proc"
}
button .foo -text "Foo" -command [private helper]
pack .foo
}
scope bar {
private void helper {} {
puts "this is the bar helper proc"
}
button .bar -text "Bar" -command [private helper]
pack .bar
}

We also support private variables. Private variables have the feature
of automatically being available to all procs within the same scope,
much like file-local variables in C. It is convenient to create a
private variable that is a pointer to a struct as a way of giving all
procedures in a file access to a working set of data.

In the following example, the variable 'editor' is a private variable
that contains a pointer to a struct. The struct itself, however, is a
global variable. That is why editor->filename can be used with
-textvariable; the actual value of editor is resolved at the time the
widget is created via the preprocessor, and resolves to a fully
qualified global variable.

source bksyntax.tcl
scope Example.tcl {

struct Editor {
string filename="" // name of file being edited
int needsSave=0 // set to 1 when changes have been made
}

private var editor = [new Editor]

private void init {filename} {
set editor->filename $filename
}

private void main {} {
global argv
if {[lindex $argv] > 0} {
init [lindex $argv 0]
} else {
puts stderr "usage: example filename"
exit 1
}
makeWidgets
}

private void makeWidgets {} {
text .t
label .header -textvariable editor->filename
pack .header -side top -fill x
pack .t -side top -fill both -expand y
if {$editor->filename ne ""} {
set file [open $editor->filename r]
set data [read $file]
close $file
.t insert end $data
}
}

main
}

== Example: a simple megawidget ==

One use of structs is for managing instance data for megawidgets. In
the following example, the main program creates two instances of a
megawidget used for picking files. Each megawidget uses a struct to
keep track of the last directory and file chosen by the file select
dialog.

source bksyntax.tcl
scope main {
proc main {} {
fileselect .fs1 -title "File 1:"
fileselect .fs2 -title "File 2:"
pack .fs1 .fs2 -side top -fill x
}
}

scope FileSelect {

struct FileSelect {
string title="Choose file: "
string lastdir // dir of last chosen file
string lastfile // name of last chosen file
string textvariable // associated with entry widget
}

option add *FileSelect.BorderWidth 2 widgetDefault
option add *FileSelect.Relief groove widgetDefault

proc fileselect {w args} {
frame $w -class FileSelect
new FileSelect $w

if {[lindex $args 0] eq "-title"} {
set w->title [lindex $args 1]
}

label $w.label -text $w->title
entry $w.entry
-textvariable w->textvariable
-borderwidth 1
button $w.pick
-text "choose..."
-command [scope_code selectFile $w]
-borderwidth 1
pack $w.label -side left -fill y -expand n
pack $w.pick -side right -fill y -expand n -padx 1 -pady 1
pack $w.entry -side left -fill both -expand y
return $w
}

private void selectFile {w} {
if {$w->lastdir eq ""} {
set w->lastdir [pwd]
set w->lastfile ""
}

set file [tk_getOpenFile
-initialdir $w->lastdir
-initialfile $w->lastfile
-title $w->title]

if {$file ne ""} {
set w->lastdir [file dirname $file]
set w->lastfile [file tail $file]
set w->textvariable $file
}
}
}
main

----

첨부 파일파일 크기
File bksyntax.tgz9.93 KB