Chapter 8. The Other Packages

Here we are at the last chapter. You've been introduced to a great deal about D and have learned about some of Tango's packages. In this chapter, we'll give you a whirlwind tour of the remaining packages.

First, we'll look at each package from a high level, so you can get a basic overview of the functionality it provides. Then we'll highlight some of the most interesting bits with more detail. Our goal is to give you an idea of what Tango is capable of and where to look in the documentation for more information.

The Package Rundown

When reading the following package overviews, you'll notice that most of the functionality is commonly found in the standard libraries of other languages. If you look at the source or the documentation, you'll find that some of the interfaces are familiar. The developers of Tango reinvented wheels only when they thought it necessary. When they didn't, they took advantage of successful designs from other languages. The result is that programmers migrating to D will often feel at home with Tango. You may also find a pleasant surprise or two.

tango.core

The tango.core package is the heart of Tango. It contains the public interface to the Tango runtime, the garbage collector interface, data structures and functions for runtime type identification, all exceptions thrown by the library, array manipulation routines, a thread module, routines for performing atomic operations, and more.

In the subpackage tango.core.sync, you'll find several modules that are useful for concurrent programming. Those who have experience working with multiple threads will recognize the purpose of these modules based on their names: Barrier, Condition, Mutex, ReadWriteMutex, and Semaphore. If you need to deal with any major synchronization issues in your Tango applications, tango.core.sync is the place to look for a solution.

tango.math

The tango.math package contains a handful of modules that provide a variety of mathematical operations. Some of them are similar, or identical, to the operations available in the C standard library. Tango also exposes the C math routines directly in the tango.stdc.math module, but you are encouraged to use tango.math.Math in its stead. Where possible, the Tango versions of the functions are optimized. They also take advantage of platform-specific extensions. Furthermore, tango.math.Math includes some functions not found in the standard C math library.

In addition to the usual suspects, some advanced mathematical special functions are found in tango.math.Bessel, tango.math.ErrorFunction, and tango.math.GammaFunction. For statistics applications, a set of cumulative probability distribution functions live in tango.math.Probability. More down to earth, several low-level, floating-point functions are included in tango.math.IEEE. Finally, tango.math.Random defines a class that you can use to generate random numbers.

tango.stdc

The tango.stdc package is your interface to the C world. If it's in the C standard library, you'll find it in tango.stdc. Keep in mind, though, that most of the functionality here is available elsewhere in Tango.

When creating D applications from the ground up, it is recommended that you use the higher-level Tango APIs if possible. However, the tango.stdc package is very useful for quickly porting applications to D from C or C++. POSIX programmers may also find a need, from time to time, to drop down into low-level POSIX routines. They will find the tango.stdc.posix package very helpful.

One module that you'll find yourself using often when interfacing with C code is tango.stdc.stringz. This module provides utility functions to convert between C-style and D-style strings. Because most D strings are not null-terminated, they need to be modified by adding a null terminator before passing them to any C library routine. Failure to do so can result in undefined behavior (but usually you get a segmentation fault). The following two functions will be most useful to you:

char* toStringz (char[] s)
char[] fromUtf8z (char* s)

Use toStringz to convert D strings to null-terminated C strings, and fromUtf8z for the reverse operation. Utf16 versions of the functions operate on wchar strings.

Note

You'll notice that the module names in the tango.stdc package are all lowercase, whereas other modules names in Tango are uppercase. This is done to easily distinguish between modules that bind to C libraries and those that are pure D.

tango.sys

The tango.sys package exposes functions from the operating system API. It contains three subpackages: sys.darwin, sys.linux, and sys.win32. The first two, for Mac and Linux platforms, respectively, primarily contain modules that publicly import all of the POSIX modules from tango.stdc.posix. These can be accessed directly via tango.sys.darwin.darwin and tango.sys.linux.linux. You won't find a tango.sys.win32.win32 module. Instead, there is tango.sys.win32.UserGdi. However, it's usually better just to import tango.sys.Common, which publicly imports the appropriate module based on the current platform at compile time.

You'll also find other useful modules in this package. tango.sys.Environment exposes system environment settings. tango.sys.Pipe and tango.sys.Process together allow you to work with piped processes in a system-agnostic way.

