2ee739cc |
1 | A very brief overview of Sapphire |
2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
3 | |
4 | _____________________________________________________________________________ |
5 | |
6 | IS THIS ALL THERE IS? |
7 | |
8 | |
9 | The Grand Plan was to write a really good library, sell some spiffy |
10 | applications which used it, and then sell the library. This |
11 | spectacularly failed to work, mainly because we had no ideas for killer |
12 | applications. The result is Sapphire. |
13 | |
14 | Since we'd planned to write some big applications before releasing |
15 | Sapphire to anyone else, we took a rather relaxed attitude to |
16 | documentation. There is some LaTeX documentation lying around, but |
17 | mostly it's initial versions of things, with big gaps where less |
18 | interesting text should be filled in later. In short, it's no use to |
19 | anyone. |
20 | |
21 | On the other hand, we realised that a library of this complexity with no |
22 | documentation at all would be a total disaster, so all the header files |
23 | are profusely commented, mostly to the standard of a final-copy |
24 | reference manual. |
25 | |
26 | This text file is intended as a brief hacker's eye view of how to write |
27 | Sapphire applications, and how some of the more complicated and/or |
28 | interesting bits work. |
29 | |
30 | _____________________________________________________________________________ |
31 | |
32 | ABOUT THE AUTHORS |
33 | |
34 | |
35 | Sapphire was designed and written, over the course of a couple of years, |
36 | by Mark Wooding and Tim `Tycho' Armes. Each source file usually bears |
37 | the initials (`MDW' or `TMA') of the person mainly responsible for |
38 | writing it. In some cases, this is misleading. For example, although |
39 | the `msgs' code for translating message tokens into strings is labelled |
40 | `MDW', the first two versions of this file were written by Tim: I took |
41 | over later because I rewrote it to kill the bugs. Similarly, the fact |
42 | that one of us wrote a source file doesn't tell you who designed it. For |
43 | example, the `menu' code was entirely written by Tim, although we both |
44 | worked on the design for several days beforehand. |
45 | |
46 | _____________________________________________________________________________ |
47 | |
48 | A HISTORY LESSON |
49 | |
50 | |
51 | Sapphire is an attempt to learn from the experience we both had with |
52 | STEEL. STEEL (Straylight's Extensive Event-driven Library) is a |
53 | more-or-less complete re-write of RISC_OSLib, and although it had given |
54 | up being call-compatible, and had diverged quite considerably by the |
55 | release of Desktop C, it suffered because of the limitations of its |
56 | structure. All the original non-RISC_OSLib code had been written by me, |
57 | in a rather ragged way. |
58 | |
59 | The name Sapphire comes from the old ITV sci-fi series `Sapphire and |
60 | Steel', obviously. It's a rotten joke, but that's what you get when you |
61 | think up names for libraries in a pub. The idea was that two of us |
62 | could do much better than either acting alone, by bouncing ideas between |
63 | each other until we sorted out the problems. We threw out any thought |
64 | of compatibility with any existing software, and any hope of interfacing |
65 | a high-level language. Then we started coding. |
66 | |
67 | _____________________________________________________________________________ |
68 | |
69 | INFLUENCES |
70 | |
71 | |
72 | The design of Sapphire has been influenced by looking at lots of other |
73 | pieces of software. A lot of structure is based on RISC_OSLib: STEEL |
74 | was derived from RISC_OSLib, and it did a lot of things right. Another |
75 | particularly strong influence is Computer Concepts' Advanced Box |
76 | Interface (ABI) as used in Impression and ArtWorks. (I even went as far |
77 | as buying the ArtWorks SDK more-or-less to get my hands on some ABI |
78 | documentation.) There was an influence from what I perceived to be `the |
79 | way OS/2's Presentation Manager does it', although this wasn't always |
80 | positive. |
81 | |
82 | _____________________________________________________________________________ |
83 | |
84 | SUPPORTED LANGUAGES |
85 | |
86 | |
87 | Sapphire supports ARM assembler. That's about it. One of the last |
88 | Sapphire-related projects was interfacing C to Sapphire; this is still |
89 | mostly incomplete. The basic job of making C code call Sapphire code |
90 | and vice-versa is finished and reliable. The hard bit, handling |
91 | Sapphire-style definition tables, isn't even begun yet. |
92 | |
93 | One of the projects I set myself over a year ago was a language, then |
94 | called `cdata', which would act as a pre-processor for C and allow |
95 | definition tables to be created without the amount of pain currently |
96 | required. (If you don't believe me, see the definition of the icon bar |
97 | menu in the `csapph' test program.) |
98 | |
99 | _____________________________________________________________________________ |
100 | |
101 | THE SAPPHIRE ENVIRONMENT |
102 | |
103 | |
104 | Sapphire doesn't use APCS. Let's get that clear right away. You try to |
105 | use APCS, and Sapphire will hate you for the rest of your life. |
106 | |
107 | Because the target language is assembler, Sapphire itself sometimes |
108 | plays fast and loose with the rules, but the basic ideas, such as they |
109 | are, are presented here. |
110 | |
111 | Registers R10-R15 are assigned special purposes. |
112 | |
113 | For architectural reasons, R15 is the program counter, and R14 is the |
114 | subroutine link register. Because R14 is modified by subroutine calls, |
115 | it is often used as a temorary register in computations. |
116 | |
117 | R13 points to a full descending stack. Sapphire doesn't allocate a very |
118 | big stack unless you ask it to, so be careful with recursion. There's |
119 | no stack limit checking either. |
120 | |
121 | R10 and R12 provide the program's current state. R12 is the `workspace' |
122 | pointer, and R10 is the `object' pointer. The idea is that R12 points |
123 | to a statically allocated data block, and R10 points to a `currently |
124 | interesting object'. For example, in a multi-document editor, R10 would |
125 | point to an information block describing the document currently being |
126 | processed. Because it's important to hide information between |
127 | components of a program, different components have different workspace |
128 | areas. Similarly, different parts of a program see the world at |
129 | different levels: the part of a drawing program which is concerned with |
130 | file-level operations keeps R10 as a pointer to the document anchor; the |
131 | part concerned with manipulating individual objects within a document |
132 | might keep R10 referring to an object. Note that R10 and R12 rarely |
133 | take part in inter-component interfaces. Object handles are passed |
134 | between components in low-numbered registers, as normal arguments, and |
135 | then copied into R10 later. |
136 | |
137 | R11 is Sapphire's `application context' pointer. It's also known as the |
138 | `scratchpad' pointer. From R11 upwards is the `scratchpad': a block of |
139 | memory available for any purpose. Any procedure may corrupt the |
140 | scratchpad, so it's never safe to assume that it is preserved over |
141 | inter-component calls. Immediately below R11 are workspace offsets. |
142 | These allow Sapphire and its client applications to find statically |
143 | allocated workspace. If R11 is pointing to the wrong place, Sapphire |
144 | will crash! |
145 | |
146 | On a procedure call, this is what usually happens: |
147 | |
148 | R0--R9 == arguments |
149 | R10, R12 == variables owned by caller |
150 | R13 == pointer to full descending stack |
151 | R11 == application context pointer (Scratchpad) |
152 | |
153 | On return: |
154 | |
155 | R0--R9 == return values, or usually preserved |
156 | R10, R12 preserved |
157 | R13 == preserved |
158 | R11 == application context pointer |
159 | Flags either preserved or set as a return value |
160 | |
161 | Sapphire's fond of returning information in the flags. For example, if |
162 | you ask for memory and there isn't any, allocation functions return with |
163 | carry set. Sometimes, errors are returned in the normal way, by setting |
164 | overflow and pointing R0 at an error block. If you don't return |
165 | something useful in a flag, preserve it. |
166 | |
167 | Sapphire behaves slightly different when it's calling its client back. |
168 | When you register a handler function, you supply a pointer to the code, |
169 | and R10 and R12 values to pass it. It will ensure that these values are |
170 | in those registers at call time. |
171 | |
172 | _____________________________________________________________________________ |
173 | |
174 | SAPPHIRE'S MEMORY MAP |
175 | |
176 | |
177 | Sapphire has strong ideas about how an application's memory is laid out. |
178 | Here's a poor attempt at a diagram: |
179 | |
180 | |
181 | _____________ <--- Top of current wimpslot |
182 | | | |
183 | : Flex : |
184 | : heap : |
185 | | | |
186 | |-------------| |
187 | | Resizing | |
188 | | heap | |
189 | |-------------| <--- Top of initial wimpslot |
190 | R13 | Stack | |
191 | |-------------| |
192 | R11 | Scratchpad | |
193 | | App context | |
194 | |-------------| |
195 | | | |
196 | |Default heap | |
197 | | | |
198 | |-------------| |
199 | R12 | Workspace | |
200 | |-------------| |
201 | | Application | |
202 | | code | |
203 | &8000 |_____________| <--- Base of application memory |
204 | |
205 | |
206 | Hmmm. Not too shabby. |
207 | |
208 | Now, in words: |
209 | |
210 | During initialisation, Sapphire's kernel finds the top of available |
211 | memory and puts the stack there, growing downwards. Below the stack, it |
212 | allocates the scratchpad area, so that there's a small amount of `slop' |
213 | between the the stack growing down and the scratchpad growing up. The |
214 | kernel then examines the available library components, and allocates |
215 | workspace for them, starting just above the application's read-write |
216 | area. (A Sapphire application shouldn't actually have any read-write |
217 | data -- R12-space is a much better way of doing the same job.) Once |
218 | that's done, it initialises an OS_Heap heap between the top of workspace |
219 | and the bottom of the application context area just below the |
220 | scratchpad. This is the `default heap'. |
221 | |
222 | Above the initial wimpslot is Flex space. Flex is Sapphire's shifting |
223 | heap manager. It's named after RISC_OSLib's shifting heap, although |
224 | it's completely rewritten, and much extended. Flex has complete control |
225 | over the area above the initial wimpslot. The first Flex block is taken |
226 | by a resizing heap manager, which creates a nonshifting heap there. This |
227 | is registered automatically with Sapphire's allocation manager, so you |
228 | can forget about it completely. |
229 | |
230 | This brings me on to... |
231 | |
232 | _____________________________________________________________________________ |
233 | |
234 | MEMORY MANAGEMENT |
235 | |
236 | |
237 | Sapphire sees three sorts of memory: |
238 | |
239 | * Static memory. This is allocated at initialisation time by the |
240 | kernel, and put into the workspace area. R12 points here. |
241 | |
242 | * Fixed-size blocks representing objects. These are allocated and |
243 | freed at run-time by the program, using Sapphire's allocation |
244 | manager. R10 points to one of these, usually. |
245 | |
246 | * Large or variable-size blocks representing data. These are |
247 | allocated and freed by the program using Flex. |
248 | |
249 | Typically each large block in Flex-space has an accompanying `anchor' |
250 | block in heap-space. |
251 | |
252 | The allocation manager attempts to paper over the mess involved with |
253 | having more than one nonshifting heap. STEEL actually does this job |
254 | better, putting the client in control of where blocks get allocated. |
255 | Sapphire's approach, to fill up one heap and move on to the next, is |
256 | simpler, but can lead to fragmentation. |
257 | |
258 | _____________________________________________________________________________ |
259 | |
260 | INITIALISATION |
261 | |
262 | |
263 | RISC_OSLib (and STEEL) programs start off with a huge chunk of |
264 | initialisation code, most of which is really boring: |
265 | |
266 | wimpt_init("progname"); |
267 | dbox_init(); |
268 | win_init(); |
269 | /* ... */ |
270 | |
271 | Sapphire gets rid of all that by using a neat feature of AOF. Each |
272 | library component, or `unit' as we called them, contains a four-word |
273 | table entry. The linker collects the entries together, and the kernel |
274 | can examine them all to see which units are linked in. Each entry |
275 | contains: |
276 | |
277 | * The address to store the unit's workspace offset. |
278 | * The size of workspace required. |
279 | * Minimum acceptable Scratchpad size (if more than 256 bytes). |
280 | * Address of initialisation procedure. |
281 | |
282 | By convention, the first word of a unit's workspace contains flags, bit |
283 | zero of which indicates that the unit has been initialised. A unit's |
284 | initialisation procedure performs the following actions: |
285 | |
286 | * It checks it's not already initialised. |
287 | * It initialises anything it depends on (this causes the required |
288 | units to be linked too). |
289 | * It initialises itself. |
290 | * It sets its initialised flag and returns. |
291 | |
292 | Library initialisation is a two-step process. The first step, |
293 | `sapphire_init', initialises the kernel and the basic memory map. The |
294 | second step, `sapphire_libInit', initialises the library units. |
295 | |
296 | _____________________________________________________________________________ |
297 | |
298 | DYNAMIC LINKING |
299 | |
300 | |
301 | The original idea was that Sapphire would be really small. Well, take a |
302 | look at its feature set and tell me that it's big. So we paid no |
303 | attention to making Sapphire dynamically linkable. When it became |
304 | obvious that this wasn't working properly, I started investigating |
305 | methods for making Sapphire into a DLL. |
306 | |
307 | I was saved by two features of Sapphire code: |
308 | |
309 | * Sapphire programs preserve R11 everywhere. |
310 | * Sapphire's kernel allocates workspace for the units at run-time. |
311 | |
312 | The first fact gives me re-entrancy automatically. R11 becomes the one |
313 | thing that the dynamically-linked Sapphire library can use to decide |
314 | where its workspace is. |
315 | |
316 | Finding workspace is rather grubby in Sapphire, so it's hidden beneath a |
317 | macro. For the curious, this is what happens: |
318 | |
319 | LDR R12,workoff_addr |
320 | LDR R14,[R11,#-magic_offset] |
321 | ADD R12,R14,R12 |
322 | |
323 | The trick works because the allocation of workspace /offsets/ depends |
324 | only on the order of units within the library, so they can be shared; |
325 | the value loaded from R11-space is worked out by the kernel and put in |
326 | application memory on startup. |
327 | |
328 | Most of the kernel ends up in the DLL. The application is linked |
329 | against a really small stub, which basically contains front ends for a |
330 | few kernel functions to (maybe) find the Sapphire DLLs, point to a |
331 | collection of tables, and call the corresponding function in the core. |
332 | |
333 | The exact mechanism is complicated. Interested readers are referred to |
334 | the source code. |
335 | |
336 | The upshot of all of this is that, although SDLS was designed |
337 | specifically to solve the problem of APCS shared libraries, Sapphire's |
338 | style of shared library requires much less code to be linked into the |
339 | client, much thinner veneering (because the kernel handles re-entrancy), |
340 | and the result is both more robust and cleaner. |
341 | |
342 | _____________________________________________________________________________ |
343 | |
344 | ERROR HANDLING |
345 | |
346 | |
347 | Sapphire programs know about two sorts of errors: |
348 | |
349 | * Program errors, which should never occur. |
350 | * Environmental errors, which must be anticipated. |
351 | |
352 | Environmental errors are reported by setting the V flag on exit from a |
353 | function. Sapphire routines are documented according to whether they |
354 | can return errors: it is safe to ignore the V flag on exit from a |
355 | routine whose documentation does not state that it returns errors. An |
356 | environmental error typically requires some sort of user intervention, |
357 | and suggests a friendly explanation that some sort of fault has |
358 | occurred. |
359 | |
360 | Program errors are raised by OS_GenerateError and caught by Sapphire's |
361 | error handler. There's a rule of thumb which states: `Never test for an |
362 | error you can't handle.' Sapphire follows this advice. Most calls to |
363 | the operating system have the X bit clear, causing errors to be sent to |
364 | the error handler. Program error messages are intentionally technical. |
365 | The idea is to encourage naive users to write them down, so that they |
366 | remember them and report them accurately. |
367 | |
368 | Part of the initial design was that a Sapphire application should |
369 | /never/ exit as a result of program fault. A user should always be able |
370 | to try to get to a save box and save data. (Too much experience of |
371 | Paint falling over with type 5s prompted this decision.) The |
372 | application pops up an error report giving the user a choice between |
373 | killing the program or trying to soldier on. Soldiering on seemed |
374 | remarkably successful in general. |
375 | |
376 | A later addition to Sapphire was `Structured Exception Handling', which |
377 | is a lower-level version of C++'s try/throw/catch exception handling. |
378 | This builds upon the existing base, and allows programs to tidy up in |
379 | the event of errors before propagating them back down to the bottom |
380 | level `Continue/Quit' handler. |
381 | |
382 | |
383 | This concludes the tour of Sapphire's run-time support functionality. |
384 | The rest describes plain ol' library features. |
385 | |
386 | _____________________________________________________________________________ |
387 | |
388 | EVENT HANDLING |
389 | |
390 | |
391 | Sapphire has a fairly simple but highly effective strategy for dealing |
392 | with events, inspired in part by the FilterManager module in RISC OS 3. |
393 | |
394 | The `event' unit is responsible for low-level event handling. |
395 | Applications call `event_poll' instead of calling the SWI Wimp_PollIdle |
396 | directly; the calling sequences are almost the same for both routines. |
397 | `event_poll' returns C set if it handled the event itself, and C clear |
398 | if it failed to handle it. A defalt handler procedure is provided to |
399 | perform any required actions for a particular event. A Sapphire |
400 | application's poll loop typically looks like this: |
401 | |
402 | loop BL event_poll |
403 | BLCC handle_unknowns |
404 | BLCC defHandler |
405 | B loop |
406 | |
407 | and that's it. |
408 | |
409 | Most of the hard work is done by filters. Sapphire knows about three |
410 | types of filters: |
411 | |
412 | * Pre-filters are shown arguments to Wimp_Poll before it's called, and |
413 | can modify them in controlled ways, e.g., clearing event mask bits, |
414 | or changing the `time-to-return' argument. A pre-filter can |
415 | `claim' a call to Wimp_Poll by storing a `fake' event in the buffer |
416 | and returning carry set. Other pre-filters are skipped, and the |
417 | fake event is passed to the post-filters. |
418 | |
419 | * Post-filters are shown the result from a call to Wimp_Poll. They |
420 | can perform any action they like, except for modifying the event. A |
421 | post-filter can claim an event by returning C set; other |
422 | post-filters are not called, and `event_poll' returns C set. |
423 | |
424 | * Fake-event filters are a novel idea introduced to handle transient |
425 | dialogue boxes. Immediately after a return from Wimp_Poll, Sapphire |
426 | scans the list of fake-event handlers. Each one is entitled to |
427 | `steal' this event and replace it with another by returning carry |
428 | set. Once an event has been stolen, it is passed through the |
429 | post-filters. When `event_poll' is called again, Sapphire |
430 | restores the previous genuine event and continues calling fake-event |
431 | filters from the point at which it left off. Thus, Sapphire |
432 | provides a general mechanism for inserting events. |
433 | |
434 | Everything else is based off this mechanism. |
435 | |
436 | The `win' unit dispatches window-specific events to window handlers by |
437 | registering a post-filter. The `menu' unit also establishes a |
438 | post-filter, and passes events to the owner of the current menu. |
439 | |
440 | The main use for fake-event handlers is to handle transient windows. A |
441 | separate Sapphire unit, `transWin', remembers the handle of the current |
442 | transient window. When an event arrives, its fake-event handler checks |
443 | whether the transient window is open. If it has been closed, it steals |
444 | the event, whatever it was, and substitutes a fake `window close' event |
445 | to be picked up by the owner of the transient window. |
446 | |
447 | [Actually, `transWin' is careful to allow redraw events through |
448 | unmolested. Bad window flicker results if redraw requests are tampered |
449 | with.] |
450 | |
451 | _____________________________________________________________________________ |
452 | |
453 | MENUS IN SAPPHIRE |
454 | |
455 | |
456 | Sapphire knows about two types of menus, and its interface to |
457 | applications reflects this. Standard RISC OS transient menus will be |
458 | familiar to most readers. Straylight tearoff menus will be less well |
459 | known -- see Glass for a real-life example of tearoff menus in action. |
460 | |
461 | Sapphire's interface to handling menus is inspired by Computer Concepts' |
462 | Advanced Box Interface system. Menus are always constructed |
463 | dynamically, as needed. The procedure `menu_create' is given four |
464 | arguments: |
465 | |
466 | * The address of a menu definition. |
467 | * The address of a menu event handler. |
468 | * R10 and R12 values to pass to the handler. |
469 | |
470 | The `menu_create' procedure may be called any number of times before the |
471 | next Wimp_Poll: the menu definitions are concatenated. This allows |
472 | applications to create dynamic and context-sensitive menus easily. |
473 | |
474 | The first `menu_create' call is different from the others: the menu |
475 | definition must contain a menu title. (It need not contain any menu |
476 | items, though.) Both the menu title and the items are /variable size/ |
477 | objects -- the more odd features a menu item has, the more space its |
478 | definition occupies. Menu definitions are also /read-only/. Dynamic |
479 | features like ticking and shading must be handled separately. |
480 | |
481 | Each menu item (and title) is begun with a `feature mask' -- a word |
482 | containing a flag bit for each feature. The feature mask is followed by |
483 | data appropriate to the various features selected. For example, the |
484 | `has a submenu' feature requires a pointer to the submenu definition and |
485 | a pointer to the procedure which will handle events for the submenu. |
486 | |
487 | Menu features which require dynamic information (e.g., context-sensitive |
488 | message strings, or ticking and shading) have, as part of their feature |
489 | data, offsets from the menu handler's R10 containing the required |
490 | information. The `shade item' feature requires an offset from R10 and a |
491 | bit position -- it will shade the item when the appropriate bit in the |
492 | word at [R10, #offset] is set. Sapphire's menu system understands the |
493 | concept of `radio items' -- groups of items of which only one may be |
494 | ticked at a time. The feature data for a radio item consists of an R10 |
495 | offset and a value to match -- the application then sets [R10, #offset] |
496 | to be (for example) 0 for the first item in the group, 1 for the second, |
497 | and so on. |
498 | |
499 | It's important to get the hang of the concepts here, because lots of |
500 | other bits of Sapphire use this same idea. |
501 | |
502 | The `menuDefs.sh' header file contains a large number of macros which |
503 | hide most of the mess of this. |
504 | |
505 | _____________________________________________________________________________ |
506 | |
507 | DIALOGUE BOXES |
508 | |
509 | |
510 | Sapphire's dialogue box handling is extremely powerful. There's also a |
511 | lot of code in `dbox.s' which does jobs that the WindowManager ought to |
512 | do, or does wrong. |
513 | |
514 | Dialogue boxes are `resources'. Sapphire tries to keep the |
515 | interface to handling resources consistent. When you want one, you |
516 | `create' it, and when you don't need it any more, you `destroy' it. |
517 | A successful `create' of a dialogue box yields a `dbox' -- a handle |
518 | which represents the dialogue box. |
519 | |
520 | Sapphire is carefully constructed so that dialogue boxes don't need to |
521 | exist unless they're actually in use. Most programs create dialogue |
522 | boxes only when they need to appear on-screen, and destroy them again |
523 | once they're closed. To this end, Sapphire generates fake close events |
524 | for transient windows. |
525 | |
526 | Dialogue boxes can be created from several different sources of |
527 | information, and to handle this, there are actually three different |
528 | `create' routines: |
529 | |
530 | * `dbox_create' is the standard call. A client passes a string naming |
531 | the template from which to create the dialogue, and the dialogue box |
532 | manager returns a handle. The window definition is copied, along |
533 | with all the indirected data, so that you can create multiple |
534 | instances of a template without tripping over aliasing problems. |
535 | |
536 | * `dbox_fromEmbedded' creates a dialogue from an `embedded template' |
537 | -- a compact and easily expanded representation of a window template |
538 | which is statically linked into the client program. Again, the data |
539 | is copied from the template, so aliasing problems don't exist. |
540 | |
541 | * `dbox_fromDefn' is the most powerful call (and the others are |
542 | implemented in terms of it). The client passes a pointer to a |
543 | complete window definition, which the Sapphire uses `as is', |
544 | without copying it. |
545 | |
546 | Embedded templates were introduced for very small programs (e.g., |
547 | DLLMerge). Sapphire makes use of them because common dialogue boxes |
548 | have been stored in a shared `Sapphire.Resources' DLL as embedded |
549 | templates. This makes programs easier to upgrade for new versions of |
550 | Sapphire (just copy the new DLLs in and you get the new dialogues free) |
551 | and reduces resource requirements since there's only one copy in memory |
552 | at any given time. |
553 | |
554 | Displaying dialogue boxes introduces more flexibility. The procedure |
555 | `dbox_open' is given a dbox and an `open style', and a collection of |
556 | arguments appropriate to the style. Styles permitted are: |
557 | |
558 | * In its current position (or in the position it was in the template |
559 | file). |
560 | |
561 | * In the middle of the screen. |
562 | |
563 | * Centred over the mouse pointer. |
564 | |
565 | * At a given Y coordinate (but the current X coordinate). |
566 | |
567 | * At a given X and Y position. |
568 | |
569 | The open style also contains a `transient or persistent' flag, which is |
570 | useful for dialogue boxes hanging off of menu items. If a menu is open |
571 | currently, Sapphire will automatically open the dbox as a submenu, in |
572 | the correct place, unless you explicitly instruct it not to do this |
573 | using another open style bit. |
574 | |
575 | Sapphire doesn't pass Wimp events directly on to client event handlers. |
576 | Instead, it pre-processes them, putting the important information from |
577 | the event into registers, and sending a `dbox event' to the handler. |
578 | The handler can then either `claim' the event for itself (by saying that |
579 | it's handled it) or pass it on. Unclaimed events are acted upon by |
580 | Sapphire. |
581 | |
582 | As an example of the sort of preprocessing Sapphire does for dialogue |
583 | box events, it breaks a redraw request into an individual event for each |
584 | rectangle. If its redraw events are unclaimed (as they ought to be), it |
585 | will call Sculptrix to fill in the 3D borders around the icons. |
586 | |
587 | Sapphire handles radio buttons itself, if click events on them are |
588 | unclaimed. Using type-11 buttons and a non-zero ESG means that the user |
589 | can deselect all buttons in a set by clicking the selected one with |
590 | `adjust'. An application can deal with this by selecting the icon |
591 | explicitly, but (a) this causes flicker, and (b) if the application is |
592 | going to the trouble of having explicit code to deal with the situation, |
593 | it may as well do the job properly. Sapphire considers an icon with |
594 | button type 3 and non-zero ESG to be a `Sapphire radio button' and does |
595 | the right thing with it. |
596 | |
597 | Shaded icons are also handled by Sapphire rather than the Wimp, because |
598 | the standard behaviour is highly inconsistent. For example, sometimes |
599 | icon backgrounds are shaded in addition to foregrounds. In text-and- |
600 | sprite icons, the text is /not/ shaded when it ought to be (for |
601 | consistency with text-only icons). It's basically a mess. Sapphire |
602 | uses a complicated shading algorithm which seems fairly close to the |
603 | behaviour of ABI. |
604 | |
605 | Sapphire also handles caret blinking and cursor changing over writable |
606 | icons automatically. |
607 | |
608 | Keypresses also receive default handling from Sapphire. Unclaimed |
609 | cursor keys cause the caret to move between writable icons, scrolling |
610 | the window where necessary (both horizontally and vertically) to ensure |
611 | that the new focus icon is visible. |
612 | |
613 | The procedure to set a string in an indirected icon is probably |
614 | excessive. It ensures that the string really needs changing before |
615 | flickering the icon. It truncates the string rather than overflowing |
616 | the icon's buffer, discarding either the start or the end depending on |
617 | the icon's `right align' flag, optionally putting a `...' to indicate |
618 | that a truncation was performed. |
619 | |
620 | Sapphire inherited a curious feature called `embedded titles' from |
621 | STEEL, which in turn was inspired by RISC OS version of the game |
622 | `Elite'. A client nominates an icon within a window which has no title |
623 | bar as being an `embedded title'. Sapphire will thereafter |
624 | automatically draw a Sculptrix group box around this icon, using the |
625 | window's real title text as the group title. This is used in `Info' |
626 | windows, as well as the warning, note and error windows. |
627 | |
628 | Sapphire can handle `nonpolling' windows. In some cases it's desirable |
629 | to arrest the user's attention by requiring acknowledgement of a message |
630 | or the taking of a decision, and sometimes continuing to poll would be |
631 | dangerous or undesirable for other reasons. For example, a serious |
632 | error report mustn't multitask, because the error might recur. The `To |
633 | save...' message caused by attempting to save a file without a full |
634 | pathname shouldn't multitask, because the click on the OK button will |
635 | close the menu tree (which is extremely irritating). The only way to |
636 | handle a prequit message under RISC OS 2 is to ask the user whether |
637 | quitting is permitted in a nonpolling way, so the decision to cancel a |
638 | shutdown sequence can be made before an acknowledgement is returned to |
639 | the task manager. The handling for nonpolling windows is split into two |
640 | parts. The `nopoll' unit displays dialogue boxes and reports mouse |
641 | clicks and keypresses as fake events through Sapphire's pre-handler |
642 | mechanism (so nonpolling dialogues present an identical programmer |
643 | interface to normal ones). The `buttons' unit displays a given |
644 | dialogue, permitting a collection of action buttons to be given |
645 | appropriate pieces of text. |
646 | |
647 | The real richness of Sapphire's dialogue box handling comes from custom |
648 | controls. A whole subsystem, `dbx' is provided for dealing with custom |
649 | controls. |
650 | |
651 | The client registers a table defining the required controls for a |
652 | particular dialogue box. The table contains variable size entries, each |
653 | one beginning with a standard header: |
654 | |
655 | * Number of the icon which hosts the control. |
656 | * Pointer to the control definition. |
657 | * Various flags (control specific). |
658 | * The offset of the control's writable data within the dialogue |
659 | handler's R10-space. |
660 | |
661 | This is followed by whatever constant data the control handler requires. |
662 | Sapphire dbx controls are always hosted by icons. This makes them easy |
663 | to lay out in template editors. |
664 | |
665 | Custom controls generate their own events which are sent to the client's |
666 | handler procedure. They are also given dialogue box events (after the |
667 | client has had a chance to claim them for itself) as `dbx events'. For |
668 | example, Sapphire will only ask a control to draw itself if (a) the |
669 | control requests redraw events and (b) the current rectangle intersects |
670 | the control's host icon. |
671 | |
672 | Sapphire takes redrawing of controls very seriously. It ensures that |
673 | the graphics clipping area is set to the intersection of the control's |
674 | host icon and the redraw rectangle, to prevent any `accidents' from |
675 | messing up the rest of the display. It passes this amended rectangle to |
676 | the control to allow it to perform source-level clipping. The custom |
677 | RGB-coloursquare and HSV-colourcircle controls in the (unfinished) |
678 | colour selector use the provided rectangle to plot the new graphics by |
679 | direct screen access. |
680 | |
681 | Predefined custom controls are: |
682 | |
683 | * A slider. This is written very carefully to avoid any flicker while |
684 | the bar is being dragged or updated. |
685 | |
686 | * A `bump' arrow button, which presses its icon in while it's being |
687 | held down, and autorepeats. The arrow control keeps a count of how |
688 | many `ticks' have been missed, and sends them to the dialogue box as |
689 | large packets, rather than individually. This makes programs which |
690 | spend a long time processing bump requests feel responsive even when |
691 | they're really being slow. |
692 | |
693 | * A file icon, which represents a filetype in a window. File icons |
694 | can optionally be draggable (for save operations). When solid drags |
695 | are requested by the user (and the DragASprite module is available), |
696 | the control will make the icon disappear while the icon is being |
697 | dragged, to produce the illusion of the icon being `lifted off' the |
698 | dialogue box. |
699 | |
700 | * A `string set' control, which drops down a menu of strings and |
701 | allows the user to choose one, which it displays. |
702 | |
703 | * A simple `colour pot' which allows a user to choose one of the |
704 | sixteen standard Wimp colours. This control pops open a small |
705 | dialogue box of its own when requsted. |
706 | |
707 | Custom controls provide a great deal of functionality at very little |
708 | programmer effort. Why no other library for RISC OS handles them |
709 | properly is a mystery to me. The Toolbox has a go, but fails miserably; |
710 | compared to the power of Sapphire's `dbx' subsystem, it might as well |
711 | not bother. |
712 | |
713 | (Sapphire was mature when the Toolbox betas were available. The idea |
714 | for proper custom controls came from OS/2, but the implementation is |
715 | very much in Sapphire's own style.) |
716 | |
717 | _____________________________________________________________________________ |
718 | |
719 | DATA TRANSFER |
720 | |
721 | |
722 | It used to be the case that data transfer was one of the really hard |
723 | bits of an application. I remember trying to put it off as long as |
724 | possible whenever I wrote a STEEL application. Sapphire tries very hard |
725 | to make data transfer as painless as possible. |
726 | |
727 | Both loading and saving have two layers to them. Underneath, there's a |
728 | low-level interface which exposes the nature of the transfer (whether |
729 | it's to a file, or in-memory). Above that, there's an abstract |
730 | interface which presents both transfer types as a (non-seekable) stream |
731 | of data, providing buffering and a simple programming interface. This |
732 | high-level interface works perfectly well on files even without a data |
733 | transfer context, and occasionally gets (ab)used as a general stream |
734 | interface to file data, for example, when saving preferences files. |
735 | |
736 | The lower-level code was designed to be useful both in conjunction with |
737 | the high-level interfaces and on its own. Often, the low-level |
738 | interface is the better choice. |
739 | |
740 | Oddly, the low-level interface is higher-level than RISC_OSLib's. |
741 | |
742 | A low-level data save operation is initiated by the `save' call. It |
743 | requires a large number of arguments, detailing the recipient's window |
744 | handle, the filename, filetype, and a block of handling routines. The |
745 | handling routines are: |
746 | |
747 | * Save: write your data to a file, given its name. Return an error if |
748 | saving failed. |
749 | |
750 | * Send: return (giving a base pointer and size) a block to send to the |
751 | recipient. Return CS if this is the last block, or CC if there's |
752 | more to come. The first time this handler is called, it's given the |
753 | value 0 in R2; after that, the return value of R2 is passed back in |
754 | the next call, to provide some sort of context. It's called over |
755 | and over until either the recipient complains or the handler returns |
756 | end-of-data. |
757 | |
758 | * Success: the data was transferred successfully (and may or may not |
759 | be permanently safe). |
760 | |
761 | * Failed: the data transfer failed, either because of a local error, |
762 | or because the receiver failed. |
763 | |
764 | Note that the `send' handler can return a block of any size. It's |
765 | Sapphire's responsibility to handle the recipient's buffer size. In the |
766 | case where all the data is in one big flex block, the send routine |
767 | becomes trivial. Sapphire makes RAM transfer /easier/ than writing to a |
768 | file! |
769 | |
770 | Loading data in similar in spirit. There's a single routine, `load', |
771 | which is given a block of handlers: |
772 | |
773 | * InitBuf: create a buffer in which to receive data, and perform any |
774 | other setting up necessary to start a RAM transfer. The handler is |
775 | given an estimated file size, and the proposed leafname. |
776 | |
777 | * KillBuf: RAM transfer has failed, so destroy the buffer (if it was |
778 | only temporary). This is called when RAM transfer was unsuccessful |
779 | for some reason. |
780 | |
781 | * Extend: the current buffer was filled: I need a new one. (It's |
782 | called `extend' because the normal reaction is to extend a flex |
783 | block and return the new area.) |
784 | |
785 | * DoneBuf: RAM transfer has succeeded, so do whatever's necessary. |
786 | (Typically, an application might free some `slop space' at the end |
787 | of the buffer.) |
788 | |
789 | * File: load the data from a given file. |
790 | |
791 | * Done: data transfer succeeded. |
792 | |
793 | * Failed: data transfer failed, either because of something we did |
794 | wrong or because the sender had problems. |
795 | |
796 | Sapphire provides routines to do most of the work of the InitBuf, |
797 | KillBuf, Extend, DoneBuf and File handlers in the case where we're just |
798 | loading directly into a flex block, and in many cases, the |
799 | supplied KillBuf, Extend and DoneBuf routines can be used directly. |
800 | |
801 | The higher-level interfaces `xsave' and `xload' are designed to link |
802 | onto the low-level interfaces, and provide handlers which can (in many |
803 | cases) be given directly to the low-level calls. They take as an |
804 | argument a tranfer routine, which is called to do the actual save or |
805 | load operation. It is called just like a normal subroutine. When it |
806 | returns, the transfer is ended. It calls `xsave' or `xload' routines |
807 | to read and write bytes, words, strings or whole blocks of memory; |
808 | Sapphire buffers data where this would be useful. Sapphire provides a |
809 | separate stack for the transfer routine, so that the read or write |
810 | routines can pause for a Wimp_Poll to send or receive more data by RAM |
811 | transfer, although this is invisible to the transfer routine, which just |
812 | sees a simple interface to a stream of data. Errors from the transfer |
813 | are carefully propagated back to the transfer routine, which sees error |
814 | returns from read or write operations. |
815 | |
816 | Sapphire includes a standard `saveAs' box, whose interface is slightly |
817 | simpler than the `save' routine. |
818 | |
819 | _____________________________________________________________________________ |
820 | |
821 | THE SAPPHIRE CHUNK FORMAT, AND PREFERENCES HANDLING |
822 | |
823 | |
824 | A Sapphire `chunk' file is very simply a file representation of a list |
825 | of named chunks of data. Sapphire provides functions to read chunk |
826 | files, claim chunks, and write chunk files back again. |
827 | |
828 | When a chunk file is loaded, each chunk is placed in a Flex block. A |
829 | client can `claim' a chunk. Once this is done, the client can either |
830 | modify the data in-place, or free the block and provide a pointer to a |
831 | procedure to write equivalent data to the chunk file when it's saved. |
832 | |
833 | The original idea was to support preferences files in a modular way -- |
834 | each component could find its own preferences chunk without needing to |
835 | know about anything other than the chunk system. We noticed that this |
836 | could easily be extended to handle binary data, and we could base a |
837 | general file format on the idea. A typical Straylight document would |
838 | look something like this: |
839 | |
840 | ; |
841 | ; Generated by Straylight Frobnitz |
842 | ; |
843 | |
844 | [Header] |
845 | Format = Frobnitz data |
846 | FormatVersion = 3.15 |
847 | Author = J. R. Hacker |
848 | |
849 | [FrobData] |
850 | Bin[00]....[binary rubbish] |
851 | |
852 | [FrobOptions] |
853 | AutoDelay = 300 |
854 | AutoMods = 50 |
855 | ... |
856 | |
857 | You get the idea. The text chunks can be modified by anyone with a text |
858 | editor -- the binary areas are self-contained and don't care where they |
859 | are in a file. |
860 | |
861 | Some user preferences are best stored as raw chunks. For example, the |
862 | `FontDir' program keeps a chunk containing the list of font directories |
863 | known to it. Most are better expressed through the sort of key/value |
864 | syntax shown in the above example. Sapphire provides a separate |
865 | mechanism for handling these which interwork with the chunk file system. |
866 | |
867 | The `options' unit will, when requested, claim a chunk from a chunk |
868 | file, and translate its textual contents into an easily digestable |
869 | binary format. When the chunk file is saved, Sapphire will |
870 | automatically read the binary data, and translate whatever's there back |
871 | into text. Using text as an external representation insulates the |
872 | client program from version changes -- default values can be provided |
873 | for keys which aren't defined, for example. |
874 | |
875 | Each key/value pair in the chunk has the general format: |
876 | |
877 | <key> [=] <string> [;<comment>] |
878 | |
879 | A <string> is either a sequence of unquoted characters, or quoted by one |
880 | of the pairs `', '', or "". A quote character can be duplicated to |
881 | insert a literal quote in the string. Any of the characters `#', `|' or |
882 | `;' begins a comment (although `;' is preferred because we're assembler |
883 | programmers). |
884 | |
885 | The set of permissable options is provided to Sapphire as a table, each |
886 | entry of which states the name of the key, the offset within the options |
887 | block to store the (binary) value, the type of the value, and any data |
888 | the type handler needs for its own purposes. It's another |
889 | variable-size-entry table, like a menu definition or a dbx control list. |
890 | More types can easily be written by client programs. Standard handlers |
891 | are provided: |
892 | |
893 | * The string type reads quoted or unquoted strings. It always writes |
894 | quoted strings. |
895 | |
896 | * The integer type reads signed or unsigned integers in any base (up |
897 | to 36). It usually writes decimal numbers although it can be |
898 | persuaded to use a different base. |
899 | |
900 | * The literal type never reads anything. It writes out a string |
901 | (maybe some kind of comment) when requested. |
902 | |
903 | * The enum type reads a string, looks it up in a table, and stores the |
904 | index. The match is case-insensitive. Abbreviations are |
905 | accepted; ambiguous abbreviations match the first string found in |
906 | the table. It writes out the full correct-case string. |
907 | |
908 | * The bool type is a special case of the enum type -- it reads either |
909 | one of `true', `on' or 'yes' for a true value, or `false', `off' or |
910 | `no' for false, and sets a single bit for its answer (for space |
911 | efficiency). On output, it writes one of `true' or `false', unless |
912 | requested to use `on' or `off', in which case it also suppresses the |
913 | optional `=' sign on output. |
914 | |
915 | * The version type reads version numbers of the form `xxx.yz' and |
916 | stores them as integers, multiplied by 100. It writes them out |
917 | in the same form, with trailing zeros if necessary. |
918 | |
919 | The preferences system DoggySoft added into WimpExtension is the nearest |
920 | thing which comes close, but Sapphire does the job in a much more |
921 | powerful way. This is actually quite surprising, since the unit isn't |
922 | particularly large. (This is partly due to an interesting coding trick |
923 | I came up with to reduce typing, which I've entirely failed to make use |
924 | of in any high-level language. See `choices/options.s' for details.) |
925 | |
926 | _____________________________________________________________________________ |
927 | |
928 | HIGH LEVEL LANGUAGE INTERFACE |
929 | |
930 | |
931 | This is stil very sketchy, but a discussion may interest some readers. |
932 | The C interface has to deal with several problems: |
933 | |
934 | * Sapphire's procedure calls aren't even a little bit compatible with |
935 | APCS. Sapphire gains a lot of its power and elegance from its lax |
936 | calling conventions. |
937 | |
938 | * C programmers tend to use different names for commonly used |
939 | functions. |
940 | |
941 | * Sapphire's variable-size-entry tables aren't easy to represent in C. |
942 | |
943 | The last problem hasn't been addressed. The trivial `Hello, world' |
944 | example program contains a horrible block of hex where the menu |
945 | definition is meant to be. To be fair, we envisaged the user-interface |
946 | parts of Sapphire applications to be in assembler, to keep the |
947 | user-facing parts of a program light and responsive, while the back-end |
948 | bits were in C. |
949 | |
950 | The second problem just involves writing a lot of veneers and playing |
951 | with macros. I didn't bother trying to make the functions ANSI |
952 | compliant. Only the bare bones of a standard library is provided. |
953 | |
954 | The first is the only technically interesting problem. Making C call |
955 | Sapphire is a fairly simple task of inserting a veneer in the right |
956 | place. I wrote a `_call' function which works rather like `_swi', to |
957 | allow C to call any Sapphire function. There's a small problem to do |
958 | with R11 which needs dealing with, though. My solution was simple -- |
959 | since Sapphire doesn't have any stack checking anyway, compile code with |
960 | stack limit checking turned off and move R11 into R10 (APCS's stack |
961 | limit pointer) and back to R11 on inter-language borders. |
962 | |
963 | The really interesting problem, then, is making Sapphire understand how |
964 | to call C functions. Again, some hacking is performed. Any C function |
965 | which is intended to be called by Sapphire is declared as follows: |
966 | |
967 | __sapph(funcname)(regset *r [, type *r10 [, type *r12 [, char *scratch]]]) |
968 | |
969 | (APCS's way of passing arguments in registers allows the flexibility of |
970 | arguments -- if a function doesn't need R10 or R12 it doesn't need to |
971 | declare them. More arguments can be read by putting them after |
972 | `scratch' -- they just get pulled off the stack.) |
973 | |
974 | The macro constructs a function with the given name, just before the |
975 | actual compiled function, whose name is changed to `__sapph__funcname'. |
976 | |
977 | The actual code generated by the compiler for this atrocity is: |
978 | |
979 | funcname STMFD R13!,{R14} |
980 | MOV R14,PC |
981 | B __sapph_veneer |
982 | |
983 | __sapph__funcname |
984 | <APCS entry sequence> |
985 | ... |
986 | |
987 | The `__sapph_veneer' routine knows that the function it's meant to call |
988 | is pointed to by R14. It stores all the registers on the stack, points |
989 | R0 at them (to make the regset variable), sets up the other arguments, |
990 | puts R11 into R10, and calls the C routine. On exit, it pulls the |
991 | registers off the stack, puts R11 back in its proper place, sets |
992 | flags appropriately, and returns. |
993 | |
994 | The C function can set return values by modifying the `regset' argument |
995 | it's passed. It can also modify the flags by returning an appropriate |
996 | value as its result. For example |
997 | |
998 | return (C_set | V_clear); |
999 | |
1000 | returns with C set, V clear and the other flags unaltered. |
1001 | |
1002 | This is a very hamfisted way of dealing with the problem. But Sapphire |
1003 | was never designed to work with C. In fact, it's almost true to say |
1004 | that Sapphire was designed never to work with C -- the fact that it's |
1005 | possible after all is more a credit to humen ingenuity than Sapphire's |
1006 | design. |
1007 | |
1008 | _____________________________________________________________________________ |
1009 | |
1010 | CONCLUSION |
1011 | |
1012 | |
1013 | Your tour through the interesting bits of Sapphire is complete. There |
1014 | are plenty of other `neat toys' in there, waiting for you to find them. |
1015 | Most source files contain some sort of interesting twist on an old idea, |
1016 | or a neat programming trick. |
1017 | |
1018 | The sources are commented to excess. Almost every line contains a |
1019 | comment. Obscure tricks and other bits of non-obvious code are usually |
1020 | tagged `Horrid hack' or `Neat trick', at least the first time we used |
1021 | that particular construction. Searching for `hack' should produce lots |
1022 | of interesting code to read. |
1023 | |
1024 | Anything else you want to know: just read the header file documentation |
1025 | and/or the source code. Or you can ask me. Just bear in mind that it's |
1026 | been a long time since I wrote anything which used Sapphire and I'll |
1027 | probably have to read the headers and/or the source code anyway. |
1028 | |
1029 | Sapphire, for Tim and myself, has been a labour of love. I'm extremely |
1030 | proud of it: of its small size, its speed, its elegant design, its power |
1031 | and versatility. Please, treat it with respect. |
1032 | |
1033 | I'm almost in tears as I type this, so I'd better stop. Thanks for |
1034 | reading, |
1035 | |
1036 | |
1037 | |
1038 | Mark Wooding, mdw@excessus.demon.co.uk |
1039 | 17 December 1997 |