470 likes | 586 Views
Windowing Systems Windowing systems are probably the most non-standard aspect of modern computers, from a developer’s point of view. There are many different windowing systems: Mac, Windows, X11, OS/2, NeXT, Java, Amiga, GEM, Desqview ...
E N D
Windowing Systems • Windowing systems are probably the most non-standard aspect of modern computers, from a developer’s point of view. • There are many different windowing systems: Mac, Windows, X11, OS/2, NeXT, Java, Amiga, GEM, Desqview ... • All have similar look and feel, and almost every element is common: windows, scrollbars, buttons, etc. • The basic “event-driven” model is also the same for each.
However, the programming interfaces for each system are all different, and this is where the difficulty lies for the programmer. • There is a very large market these days for cross-platform GUI toolkits that allow cross-compilation on different windowing systems: qt, wxWindows, etc. • This situation has allowed the most popular application platform (you know who) to acquire a large amount of platform specific software, because the costs required to produce separate versions of each program aren’t always justified by the (often low) demand from the smaller markets such as Linux or the Macintosh.
A brief history of windowing systems • The first GUI with a WIMP interface (Windows, Icons, Mouse and Pull-down menu’s) was created at Xerox PARC in the mid 1970’s: the “Alto”. • However, the elements that were used in the “Alto” and (later) in the “Star” were designed in the 1960’s by Doug Engelbart, Alan Kay and others. • The Macintosh was the first computer with a GUI to reach the mass market. Other windowing systems drew heavily upon the Mac’s “look and feel.”
We’re not going to discuss each windowing system; that would take many lectures. We’ll focus on the common aspects that are important to programmers. • Ordinary text-based programs tend to have a structure that looks like this:initialize; get input; while (!end-of-input) { process-input; get-more-input; } end program • Sometimes the program might only take one set of inputs.
Programs written for windowing systems are “event-driven.” This means that they have a main processing loop that looks like this:initialize; while (!done) { get next event; process event; } end program; • It may look similar to traditional programs, but there is a basic difference: “events” are more abstract than “input” and may come from many sources: the user, another application, the OS, even from across a network.
In traditional programming, input is generally entered in a serial fashion and processed in order. • Windowing events are deposited in an event queue in the order of their appearance and processed in that order. • But windowed programs can process certain events, spawn threads to handle other events asynchronously or even completely ignore them! • The different events handled by a windowing system include: mouse movements, mouse clicks, keyboard activity, redraw requests, button toggles, scroll requests, gain and loss of focus, etc.
Window programs would be almost impossible to write if they had to implement each and every detail involved in detecting these events! • Fortunately, the OS usually handles the hard parts, especially those that have to do with hardware. • The OS (in the case of the Mac or Windows) or the windowing libraries (in the case of the various Unixen) “intercept” all user interaction. For each user action, they determine which window is affected (if any) and dispatch an event to the window’s event queue. • (Some OS’s use one queue for all windows.)
So a basic GUI program only has to do something like this:initialize;draw window to some specification;while (!done) { get an event; // An OS or GUI lib call switch (event_type) { case REDRAW: redraw window; break; case MOUSE_CLICK: determine location of click; move battleships to location; break; default: // ignore; }}
If you write GUI code in a non-object oriented language (such as C) for any windowing system, your code will look just like that. E.g., Windows 3.1x code. • There are a lot of details associated with this style of programming. There is usually a lot of code that is the duplicated from program to program. Programmers will find themselves writing more skeleton code than application code. • In fact, the basic switch-based structure of this model is reminiscent of ... Implementations of object hierarchies (such as shapes) in non-object oriented languages.
void DrawShape (Type t, int x, int y, int size, Color c) { switch (t) { case TRIANGLE: DrawTriangle (x, y, size, c); break; case CIRCLE: DrawCircle (x, y, size / 2, c); break; case SQUARE: DrawSquare (x, y, size, c); break; default: } }
We found a better way to express the notion of different shapes with common interfaces: an object hierarchy:class Shape { virtual void Draw (int x, int y, int size, Color c)= 0;};class Circle : public Shape { void Draw (int x, int y, int size, Color c){ // Draw a circle }};...Class Square : public Shape { ... }
Similarly, window applications lend themselves well to formulations as object oriented systems. • But in order to use C++ constructs, the OS or windowing libraries have to be written in C++, right? • Well, it helps. But there are many “frameworks” that are built on top of windowing systems that are written in C++. • It’s a simple concept: define a class “WindowObject” that implements every kind of event with virtual functions. Then, any class that inherits from WindowObject will automatically be GUI-aware; all it’ll have to do is implement the events that it’s interested it.
This is how libraries like MFC operate: they sit on top of their OS’s C-based windowing API (Application Programming Interface) and, with the help of a few wizards and some compiler-IDE support, actually make window program development rather easy. • So with libraries like MFC presenting large collections of pre-programmed standard components and a collection of virtual event handlers to override, developers can spend more time developing the features of their programs and not re-implementing the same old window-control routines.
Window toolkits usually follow similar display standards:- Title bar: icon, title, management buttons- Menus: file, options, help...- Toolbars: save / load buttons, widgets...- Client area: graphics, bitmaps, text...- Status bar: messages, general information... • Some systems can get quite complicated: for example, MFC parent-child window interaction is highly complex. • In general, dialog box elements (checkboxes, radio buttons) are separate classes that can be inherited from and given custom behavior.
We’re not going to cover specifics in CS 213. A good site for MFC questions is http://www.stingsoft.com/mfc_faq/ • Some general Win32 links can be found at http://www.r2m.com/windev/win32.html and Microsoft’s MSDN site. • For other OS’s, you’re pretty much on your own. My advice would be to purchase a book that discusses your GUI toolkit in detail. There are good books at the campus store that discuss programming the MacOS, Motif, etc.
File I/O • Files, as we’ve seen, can be thought of as bidirectional data streams. This is the preferred C++ view, exemplified by standard library classes such as ifstream and ofstream. • This is a good conceptual view of files, and it’s especially useful for text-files, which store simple ASCII strings. • For example, here’s a program that writes a series of words from cin to a file and another program that reads the words from the file back to the screen.
#include <fstream.h> void main() { char pszBuffer [1024]; ofstream ofFile ("C:/Log.txt"); if (!ofFile.is_open()) { cout << "Error opening file!" << endl; } while (true) { cin >> pszBuffer; if (*pszBuffer == '/') { break; } ofFile << pszBuffer << endl; } ofFile.close(); }
#include <fstream.h> void main() { char pszBuffer [1024]; ifstream ifFile ("C:/Log.txt"); if (!ifFile.is_open()) { cout << "Error opening file!" << endl; } ifFile >> pszBuffer; while (!ifFile.eof()) { cout << pszBuffer << " "; ifFile >> pszBuffer; } cout << endl; ifFile.close(); }
However, sometimes programmers want to use lower-level operations in order to store data structures and objects in a file. • For example, here’s a function that writes the following struct to disk and another that restores it:struct Student { int StudentID; char StudentName [100]; long TuitionRate; double GPA;};
void SaveStudent (char* pszF, const Student& s) { ofstream ofFile (pszF, ios::binary); ofFile.write ((char*) &s, sizeof (Student)); } void LoadStudent (char* pszF, Student& s){ ifstream ifFile (pszF, ios::binary); ifFile.read ((char*) &s, sizeof (Student)); } • These functions would be called like this:
void main() { Student s; s.StudentID = 45; strcpy (s.StudentName, "Max Feingold"); s.TuitionRate = 10000; s.GPA = 4.3; SaveStudent ("C:/student.dat", s); Student s2; LoadStudent ("C:/student.dat", s2); }
General object serialization is more complicated. Because of the way C++ objects are laid out in memory (remember vtables?), a simple struct copy won’t work. • Also, an object may contain dynamically allocated memory or other objects that aren’t easily serializable. • In general, the best way to approach this problem is on an object-by-object basis, creating a framework in which every object implements the following static functions:void Save (File* file, Object& a);Object* Load (File* file);
This scheme forces each class to implement on one hand a specific routine that saves an object to a file, and on the other, an “object factory” that knows how to create an object from file data. • The Load function in particular can be made static and thus be called from anywhere in the client program’s code. • If a class contains objects as data: in this case, the specific Save() functions defined for these objects’ classes can be nested into the main class’s Save() function. • Alternative approaches use constructors with File* arguments instead of Load(). The effect is the same.
In general, a theoretical file class would present the following interface: class File { public: int Open (char* pszFileName, int mode);int Close(); bool IsOpen(); int Read (char* pszBuffer, int iNumBytes);int Write (char* pszBuffer, int iNumBytes); int Seek (int iNumBytes; int GetPosition(); int Search (char* pszBuffer, int iNumBytes); bool Eof(); };
Note that this isn’t meant to be a description of an actual file class that exists (although some existing classes, like MFC’s CFile, are somewhat similar). • Rather, it is meant to be a template for understanding what existing file I/O classes or file libraries do. • The simplest file I/O library to use is probably the standard C library, which has four simple primitives: • fopen(); • fread(); • fwrite(); • fclose();
When working on your projects, feel free to use the libraries that suit your program the best. We encourage you to use the stream redirection operators and the C++ standard library whenever possible, but there are cases where simpler solutions are better. • Remember that the C++ standard library classes and the C library present interfaces that are portable across multiple platforms. Other possibilities, such as MFC classes, will only work with Windows.
Sockets • Sockets were originally designed in a C-based UNIX environment. • In UNIX, just about every system “object” (such as files, pipes or devices) presents a simple open/read/write/close interface. • It seemed logical to the sockets designers to extend this paradigm to networks as well. The idea is a natural one: open a socket, read from it, write data to it, and finally close it.
So, what exactly is a socket? Well, sockets can be thought of as logical pipes that connect two computers over some physical connection medium (such as the telephone line or ethernet). • There must exist a large infrastructure beneath the sockets level for connections to be established: • Physical connection medium. • Routers. • OS support for devices: modems, ethernet. • OS support for low-level protocols: IP • OS support for higher-level protocols: TCP, UDP, IPX, NetBios, etc. • OS buffers. • Etc.
Socket connections are established with a few simple C commands, but there’s a lot going on under the surface. That’s covered in CS 514 or 519, not here. • Sockets are abstractions of connections or pipes between two different computers. They can be read from and written to without worrying about issues like flow control, byte ordering, network routing, error control, retries, etc. For the programmer, within limits, they just “work.” • Those limits include the following possibilities: • Connections fail due to network conditions. • The client or server on the other end sends “erroneous” data. • There are different versions of protocols or applications.
The basic socket primitives for TCP connections (the only kind we’re going to discuss) are the following: • Open: prepare a socket for a connection • Listen: assign a socket to listen at a TCP port • Accept: wait until a incoming connection arrives • Connect: connect to a given server and port • Send: send data through the socket • Recv: receive data from the socket • Close: close the socket (and its connection) Different languages may implement these in different ways, but the basics are the same in C, Java or the MFC libraries (CSocket).
C socket programming is rather low-level and ugly. In this lecture we will use a Socket class that I wrote to abstract away the ugly details. • As an example of a client program that uses sockets, I wrote a simple web browser that requests an arbitrary document from a server and cout’s the result to the screen. • (Note that a proper web browser is a lot more complicated than this and would implement better handling of various issues, among them HTTP header parsing.)
Server programs are the mirror image of the client code that we’ve seen. The server would look like this: Socket s; s.Open(); s.Listen (80); Socket* p = s.Accept(); // Blocking call p->Recv(); // etc. p->Send(); // etc. delete p; • A typical server would enclose the Accept() call in a loop and either spawn a thread for each incoming connection or pass requests off to a queue.
Socket s; s.Open(); s.Listen (80); Socket* p = NULL; while (true) { p = s.Accept(); EnQueue (p); if (some_condition) { break; // Exit server loop } } • The assumption here is that some other thread would handle the queued socket and delete it when finished.
Threads and synchronization • Threads are a very important topic in C++ programming. You aren’t a “real” programmer until you’ve had to deal with the problems of writing multi-threaded applications. • However, the same problems that we were having with GUI’s occur with threads: there are no standard library interfaces tha define thread creation and use. • There are several different thread “packages” the one can use: C-threads, MFC, Win32, etc. Most OS’s implement thread support in different proprietary ways, if at all.
Traditionally, each program (or “process”) on a system would have only one line of execution (or “thread”). Each program would begin, process data and eventually exit. Each process would have its own virtual address space and its own heap, its own stack and its own set of global and static variables. • However, most OS’s allow multiple threads of execution per process, which share the same heap (usually) and the same global variables. • Each thread has its own stack, however, which makes sense because each thread enters and exits functions independently from the others.
Programs with more than one thread are called “multi-threaded.” Examples of multithreaded applications include: • Windows Explorer (the desktop process) • Any internet server (obviously) • Netscape (opens a thread per connection) • Microsoft Powerpoint (who knows?) • Most GUI applications (to provide fast redraws while performing background processing) • Games (different threads might control different aspects of the computer’s AI) • etc.
So how do threads work? This is their conceptual interface (provided in some form by most thread packages): • StartThread: spawn a new thread in a given function and initialize it with the given data (more on this in a minute). • SuspendThread: put the thread to sleep • ResumeThread: wake the thread up • SetPriority: change the priority of the thread • “Priority” refers to the importance of the specific thread in the OS’s scheduler. In a pre-emptively multitasking environment, this determines how much CPU time the thread gets with respect to other processes and threads.
The complicated material here is in the StartThread function (which in different OS’s is called CreateThread, _beginthread, etc.) • This function is usually defined as follows:StartThread (THREAD_FXN*, void*); • THREAD_FXN would be a pointer to a function:typedef int (*THREAD_FXN) (void*); • So one would call the function as follows: DataStructure* p = new DataStructure(); StartThread (RunServer, (void*) p);
The call would return immediately and a new thread would start executing in the following function: int RunServer (void* pData) { DataStructure* p = (DataStructure*) pData; ... } • It is important to note that no assumptions can be made about the scheduling of the different threads within a process. Regardless of priorities, one can never assume that an instruction in one thread will be executed before an instruction in another thread.
A corollary to this observation is that once multiple threads are running within a process, no assumptions can be made about the values of global objects or dynamically allocated memory referenced by global or shared pointers. • For example, if several web server threads share a common pointer to a linked list of cached DNS entries, then all of them could conceivably be reading, inserting or deleting nodes at the same time. • The dangers of this situation are obvious. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
An even simpler case can cause problems with multi-threaded applications. Imagine a global integer x with an increment function: void IncrementX() { x ++; } • While this might seem to be an atomic operation, it actually isn’t. Computers don’t understand C++ natively; the C++ passes through a compiler and gets turned into machine code. • IncrementX() looks like this when compiled in debug mode with VC++ 6.0:
004016A0 push ebp 004016A1 mov ebp,esp 004016A3 sub esp,40h 004016A6 push ebx 004016A7 push esi 004016A8 push edi 004016A9 mov eax,[x (00415414)] 004016AE add eax,1 004016B1 mov [x (00415414)],eax 004016B6 pop edi 004016B7 pop esi 004016B8 pop ebx 004016B9 mov esp,ebp 004016BB pop ebp 004016BC ret
The highlighted instructions are the important ones: 004016A9 mov eax,[x (00415414)] 004016AE add eax,1 004016B1 mov [x (00415414)],eax • What could happen in a multithreaded application is the following: • Thread 1 calls IncrementX() and executes:004016A9004016AE //The value in EAX is now x + 1 • Thread 2 calls IncrementX() and returns. x is now x + 1. • Thread 1 executes 004016B1 // x is now x + 1 • So two calls to IncrementX only incremented x once!!
Clearly we need a solution to this problem before we can write multithreaded applications. That solution is called “synchronization.” • Synchronization consists of using objects called “mutex locks” (mutex == mutual exclusion). Other types of objects can be used (such as semaphores), but we’ll concentrate on objects that exist in common C++ libraries. • Mutex objects have three basic interface functions: • Create: in Win32, CreateMutex() • Wait: in Win32, WaitForSingleObject() • Signal: in Win32, ReleaseMutex()
The best way to think of mutexes is to conceive of them as boolean values with two possible states: ‘wait’ and ‘signal.’ Mutexes are initialized to the ‘signal’ state. • Wait() blocks until the state of the mutex is ‘signal’, then sets it to ‘wait’ and returns. • Signal() sets the mutex back to the ‘signal’ state, allowing another waiting thread to be woken up. • So if we surround our x++ instruction with a Wait and a signal, only one thread can execute that instruction at a time:
Mutex m; void IncrementX() { m.Wait(); x ++; // Critical section m.Signal(); } • Only one thread will be able to enter the “critical section” at a time, so there’s no danger of incrementing x incorrectly. • Locking strategies may be arbitrarily complex. There are many “special case” algorithms like “readers and writers.”
These more complex algorithms are used for different kinds of problems. • R&W, for example, applies to the case where reads and writes of a resource take a long time, so it is desirable to allow readers to access the resource at the same time and only be locked out in the case of concurrent writer. • In database systems, for example, this type of problem is solved with a “lock manager” that assigns read and write locks upon request, and has the right to deny them if the system’s conditions do not allow their concession.