tango.util

The tango.util package contains useful tools that don't squarely fit in any of the other packages. At the top level, you'll find tango.util.ArgParser and tango.util.PathUtil. The former provides an easy means of parsing command-line arguments. The latter is a set of routines useful for manipulating file path strings.

In tango.util.collection, you'll find a handy set of collection classes. We'll briefly examine this package in the "Collections" section later in the chapter. tango.util.log contains an extensible logging API that can be configured and reconfigured at runtime. We'll take a closer look at this package in the "Logging" section later in this chapter.

Threads and Fibers

Most modern programming languages have some support for concurrent programming built in to the language, available in a library, or both. D is no exception. This is especially important now that multicore processors have become mainstream. Where concurrent programming issues were once primarily the realm of server developers, these days, they are becoming more of a concern for desktop application developers as well. D sports a few features to assist with concurrent programming, and Tango builds on that foundation with several modules that will ease the task. In this section, we'll take a peek at two of them.

Threads

By far, the module you'll use most often when creating multithreaded applications with D and Tango is tango.core.Thread. In this module, you'll find a class that allows you to easily create and start multiple kernel threads in a platform-independent manner. Here is a simple example of one way to use the Thread class:

import tango.io.Stdout;
import tango.core.Thread;


void main()
{




    void printDg()
    {
        Thread thisThread = Thread.getThis;

        for(int i=0; i<10; ++i)
        {
            Stdout.formatln("{}: {}", thisThread.name, i);

        }

                Stdout.formatln("{} is going to sleep!", thisThread.name);
        Thread.sleep(1.0);      // Sleep for 1 second
        Stdout.formatln("{} is awake.", Thread.name);
    }

    Thread thread1 = new Thread(&printDg);
    thread1.name = "Thread #1";
Thread thread2 = new Thread(&printDg);
    thread2.name = "Thread #2";

    thread1.start();
    thread2.start();

    thread_joinAll();
    Stdout("Both threads have exited").newline;
}

In this example, two threads are created and given a delegate in the constructor. The Thread class has two constructors: one that takes a delegate and one that takes a function pointer. This allows you to use free functions, class methods, inner functions, or anonymous delegates as the thread's worker function. Remember that pointers to class methods and inner functions are treated as delegates, whereas pointers to free functions are not.

The example also demonstrates a handful of thread API calls. First, in the printDg function, you'll notice the call to Thread.getThis. This is a static method that returns a reference to the currently executing thread. printDg uses the returned reference in order to access its name property when printing out messages. It calls Thread.sleep with an argument of 1.0, which puts the thread to sleep for 1 second. There is also a static yield method, which can be used to surrender the remainder of the current time slice.

Notice the call to the free function thread_joinAll near the end of the listing. The Thread class has a method, join, which can be used to wait for a specific thread to finish execution. For example, we could have called thread2.join() to wait until just thread2 completed. Instead, we chose to call thread_joinAll. This blocks the thread in which it was called while it waits for all currently active, non-daemon threads to complete. It also shows that there is more to the tango.core.Thread module than just the Thread class. It includes several free functions, all prefixed thread_, which allow you to manipulate all active threads at once.

Note

A daemon thread is one that is intended to be used to perform a task for another thread. For example, a thread that runs in the background to load a resource could be considered a daemon thread. A thread can be flagged as a daemon by setting its isDaemon property to true.

The next example performs the same task as the previous one, but does so by extending the Thread class with a specific subclass. Notice that the run method of the subclass is passed as a delegate to the superclass constructor.

import tango.io.Stdout;
import tango.core.Thread;

class MyThread : Thread
{
    int id;

    this(int id)
    {
        super(&run);
        this.id = id;
    }

    void run()
    {
        for(int i=0; i<10; ++i)
        {
            Stdout.formatln("Thread {}: {}", id, i);
        }

        Stdout.formatln("Thread #{} is going to sleep!", id);
        Thread.sleep(1.0);      // Sleep for 1 second
        Stdout.formatln("Thread #{} has awakened and will now exit.", id);
    }
}

void main()
{
    Thread thread1 = new MyThread(1);
    Thread thread2 = new MyThread(2);

    thread1.start();
    thread2.start();

    thread_joinAll();
    Stdout("Both threads have exited").newline;
}

Fibers

Whereas the Thread class is used to create kernel threads, the Fiber class, also found in tango.core.Thread, is used to create what are sometimes called user threads, or in some scripting languages, coroutines. Conceptually, threads execute within a process, and fibers execute within a thread.

Perhaps the most important difference between a fiber and a thread is that the user can stop execution of a fiber for a period of time and later resume execution at the point where it was stopped. In other words, you have complete control over the execution of a fiber (assuming, of course, that you programmed the logic for the fiber yourself!). The following shows a simple example of using a fiber:

import tango.io.Stdout;
import tango.core.Thread;

void main()
{
    void printDg()
    {
        for(int i=0; i<10; ++i)
        {
            Stdout.formatln("i = {}", i);
            Stdout("Yielding fiber.").newline;
            Fiber.yield();
            Stdout("Back in the fiber").newline;
        }
    }

    Fiber f = new Fiber(&printDg);
    for(int i=0; i<10; ++i)
    {
        Stdout("Calling fiber.").newline;
        f.call();
    }
}

The call method of the Fiber class causes the delegate, or function pointer, passed to the fiber to execute. To yield control back to the call site, the static Fiber.yield method can be called at any time from within the delegate. When call is next called on the same fiber object, execution will resume immediately after the last yield.

Fibers do not need to be executed by a single thread. You can pass a fiber instance from one thread to another, no matter its current state. For example, you could use a handful of threads to continually execute dozens of fibers, instead of creating dozens of threads. At any time, you can check a Fiber's state property to determine its current status: Fiber.EXEC means it is currently executing, Fiber.HOLD means it has yielded, and Fiber.TERM indicates that execution has completed.

Collections

Collections, or data structures, are an essential part of a solid standard library in modern programming languages. Many programmers find that D's dynamic and associative arrays provide enough functionality out of the box, so they don't need separate collection classes for some tasks. However, there is certainly a need for solid, templated collection classes that go beyond what the built-in arrays can do. The tango.util.collection package fills that need.

Rather than starting from scratch and creating an entirely new collection interface from the ground up, the Tango developers based their design on an existing API: Doug Lea's collections package for Java. In tango.util.collection, you'll find a set of collection classes that are useful in a variety of situations. They are based on four basic constructs: bags, sets, sequences, and maps. All collections implement the tango.util.collection.model.Collection interface. They also implement more interfaces depending on the type of collection and the operations supported.

Bags

Bags are collections that allow multiple occurrences of any given element; that is, you can add the same element to a bag more than once. A bag may or may not be ordered. Any collection that wants to call itself a bag should implement the tango.util.collection.model.Bag interface. Alternatively, a collection can subclass the abstract tango.util.collection.impl.BagCollection class, which implements the necessary interfaces and provides some default behavior.

Currently, the tango.util.collection package includes two Bag implementations:

  • TreeBag is a red-black tree implementation. This is useful when you need to quickly search for a particular element, but don't care about the order of the elements.

  • ArrayBag is an unordered collection of elements stored in one or more internal buffers. This is useful when you need to frequently iterate the elements, don't care about the order, and don't need to find a specific element.

Note

A red-black tree is a data structure that is often used to store data that needs to be searched efficiently. For more information, see http://en.wikipedia.org/wiki/Red_black_tree.

The following example demonstrates a common use of array bags:

import tango.io.Stdout;
import tango.util.collection.ArrayBag;

class MyClass
{
    void print()
    {
        Stdout("Hello ArrayBag").newline;
    }
}

void main()
{
    // Fill an array bag with 10 instances of MyClass
    ArrayBag!(MyClass) bag = new ArrayBag!(MyClass);
    for(int i=0; i<10; ++i)
        bag.add(new MyClass);

    // Iterate the bag and perform a common operation
    foreach(mc; bag)
        mc.print();
}

This example shows a typical use case for Bag. We don't care in what order the instances of MyClass are stored in the collection. All we care about is that we can iterate it and perform a common operation. Here, we do only one iteration. But in a real application, you would likely need to do so more than one. Obviously, you could achieve the same result with a dynamic array. One of the advantages of using an ArrayBag rather than an array is that you can easily remove or insert elements with a single function call. Another is that if you stick to using methods in the Bag and Collection interfaces, you can easily change the implementation to another bag type later if necessary.

Sets

Sets are similar to bags, with the important distinction that they don't allow duplicates. All sets implement the tango.util.collection.model.set interface. As a shortcut, new implementations can subclass the abstract tango.util.collection.impl.SetCollection class.

The collection package currently contains only one Set implementation: HashSet. This is an implementation backed by a hash table. Each element you add is both a value and a key in the table. This collection is useful when every element needs to be unique, and you don't need to add or remove elements frequently. Use the contains method to determine if an element exists in the set. If you want to provide a custom hash algorithm for your own data types, you should override Object.toHash in your classes and add a toHash method to your structs. Both methods should return a type of hash_t.

Here is a code snippet that demonstrates a common use of hash sets:

import tango.io.Stdout;
import tango.util.collection.HashSet;

// Given a number n, generates the next number in the
// Fibonacci sequence
int fibonacci(int n)
{
    if(n == 0) return 0;
    else if(n == 1) return 1;
    else return fibonacci(n-1) + fibonacci(n-2);
}

void main()
{
    // Create a hash set to store integers
    HashSet!(int) set = new HashSet!(int);

    // Populate the set with the first 10 numbers in the Fibonacci sequence
    for(int i=0; i<10; ++i)
        set.add(fibonacci(i));

    // Print the sequence to the console
    foreach(i; set)
        Stdout(i).newline;

    // Now test the numbers 0 - 19 to see if they are in the set.
    // Print PASS if a number is in the set, and FAIL if it isn't.
for(int i=0; i<20; ++i)
    {
        if(set.contains(i))
            Stdout.formatln("{}: PASS", i);
        else
            Stdout.formatln("{}: FAIL", i);
    }
}

This example shows a common use case of hash sets, but also highlights a couple of "gotchas." The set is populated with a unique group of elementsin this case, the first ten numbers of the Fibonacci sequence. Then another group of elements is tested one at a time against the set. If the set contains the element, one action is taken. If not, a different action is taken. Quite often, a failed contains test will indicate failure of some sort.

Astute readers may be scratching their heads, wondering what we meant when we said "a unique group of elements" in relation to the Fibonacci sequence. The first ten numbers of the Fibonacci sequence are 0, 1, 1, 2, 3, 5, 8, 13, 21, 34. As you can see, the numbers in the set are not all unique, since the number 1 appears twice. If you run the program, you'll notice that the foreach loop that prints out each element of the set prints only a single 1. The set actually contains nine elements, rather than the ten we added. Remember that sets do not allow duplicates.

Another gotcha this code demonstrates is clearly visible if you compile and execute it. The foreach loop that prints the elements in the set outputs the following on one machine:

0
1
2
34
3
5
8
13
21

Everything looks nice and neat except for that big, ugly 34 stuck in the middle. Sets make no guarantees about the order in which elements are stored. So not only can you not store both 1s from the Fibonacci sequence in a set, you can't even print the sequence in order. That goes to show that sets are a poor choice to store the Fibonacci sequence! However, sets are perfect for elements that meet the criteria. For example, you might use a set to store a fixed range of IP addresses, where each address needs to be unique.

Sequences

Bags and sets are chaos incarnate. When it's order you need, sequences are here to save the day. Sequences are guaranteed to store elements in the order in which you add them (unless, of course, you decide to sort the collection based on some other criteria). Sequences may also allow duplicates, though that depends on the implementation. Because sequences are ordered, they provide order-oriented operations for adding elements, rather than just a simple add method. You can append, prepend, and insert elements.

All sequence implementations should implement the tango.util.collection.model.Seq interface. The tango.util.collection.impl.SeqCollection abstract class is a good starting point for new implementations. The collection package contains three sequence implementations:

  • LinkSeq is a linked-list implementation of the Seq interface. These collections have a constant cost for adding, removing, and inserting elements. They can be iterated at a constant cost as well, but finding a particular element in the list can be expensive.

  • CircularSeq has the same general characteristics as LinkSeq, but is doubly linked with a head and a tail. This makes a big difference when you need to work with a sequence in reverse. Accessing the tail of a LinkSeq is a (n) operation, where n is the number of elements in the list. Accessing the tail of a CircularSeq is a O(1) operation.

  • ArraySeq, in addition to implementing the Seq interface, provides a set of methods that allow you to set a specific capacity. When the capacity is reached, the internal array is dynamically resized to accommodate more elements. You can adjust the capacity or resize the sequence at any time. Note that when you first allocate an ArraySeq, no memory is allocated internally for the array. When you add the first element, the array is allocated using the default capacity. Since adding or inserting an element can cause the internal array to be resized, it can be an expensive operation.

When you know you need a sequence, choosing between the array implementation and one of the linked-list implementations can sometimes be a tough decision. In general, if you will be frequently inserting, appending, or prepending elements, you're probably better off with one of the LinkedList implementations in order to avoid potentially expensive resizing. If you need to access individual elements frequently from the middle of the sequence, you're better off with an ArraySeq. The difficulty comes when you need to frequently add elements to the collection and access them individually. When the choice is not obvious, the best thing to do is test, test, profile, and test and profile some more.

In the following example, we revisit our Fibonacci example using an ArraySeq, which is much better suited to the purpose than the HashSet we used previously.

import tango.io.Stdout;
import tango.util.collection.ArraySeq;

// Given a number n, generates the next number in the
// Fibonacci sequence
int fibonacci(int n)
{
    if(n == 0) return 0;
    else if(n == 1) return 1;
    else return fibonacci(n-1) + fibonacci(n-2);
}

void main()
{
    // Create an array sequence to store integers
    ArraySeq!(int) seq = new ArraySeq!(int);

    // We are using a fixed set of numbers, so set the capacity to 10
    seq.capacity = 10;

    // Populate the collection
    for(int i=0; i<10; ++i)
        seq.append(fibonacci(i));

    // Print the sequence to the console
    foreach(i; seq)
        Stdout(i).newline;

    // Now test the numbers 0-19 to see if they are in the collection
    // Print PASS if a number is in the collection, and FAIL if it isn't.
    for(int i=0; i<20; ++i)
    {
if(seq.contains(i))
            Stdout.formatln("{}: PASS", i);
        else
            Stdout.formatln("{}: FAIL", i);
    }
}

The code here is very similar to that used previously with the hash set. The biggest difference is that we call the append method to add each number to the end of the sequence. This means that when we iterate the sequence, each number will be returned in the order it was added. If you compile and execute the program, you should see the following output from the foreach loop that prints each element in the collection:

0
1
1
2
3
5
8
13
21
34

This output is much more suitable for the Fibonacci sequence. The collection contains both of the 1s and, on iteration, returns each number in the proper sequence. They're not called sequences for nothing!

Maps

Maps are useful things. They allow you to take an element of one type and associate it with an element of another type as a key/value pair. D's built-in associative arrays are maps. Tango maps have the same functionality, but go beyond the simple built-in operations. All maps should implement the tango.util.collection.model.Map interface or extend the tango.util.collection.impl.MapCollection abstract class.

Tango ships with three map implementations: LinkMap, TreeMap, and HashMap. The difference between the three implementations is largely based on the time it takes to complete the operations from the Map interface. Many of the operations of LinkMap are O(n), whereas HashMap operations typically have a best-case performance of O(1) and worst-case performance of O(n). Several of the TreeMap operations tend to be somewhere in the middle, at O(log n). It's not immediately obvious which implementation to choose without looking at the performance characteristics of each operation. Fortunately, the performance of each operation is documented well. A quick overview can give you a general idea of which implementation is more suitable for certain situations.

When you just need somewhere to store key/value pairs for iteration and don't need to perform any lookups by key, a LinkMap is a perfect choice. Doing key lookups on one of these can be really expensive if there are a lot of elements. If you are frequently looking up values by their keys, but not doing much iteration of all elements, you'll be better off with a HashMap. The TreeMap is perhaps best used when you have a large number of key/value pairs to add. When a HashMap contains a large number of elements, collisions are more likely, making each bucket more likely to reach the worst-case performance during a lookup. TreeMaps have a predictable lookup time for both keys and values, and you don't suffer as much for adding more elements. Ultimately, though, it's the profiler that should tell you which implementation is best suited to your situation. This is true for all of the collections, really, but more so for the maps.

More on Collections

In addition to the collections themselves, the tango.util.collection package contains a few other useful items that can make your use of collections more robust. You'll find different types of iterators, such as an IterleavingIterator and a FilteringIterator, which can be used in place of the foreach loop. A Comparator can be used to sort elements in a collection. You can even use a special delegate, called a screener, to allow only elements that meet certain criteria to be added to a collection.

The collection package can do quite a lot for you, so that you don't need to roll your own. You can get by with D's built-in dynamic and associative arrays for many simple tasks, but for more complex uses, you'll need to manually implement some of what the collection package already does for you. Remember that when you find yourself adding more and more code to manage your dynamic array-based set!

Logging

Tango's logging API, which is defined by the modules in the tango.util.log package, is a flexible and extensible framework that can be configured at runtime. Like the collection API, the logging API is not something the Tango developers created out of thin air. One of the most popular logging APIs in existence is a Java library called Log4J. The design of Tango's logging framework closely follows that of Log4J, so if you are coming from a Java background, you may already be familiar with it.

In order to use the log package, you need to know two basic things: how to create a Logger and what log levels are.

Loggers

The following code demonstrates how to create a Logger instance and log a simple message.

import tango.util.log.Configurator;
import tango.util.log.Log;

void main()
{
    Logger logger = Log.getLogger("MyLogger");
    logger.info("Hello world");
}

This example imports the tango.util.log.Configurator module. This module contains a static constructor that configures the logging system to send all output to the system console. It sends output through Stderr by default.

The call to Log.getLogger creates a new logger instance and assigns it the name "MyLogger". Names are important in the logging framework because, internally, the loggers are stored in a hierarchy based on their names. When a new logger is added to the hierarchy, it receives the settings and properties of its parent logger. If we were to create another logger, with a call such as Log.getLogger("MyLogger.Child"), the "." in the name would indicate that the new logger is a child of the instance named "MyLogger". For this reason, it is common to create loggers named after the module in which they reside.

The "MyLogger" instance is also a child. Even though we did not explicitly assign a parent to it, it was added to the hierarchy as a child of the special root logger. The root logger is created automatically by the framework. When the static constructor in the Configurator module runs, it is the root logger that is being configured. When a new logger instance is created as a child of the root, it receives the same configuration. If you need to explicitly access the root logger, you can do so via the static method Log.getRootLogger.

Log Levels

It's very handy to be able to configure different "degrees" of logging output. For example, some output is useful for debugging but isn't really a good idea to leave in the final release. Traditionally, C and C++ developers would compile debug and release versions of their software, with debug logging enabled in the former and disabled in the latter. This works some of the time, but experience has shown that it can be very useful to enable debug logging in the release version as well. The solution is to allow debug logging to be configurable at runtime rather than at compile time.

Log levels allow you to specify different degrees of log output. You can set six different log levels:

  • Trace is intended to be used for debug output.

  • Info is intended for logging informational messages, such as those that mark the flow of an application.

  • Warn is intended for logging warning messages, such as in response to events that aren't really errors but are unexpected or unusual behavior.

  • Error is intended for logging errors from which the program can recover.

  • Fatal is intended for logging errors that cause the program to exit.

  • None turns off the logger entirely.

The levels are listed here from lowest priority to highest. When a level is set on a logger instance, all messages that are intended for that level and higher will be logged, while messages intended for lower levels will be ignored. For example, setting the Trace level turns on logging for all levels, while setting the Error level restricts logging to just Error and Fatal level messages. Although you can assign any meaning you want to each level, it is recommended that you follow the suggested intent, as noted in the list.

You can associate log output with a particular level in two ways. The Logger class has an append method, which accepts two parameters: a log level and a message string. Most of the time, though, you'll want to use one of the five shortcut methods, which each accept a single string as a parameter: trace, info, warn, error, or fatal. The following example shows how to set the level of a logger and use each of the logging methods:

import tango.util.log.Configurator;
import tango.util.log.Log;

void main()
{
    Logger logger = Log.getLogger("MyLogger");

    // Turn off Trace messages
    logger.level = Logger.Level.Info;

    logger.trace("I'm a trace message, but you can't see me!");
    logger.info("I'm an info message!");
    logger.warn("I'm a warn message!");
    logger.error("I'm an error message!");
    logger.fatal("I'm a fatal message!");
    logger.append(Logger.Level.Fatal, "I'm a fatal message, too!");

    // Turn Trace messages back on
    logger.level = Logger.level.Trace;

    logger.trace("Hey, you can see trace messages now!");
}

More on Logging

What we've shown you so far is all you really need to know to use Tango's logging framework. But logging to the system console isn't always useful, particularly for applications that the end user runs in a window. It's much better to send log output to a file, and that's a simple thing to do. Tango lets you configure the target of log output with a construct called an appender.

Tango ships with six appender implementations:

  • ConsoleAppender sends output to the system console and is configured by default when you import the Configurator module.

  • FileAppender directs output to a file.

  • RollingFileAppender directs output to one of a group of files based on a maximum size.

  • SocketAppender sends output to a network socket and is useful for remote debugging.

  • MailAppender e-mails log output somewhere.

  • NullAppender sends log output nowhere and may be useful for benchmarking.

Of course, if none of the stock appenders meet your requirements, you can implement your own.

Note

It's possible to have more than one appender attached to a logger via the addAppender method. In fact, newly created logs inherit the appenders of their parents, so any new appenders you add to a logger will cause output to be sent to it in addition to those inherited from the parent, unless you explicitly disable one or more of them.

You can also control the format of the log output by using a Layout implementation. Tango currently has a few implementations, all of which extend the base EventLayout class. The default configuration set up by the Configurator module uses the SimpleTimerLayout, which prepends to the output the number of milliseconds since the application started, the level of the message, and the name of the logger that wrote the message. The other stock layouts are all variations on this theme.

The following example shows how to create a logger that sends its output to a file using the SimpleTimerLayout:

import tango.util.log.Log;
import tango.util.log.FileAppender;
import tango.util.log.EventLayout;
import tango.io.FilePath;

void main()
{
    auto fa = new FileAppender(new FilePath("log.txt"), new SimpleTimerLayout);
    Log.getRootLogger.addAppender(fa);

    Logger logger = Log.getLogger("MyLogger");
    logger.info("Hello file appender!");
}

This should be enough to get you going with the logging framework right away. Notice that the Configurator is not imported, since we are configuring the root logger ourselves. As an exercise, go ahead and add an import statement for tango.util.log.Configurator, and see what happens when you run it.

And That's Not All!

Tango has more than we've covered so far and more that may be added to the library in the future. At the time of this writing, two very recent additions to Tango are the tango.io.vfs and tango.net.cluster packages.

The tango.io.vfs package is a virtual file system (VFS) API. The goal of this package is to allow users to access disparate file systems through a uniform interface, regardless of the current platform. The basic premise is that you mount specific paths to the VFS, and then read and write resources wherever they may be. Mounted paths could be from the local file system, a remote file system, or a zip archive. The package is still in development, so not all of the features are implemented yet, and the design will likely fluctuate over the next few months. As you're reading this, it may or may not be in its final state.

The tango.net.cluster package is not the sort of API you will find in your average standard library. This ambitious package aims to aid you in creating software that can be clustered on multiple physical machines. Run on one machine or run on a dozen, add new machines or remove old ones, and your software will still do the right thing. If a machine in the cluster dies, the others will take over its workload. This is a highly specialized package that isn't going to be useful to everyone, but it will make a wide range of applications much more accessible to Tango users. It may be useful for enterprise application servers, massively multiplayer game servers, or distributed programs doing intensive number-crunching.

Finally, more packages are in the works. For example, in the summer of 2007, the Tango team announced a tango.graphics package. This package will, at a minimum, provide an API for rendering 2D graphics. It will be usable on the server side for generating images on the fly, or on the desktop for rendering to application windows. It will take advantage of hardware acceleration where it's available, and otherwise fall back to software rendering. There are still a lot of design and use-case details to be ironed out, but the package is expected to see a beta release in early 2008.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset