/
ch08_architecture.pod
1353 lines (1159 loc) · 65.1 KB
/
ch08_architecture.pod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
=pod
=head0 Parrot Internals
Z<CHP-7>
This chapter details the architecture and internal workings of Parrot,
and attempts to explain how Parrot has been designed and how it operates.
As we've mentioned before, Parrot is a register-based, bytecode-driven,
object-oriented, multithreaded, dynamically typed, self-modifying,
asynchronous interpreter. That may seem awfully confusing when you first
look at it, but the design fits together remarkably well.
=head1 Core Design Principles
Z<CHP-7-SECT-1>
X<Parrot;internals>
Three main principles drive the design of
X<Parrot;core design principles> Parrot: speed, abstraction, and
stability.
I<Speed> is a paramount concern. Parrot absolutely must be as fast as
possible, since the engine effectively imposes an upper limit on the
speed of any program running on it. It doesn't matter how efficient
your program is or how clever your program's algorithms are if the
engine it runs on limps slowly along. While Parrot can't make a poorly
written program run fast, it could make a well-written program run
slowly, a possibility we find entirely unacceptable.
Speed encompasses more than just raw execution time. It extends to
resource usage. It's irrelevant how fast the engine can run through
its bytecode if it uses so much memory in the process that the system
spends half its time swapping to disk. While we're not averse to using
resources to gain speed benefits, we try not to use more than we need,
and to share what we do use.
I<Abstraction> indicates that things are designed such that there's a
limit to what anyone needs to keep in their head at any one time. This
is very important because Parrot is conceptually very large, as you'll
see when you read the rest of the chapter. There's a lot going on, too
much to keep the whole thing in mind at once. The design is such that
you don't have to remember what everything does, and how it all works.
This is true regardless of whether you're writing code that runs on
top of Parrot or working on one of its internal subsystems.
Parrot also uses abstraction boundaries as places to cheat for speed.
As long as it I<looks> like an abstraction is being completely
fulfilled, it doesn't matter if it actually I<is> being fulfilled,
something we take advantage of in many places within the engine. For
example, variables are required to be able to return a string
representation of themselves, and each variable type has a "give me
your string representation" function we can call. That lets each
class have custom stringification code, optimized for that particular
type. The engine has no idea what goes on beneath the covers and
doesn't care--it just knows to call that function when it needs the
string value of a variable. Objects are another good case in
point--while they look like nice, clean black boxes on the surface,
under the hood we cheat profoundly.
I<Stability> is important for a number of reasons. We're building the
Parrot engine to be a good backend for many language compilers to
target. We must maintain a stable interface so compiled programs can
continue to run as time goes by. We're also working hard to make
Parrot a good interpreter for embedded languages, so we must have a
stable interface exposed to anyone who wants to embed us. Finally, we
want to avoid some of the problems that Perl 5 has had over the years
that forced C extensions written to be recompiled after an upgrade.
Recompiling C extensions is annoying during the upgrade and
potentially fraught with danger. Such backward-incompatible changes
have sometimes been made to Perl itself.
=head1 Parrot's Architecture
Z<CHP-7-SECT-2>
The X<architecture;Parrot>
X<Parrot;architecture>
Parrot system is divided into four main parts, each with its own
specific task. The diagram in A<CHP-7-FIG-1>Figure 7-1 shows
the parts, and the way source code and control flows through Parrot.
Each of the four parts of Parrot are covered briefly here, with the
features and parts of the interpreter covered in more detail
afterward.
=begin figure Parrot's flow
Z<CHP-7-FIG-1>
F<figs/p6e_0801.png>
=end figure
The flow starts with source code, which is passed into the parser
module. The parser processes that source into a form that the compiler
module can handle. The compiler module takes the processed source and
emits bytecode, which Parrot can directly execute. That bytecode is
passed into the optimizer module, which processes the bytecode and
produces bytecode that is hopefully faster than what the compiler
emitted. Finally, the bytecode is handed off to the interpreter
module, which interprets the bytecode. Since compilation and execution
are so tightly woven in dynamic languages such as Perl and Python, the
control may well flow back to the parser to parse more code.
X<Parrot;compiler module>
Parrot's compiler module also has the capability to freeze bytecode to
disk and read that frozen bytecode back again, bypassing the parser
and compilation phases entirely. The bytecode can be directly
executed, or handed to the optimizer to work on before execution. This
may happen if you've loaded in a precompiled library and want Parrot
to optimize the combination of your code and the library code. The
bytecode loader is interesting in its own right, and also warrants a
small section.
=head2 Parser
Z<CHP-7-SECT-2.1>
X<parser, Parrot>
X<Parrot;parser module>
The parser module is responsible for source code in PASM, PIR, or one of
the high-level languages, and turning it into an X<AST (Abstract Syntax
Tree)> X<Abstract Syntax Tree (AST)> Abstract Syntax Tree (AST). An AST
is a digested form of the program, one that's much easier for Parrot to
work with. In some systems this task is split into two parts--the
lexing and the parsing--but since the tasks are so closely bound, Parrot
combines them into a single module.
X<lexing>
Lexing (or X<tokenizing> tokenizing) turns a stream of characters into a
stream of tokens. It doesn't assign any meaning to those tokens--that's
the job of the parser--but it is smart enough to see that an input line
such as C<$a = 1 + 2;> is composed of 6 tokens (C<$>, C<a>, C<=>, C<1>,
C<+>, and C<2>).
Parsing is the task of taking the tokens that the lexer has found and
assigning some meaning to them. Individual tokens from the lexer are
combined by the parser into larger and more complex tokens, which
represent common programming constructs like statements, subroutines,
and programs. When the input source code has been completely converted
into these complex data structures, the parsing stage is complete.
Sometimes the parsed output can be directly executed by the interpreter,
sometimes it is compiled first.
Parsing can be a chore as anyone who's done it before knows. In some
cases it can be downright maddening--Perl 5's parser has over ten
thousand lines of C code. Special utility programs such as I<lex>
and I<yacc> are often used to automate the generation of parser code.
Perl 5 itself uses a I<yacc>-based grammar to handle parts of the
processing task.N<I<yacc> can handle only part of the task, though.
As the quote goes, "The task of parsing Perl is divided between
I<lex>, I<yacc>, smoke, and mirrors."> Rather than going with a
custom-built parser for each language, Parrot provides a
general-purpose parser built on top of Perl 6's grammar engine, with
hooks for calling out to special-purpose code where necessary. Perl 6
grammars are designed to be powerful enough to handle parsing Perl, so
it made good sense to leverage the engine as a general-purpose parser.
Parrot provides some utility code to transform a I<yacc> grammar into a
Perl 6 grammar, so languages that already use I<yacc> can be moved over
to Parrot's parser with a minimum amount of fuss. This allows you to use
a I<yacc> grammar instead of a Perl 6 grammar to describe the language
being parsed, both because many languages already have their grammars
described with I<yacc> and because a I<yacc> grammar is sometimes a more
appropriate way to describe things.
Parrot does support independent parsers for cases where the Perl 6
grammar engine isn't the appropriate choice. A language might already
have an existing parser available, or different techniques might be in
order. The quirky parsing engines such as the one for Perl 5 may get
embedded this way, as it's easier to embed some quirky parsers than it
is to recreate all the quirks in a new parser.
=head2 Compiler
Z<CHP-7-SECT-2.2>
X<Parrot;compiler module>
X<compiler module, Parrot>
The parser outputs data in a form called an Abstract Syntax Tree (AST).
The compiler module takes this AST and converts it into bytecode that the
interpreter engine can execute. This translation is very straightforward
and isn't too expensive in terms of operating time and resources. The
tree is flattened, and is passed into a series of substitutions and
transformations. The compiler is the least interesting part or Parrot,
little more than a simple rule-based filter module. It is simple, but
it's a necessart part of Parrot.
For many languages the parser and compiler are essentially a single
unit. Like the parser, the compiler is modular, so you can load
in your own compiler if needed. Parrot itself comes with two compiler
modules for Parrot assembly and PIR. Instead of emitting bytecode
directly, many HLL compilers emit PIR code, and that is compiled by
Parrot into bytecode.
=head2 Optimizer
Z<CHP-7-SECT-2.3>
X<Parrot;optimizer module>
X<optimizer>
The optimizer module takes the AST from the parser and the bytecode
from the compiler, and transforms the bytecode to make it run faster.
X<optimizing code for dynamic languages>
Optimizing code for dynamic languages such as Perl, Python, and Ruby
is an interesting task. The languages are so dynamic that the
optimizer can't be sure how a program will actually run. For example,
the code:
$a = 0;
for (1..10000) {
$a++;
}
looks straightforward enough. The variable C<$a> starts at 0, gets
incremented ten thousand times, and has an end value of 10000. A
standard optimizer would turn that code into the single line:
$a = 10000;
and remove the loop entirely. Unfortunately, that's not necessarily
appropriate for these dynamic languages. C<$a> could easily be an
active value, which creates side effects when it is accessed or
incremented. If incrementing the variable ten thousand times causes
a hardware stepper motor to rotate smoothly, then just assigning a
value of 10000 to the variable might not only be incorrect but actually
dangerous to bystanders. An active variable like this might also keep
track of the number of times it has been accessed or modified. Either
way, optimizing the loop away changes the semantics of the program in
ways the original programmer didn't want.
Because of the potential for active or tied data, especially for
languages as dynamically typed as Perl, Python, Ruby, and PHP,
optimizing is a non-trivial task. Other languages, such as C or Pascal,
are more statically typed and lack active data, so an aggressive
optimizer is in order for them. Breaking out the optimizer into a
separate module allows us to add in optimizations piecemeal without
affecting the compiler. There's a lot of exciting work going into
the problem of optimizing dynamic languages, and we fully expect to
take advantage of it where we can.
Optimization is potentially an expensive operation, which is another
good reason to have it in a separate module. Spending ten seconds
optimizing a program that will run in five seconds is a huge waste of
time. On the other hand, spending ten seconds to optimize a program
makes sense if you save the optimized version to disk and use it over
and over again. Even if you save only one second per program run, it
doesn't take long for the ten-second optimization time to pay off. The
default is to optimize heavily when freezing bytecode to disk and
lightly when running directly, but this can be changed with a
command-line switch.
Perl, Python, and Ruby all lack a robust optimizer (outside their
regular expression engines), so any optimizations we add will increase
their performance. In fact, optimizations that we add might
help improve the performance of all high-level languages that run on
Parrot. This, we feel, is a good thing.
=head2 Interpreter
Z<CHP-8-SECT-2.4>
The interpreter module is the part of the engine that executes the
generated bytecode. Calling it an interpreter is something of a
misnomer, since Parrot's core includes both a traditional bytecode
interpreter module as well as a high-performance just-in-time (JIT)
compiler engine, but that's a detail of the implementation that we
don't need to discuss here at length N<At least, we won't discuss it
yet>.
All the interesting things happen inside the interpreter, and the
remainder of the chapter is dedicated to the interpreter and the
functions it provides. It's not out of line to consider the
interpreter as the real core of Parrot, and to consider the parser,
compiler, and optimizer as utility modules whose ultimate purpose is
to feed bytecode to the interpreter.
=head2 Bytecode Loader
Z<CHP-8-SECT-2.5>
X<Parrot;bytecode loader>
X<bytecode, Parrot;loader>
The bytecode loader isn't part of our block diagram, but it is
interesting
enough to warrant brief coverage.
The bytecode loader handles loading in bytecode that's been frozen to
disk. The Parrot bytecode loader is clever enough to handle loading in
Parrot bytecode regardless of the sort of system that it was saved on,
so we have cross-platform portability. You can generate bytecode on a
32-bit x86 system and load it up on a 64-bit Alpha or SPARC system
without any problems.
The bytecode loading system also has a heuristic engine built into it,
so it can identify the bytecode format it's reading. This means Parrot
can not only tell what sort of system Parrot bytecode was generated on
so it can properly process it, but also allows it to identify bytecode
generated for other bytecode driven systems, such as .NET, the JVM,
and the Z-machine.N<The Z-machine is the interpreter for
Infocom text adventures, such as Zork and The Lurking Horror.>
In addition to loading in bytecode, the loader is sufficiently clever
to recognize source files for any language that has a registered
compiler. It loads and compiles that source as if it were frozen
bytecode.
X<Parrot;loadable opcode library system>
Together with Parrot's loadable opcode library system (something we'll
talk about later), this gives Parrot the capability to load in foreign
bytecode formats and transform them into something Parrot can execute.
With a sophisticated enough loader, Parrot can load and execute Java and
.NET bytecode and present Java and .NET library code to languages that
generate native Parrot bytecode. This is something of a happy accident.
The original purpose of the architecture was to allow Parrot to load and
execute Z-machine bytecode, but happy accidents are the best kind.
=head1 The Interpreter
Z<CHP-7-SECT-3>
The X<interpreter, Parrot>
interpreter is the engine that actually runs the code emitted by the
parser, compiler, and optimizer modules. The Parrot execution engine
is a virtual CPU done completely in software. We've drawn on research
in CPU and interpreter design over the past forty years to try and
build the best engine to run dynamic languages.
That emphasis on dynamic languages is important. We are not trying to
build the fastest C, Forth, Lisp, or Prolog engine. Each class of
languages has its own quirks and emphasis, and no single engine will
handle all the different types of languages well. Trying to design an
engine that works equally well for all languages will get you an
engine that executes all of them poorly.
That doesn't mean that we've ignored languages outside our area of
primary focus--far from it. We've worked hard to make sure that we can
accommodate as many languages as possible without compromising the
performance of our core language set. We feel that even though we may
not run Prolog or Scheme code as fast as a dedicated engine would, the
flexibility Parrot provides to mix and match languages more than makes
up for that.
Parrot's core design is that of a register-rich CISC CPU, like many of
the CISC machines of the past such as the VAX, Motorola 68000, and IBM
System/3x0. It also bears some resemblance to modern RISC CPUs such as
the IBM Power series and Intel Alpha,N<Formerly HP, formerly Compaq,
formerly Digital Alpha.> as it does all its operations on data in
registers. Using a core design similar to older systems gives us
decades of compiler research to draw on. Most compiler research since
the early 1970s deals with targeting register systems of one sort or
another.
Using a register architecture as the basis for Parrot goes against the
current trends in virtual machines, which favor stack-based
approaches. While a stack approach is simpler to implement, a register
system provides a richer set of semantics. It's also just more
pleasant for us assembly old-timers to write code for. Combined with
the decades of sophisticated compiler research, we feel that it's the
correct design decision.
=head2 Registers
Z<CHP-7-SECT-3.1>
X<interpreter, Parrot;registers>
As we've seen in previous chapers, Parrot has four basic types of
registers: PMC, string, integer, and floating-point numbers, one for
each of the core data types. We separate the register types for ease
of implementation, garbage collection, and space efficiency. Since
PMCs and strings are garbage-collectable entities, restricting what
can access them--strings in string registers and PMCs in PMC registers
--makes the garbage collector a bit faster and simpler. Integers and
floats map directly to low-level machine data types and can be stored
in sequential arrays to save space and increase access speed.
=head2 Strings
Z<CHP-7-SECT-3.3>
X<strings;Parrot>
X<interpreter, Parrot;strings>
Text data is deceptively complex, so Parrot has strings as a
fundamental data type and tackles the problems head-on. We do this
out of sheer practicality, because we know how complex and error-prone
strings can get. We implement them one, and all languages that target
Parrot can share that same implementation.
The big problem with text is the vast number of human languages and
the variety of conventions around the world for dealing with it. Long
ago, 7-bit ASCII with 127 characters was sufficient N<And if that wasn't
sufficient, too bad. It's all you had.>. Computers were limited and
mostly used in English, regardless of the user's native language. These
heavy restrictions were acceptable because the machines of the day were
so limited that any other option was too slow. Also, most people using
computers at the time were fluent in English either as their native or
comfortable second language.
That day passed quite a few years ago. Many different ways of
representing text have sprung up, from the various multibyte Japanese
and Chinese representations--designed for languages with many thousands
of characters--to a half dozen or so European representations, which
take only a byte but disagree on what characters fit into that byte.
The Unicode consortium has been working for years on the Unicode standard
to try and unify all the different schemes, but full unification is still
years away, if it ever happens.
In the abstract, strings are a series of integers with meaning
attached to them, but getting from real-world data to abstract
integers isn't as simple as you might want. There are three important
things associated with string data--encoding, character set, and
language--and Parrot's string system knows how to deal with them.
X<strings;encoding>
A string's I<encoding> says how to turn data from a stream of bytes to a
stream of characters represented by integers. Something like ASCII data
is simple to deal with, since each character is a single byte, and
characters range in value from 0 to 255. UTF-8, one of the Unicode
encodings, is more complex--a single character can take anywhere from 1
to 6 bytes.
X<strings;character set>
The I<character set> for a string tells Parrot what each of the integers
actually represents. Parrot won't get too far if it doesn't know that 65
is a capital "A" in an ASCII or Unicode character stream, for example.
X<strings;language>
Finally, the I<language> for a string determines how the string behaves
in some contexts. Different languages have different rules for sorting
and case-folding characters. Whether an accented character keeps its
accent when upper-cased or lowercased, for instance, depends on the
language that the string came from.
The capability of translating strings from one encoding to another and
one character set to another, and to determine when it's needed, is
built into Parrot. The I/O and regular expression systems fully
exploit Parrot's core string capabilities, so any language that uses
Parrot's built-in string functionality gets this for free. Since
properly implementing even a single system like Unicode is fraught
with peril, this makes the job of people writing languages that target
Parrot much easier.
While Parrot provides these facilities, languages aren't required to
make use of them. Perl 6, for example, generally mandates that all
strings will be treated as if they are Unicode. In this case Parrot's
multi-lingual capabilities mainly act as filters to translate to and
from Unicode. Parrot presents all the data as if it were Unicode, but
only translates non-Unicode data to Unicode in situations where your
program may notice.
Unicode is Parrot's character set of last resort when it needs one.
We use IBM's ICU Unicode library to do all the heavy lifting, since
writing a properly done Unicode library is a non-trivial undertaking.
It makes more sense to use a well-tested and debugged library than it
does to try and reimplement Unicode again.
=head2 Variables
Z<CHP-7-SECT-3.4>
X<variables;Parrot interpreter and>
X<interpreter, Parrot;variables>
Variables are a fundamental construct in almost all computer
languages.N<With the exception of functional languages, though they
can be useful there as well.> With low-level languages such as C,
variables are straightforward--they are either basic hardware
constructs like a 32-bit integer, a 64-bit IEEE floating-point number,
or the address of some location in memory, or they're a structure
containing basic hardware constructs. Exchanging variables between
low-level languages is simple because all the languages operate on
essentially the same things.
Once you get to higher-level languages, variables get more
interesting. OO languages have the concept of the object as a
fundamental construct, but no two OO languages seem to agree on
exactly how objects should behave or how they should be implemented.
Then there are higher-level languages like Perl, with complex
constructs like hashes, arrays, and polymorphic scalars as fundamental
constructs.
The first big issue that Parrot had to face was implementing these
constructs. The second was doing it in a way that allowed Perl code to
use Ruby objects, Ruby code to use Python objects, and Lisp code to
use both.N<Or vice-versa> Parrot's solution is the PMC datatype.
A PMC, as we've seen in previous chapers, is an abstract variable type.
The languages we're working to support--Perl, Python, and Ruby for
example--have base variables that are far more complex than just an
integer or floating-point number. If we want them to exchange any
sort of real data, they must have a common base variable type. Parrot
provides that with the PMC construct. Each language can build on this
common base. More importantly, each language can make sure that their
variables behave properly regardless of which language is using them.
When you think about it, there is a large list of things that a
variable should be able to do. You should, for example, be able to
load or store a value, add or subtract it from another variable, call
a method or set a property on it, get its integer or floating-point
representation, and so on. What we did was make a list of these
functions and turn them into a mandatory interface called the VTABLE.
Each PMC has a VTABLE attached to it. This table of function pointers
is fixed--the list of functions, and where they are in the table, is
the same for each PMC. All the common operations a program might
perform on a variable, as well as all the operators that might be
overloaded for a PMC, have VTABLE entries.
=head2 Bytecode
Z<CHP-7-SECT-3.5>
Like any CPU, software, or hardware, Parrot needs a set of
instructions to tell it what to do. For hardware, this is a stream of
executable code or machine language. For Parrot, this is bytecode.
Calling it bytecode isn't strictly accurate, since the individual
instructions are 32 bits each rather than 8 bits each, but since it's
the common term for most other virtual machines, it's the term we use.
Each instruction--also known as an X<opcode> I<opcode>--tells the
interpreter engine what to do. Some opcodes are very low level, such as
the one to add two integers together. Others are significantly more
complex, like the opcode to take a continuation.
X<bytecode, Parrot>
X<Parrot;bytecode>
Parrot's bytecode is designed to be directly executable. The code on
disk can be run by the interpreter without needing any translation.
This gets us a number of benefits. Loading is much faster, of course,
since we don't have to do much (if any) processing on the bytecode as
it's loaded. It also means we can use some special OS calls that map a
file directly into the memory space of a process. Because of the way
this is handled by the operating system,N<Conveniently, this works the
same way for all the flavors of Unix, Windows, and VMS.> the bytecode
file will be loaded into the system's memory only once, no matter how
many processes use the file. This can save a significant amount of
real RAM on server systems. Files loaded this way also get their parts
loaded on demand. Since we don't need to process the bytecode in any
way to execute it, if you map in a large bytecode library file, only
those bits of the file your program actually executes will get read in
from disk. This can save a lot of time.
Parrot creates bytecode in a format optimized for the platform it's
built on, since the most common case by far is executing bytecode that's
been built on the system you're using. This means that floating-point
numbers are stored in the current platform's native format, integers
are in the native size, and both are stored in the byte order for the
current platform. Parrot does have the capability of executing
bytecode that uses 32-bit integers and IEEE floating-point numbers on
any platform, so you can build and ship bytecode that can be run by
anyone with a Parrot interpreter.
If you do use a bytecode file that doesn't match the current
platform's requirements (perhaps the integers are a different size),
Parrot automatically translates the bytecode file as it reads it in.
In this case, Parrot does have to read in the entire file and process
it. The sharing and load speed benefits are lost, but it's a small
price to pay for the portability. Parrot ships with a utility to turn
a portable bytecode file into a native format bytecode file if the
overhead is too onerous.
X<Parrot;interpreter;;(see interpreter, Parrot)>
=head1 I/O, Events, and Threads
Z<CHP-7-SECT-4>
Parrot has comprehensive support for I/O, threads, and events. These
three systems are interrelated, so we'll treat them together. The
systems we talk about in this section are less mature than other parts
of the engine, so they may change by the time we roll out the final
design and implementation.
=head2 I/O
Z<CHP-7-SECT-4.1>
Parrot's base X<I/O;Parrot> I/O system is fully X<asynchronous I/O>
asynchronous with callbacks and per-request private data. Since this
is massive overkill in many cases, we have a plain vanilla synchronous
I/O layer that programs can use if they don't need the extra power.
Asynchronous I/O is conceptually pretty simple. Your program makes an
I/O request. The system takes that request and returns control to your
program, which keeps running. Meanwhile the system works on satisfying
the I/O request. Once satisfied, the system notifies your program in
some way. Since there can be multiple requests outstanding, and you can't
be sure exactly what your program will be doing when a request is
satisfied, programs that make use of asynchronous I/O can become very
complex.
X<synchronous I/O>
Synchronous I/O is even simpler. Your program makes a request to the
system and then waits until that request is done. There can be only
one request in process at a time, and you always know what you're
doing (waiting) while the request is being processed. It makes your
program much simpler, since you don't have to do any sort of
coordination or synchronization.
The big benefit of asynchronous I/O systems is that they generally
have a much higher throughput than a synchronous system. They move
data around much faster--in some cases three or four times faster.
This is because the system can be busy moving data to or from disk
while your program is busy processing the next set of data.
For disk devices, having multiple outstanding requests--especially on
a busy system--allows the system to order read and write requests to
take better advantage of the underlying hardware. For example, many
disk devices have built-in track buffers. No matter how small a
request you make to the drive, it always reads a full track. With
synchronous I/O, if your program makes two small requests to the same
track, and they're separated by a request for some other data, the
disk will have to read the full track twice. With asynchronous I/O, on
the other hand, the disk may be able to read the track just once, and
satisfy the second request from the track buffer.
Parrot's I/O system revolves around a request. A request has three
parts: a buffer for data, a completion routine, and a piece of data
private to the request. Your program issues the request, then goes about
its business. When the request is completed, Parrot will call the
completion routine, passing it the request that just finished. The
completion routine extracts out the buffer and the private data, and
does whatever it needs to do to handle the request. If your request
doesn't have a completion routine, then your program will have to
explicitly check to see if the request was satisfied.
Your program can choose to sleep and wait for the request to finish,
essentially blocking. Parrot will continue to process events while
your program is waiting, so it isn't completely unresponsive. This is
how Parrot implements synchronous I/O--it issues the asynchronous
request, then immediately waits for that request to complete.
The reason we made Parrot's I/O system asynchronous by default was
sheer pragmatism. Network I/O is all asynchronous, as is GUI
programming, so we knew we had to deal with asynchrony in some form.
It's also far easier to make an asynchronous system pretend to be
synchronous than it is the other way around. We could have decided to
treat GUI events, network I/O, and file I/O all separately, but there
are plenty of systems around that demonstrate what a bad idea that is.
=head2 Events
Z<CHP-7-SECT-4.2>
An X<events, Parrot> event is a notification that something has
happened: the user has manipulated a GUI element, an I/O request has
completed, a signal has been triggered, or a timer has expired. Most
systems these days have an event handler,N<Often two or three, which is
something of a problem.> because handling events is so fundamental to
modern GUI programming. Unfortunately, most event handling systems are
not integrated, or poorly integrated, with the I/O system. This leads
to nasty code and unpleasant workarounds to try and make a program
responsive to network, file, and GUI events simultaneously. Parrot,
on the other hand, presents a unified event handling system integrated
with its I/O system; this makes it possible to write cross-platform
programs that work well in a complex environment.
Parrot's events are fairly simple. An event has an event type, some
event data, an event handler, and a priority. Each thread has an event
queue, and when an event occurs it is put into the queue for the correct
thread. Once in the queue, events must wait until an event handler gets
a chance to process it. If there is no clear destination thread for the
event, it is put into a default queue where it can be processed.
Any operation that would potentially block normal operation, such as a
C<sleep> command or the cleanup operations that Parrot calls when it exits
a subroutine, causes the event handlers to process through the events
in the queue. In this way, when your program thinks it is just waiting,
it is actually getting a lot of work done in the background. Parrot
doesn't check an outstanding event to handle during every opcode. This
is a pure performance consideration: All those checks would get expensive
very quickly. Parrot generally ensures timely event handling, and events
shouldn't ever be ignored for more then a few milliseconds. N<Unless
asynchronous event handling is explicitly disabled, and then events will
stay ignored for as long as the programmer wants.>.
When Parrot does extract an event from the event queue, it calls that
event's event handler, if it has one. If an event doesn't have a
handler, Parrot instead looks for a generic handler for the event type
and calls it instead. If for some reason there's no handler for the
event type Parrot falls back to the generic event handler which
throws an exception as a last resort. You can override the generic event
handler if you want Parrot to do something else with unhandled events,
perhaps silently discard them instead.
Because events are handled in mainline code, they don't have the
restrictions commonly associated with interrupt-level code. It's safe
and acceptable for an event handler to throw an exception, allocate
memory, or manipulate thread or global state safely. Event handlers
can even acquire locks if they need to. Even though event handlers have
all these capabilities, it doesn't mean they should be used with
impugnity. An event handler blocking on a lock can easily deadlock a
program that hasn't been properly designed. Parrot gives you plenty of
rope, it's up to the programmer not to trip on it.
Parrot uses the priority on events for two purposes. First, the
priority is used to order the events in the event queue. Events for a
particular priority are handled in a FIFO manner, but higher-priority
events are always handled before lower-priority events. Parrot also
allows a user program or event handler to set a minimum event priority
that it will handle. If an event with a priority lower than the
current minimum arrives, it won't be handled, instead sitting in the
queue until the minimum priority level is dropped. This allows an
event handler that's dealing with a high-priority event to ignore
lower-priority events.
User code generally doesn't need to deal with prioritized events, so
programmers should adjust event priorities with care. Adjusting the
default priority of an event, or adjusting the current minimum
priority level, is a rare occurrence. It's almost always a mistake to
change them, but the capability is there for those rare occasions
where it's the correct thing to do.
=head2 Signals
Z<CHP-7-SECT-4.3>
X<signals, Parrot>
Signals are a special form of event, based on the standard Unix signal
mechanism. Even though signals are occasionally described as being
special in some way, under the hood they're treated like any other event.
The primary difference between a signal and an event is that signals
have names that Unix and Linux programmers will recognize. This can be
a little confusing, especially when the Parrot signal doesn't use exactly
the same semantics as the Unix signal does.
The Unix signaling mechanism is something of a mash, having been
extended and worked on over the years by a small legion of ambitious but
underpaid programmers. There are generally two types of signals to deal
with: those that are fatal, and those that are not.
X<fatal signals>
Fatal signals are things like X<SIGKILL>
SIGKILL, which unconditionally kills a process, or X<SIGSEGV> SIGSEGV,
which indicates that the process has tried to access memory that isn't
part of your process. Most programmers will better know SIGSEGV as a
"segmentation fault", something that should be avoided at all costs.
There's no good way for Parrot to catch and handle these signals, since
the occur at a lower level in the operating system and are typically
presented to Parrot long after anything can be done about it. These
signals will therefore always kill Parrot and whatever programs were
running on t. On some systems it's possible to catch some of
N<sometimes> the fatal signals, but Parrot code itself operates at too
high a level for a user program to do anything with them. Any handlers
for these kinds of signals would have to be written at the lowest levels
in C or a similar language, something that cannot be accessed directly
from PIR, PASM, or any of the high-level languages that run on Parrot.
Parrot itself may try to catch these signals in special circumstances for
its own use, but that functionality isn't exposed to a user program.
X<non-fatal signals>
Non-fatal signals are things like X<SIGCHLD> SIGCHLD, indicating that a
child process has died, or X<SIGINT> SIGINT, indicating that the user
has hit C<^C> on the keyboard. Parrot turns these signals into events
and puts them in the event queue. Your program's event handler for the
signal will be called as soon as Parrot gets to the event in the queue,
and your code can do what it needs to with it.
SIGALRM, the timer expiration signal, is treated specially by
Parrot. Generated by an expiring alarm() system call, this signal is
normally used to provide timeouts for system calls that would
otherwise block forever, which is very useful. The big downside to
this is that on most systems there can only be one outstanding
alarm() request, and while you can get around this somewhat with the
setitimer call (which allows up to three pending alarms) it's still
quite limited.
Since Parrot's IO system is fully asynchronous and never blocks--even
what looks like a blocking request still drains the event queue--the
alarm signal isn't needed for this. Parrot instead grabs SIGALRM for
its own use, and provides a fully generic timer system which allows
any number of timer events, each with their own callback functions
and private data, to be outstanding.
=head2 Threads
Z<CHP-7-SECT-4.4>
X<threads, Parrot>
Threads are a means of splitting a process into multiple pieces that
execute simultaneously. It's a relatively easy way to get some
parallelism without too much work. Threads don't solve all the
parallelism problems your program may have N<And in fact, threading
can cause it's own parallism problems, if you aren't careful>.
Sometimes multiple processes on a single system, multiple processes
on a cluster, or processes on multiple separate systems are better
for parallelized tasks then using threads.
All the resources in a threaded process are shared between threads.
This is simultaneously the great strength and great weakness of the
method. Easy sharing is fast sharing, making it far faster to
exchange data between threads or access shared global data than to
share data between processes on a single system or on multiple
systems. Easy sharing of data can be dangerous, though, since data can
be corrupted if the threads don't coordinate between themselves somehow.
And, because all the threads are contained within a single process, if
any one of them causes a fatal error, Parrot and all the programs and
threads running on top of it dies.
With a low-level language such as C, these issues are manageable. The
core data types, integers, floats, and pointers are all small enough
to be handled atomically. You never have to worry that two threads will
try to write a value to the same integer variable, and the result will
be a corrupted combination of the two. It will be one or the other value,
depending on which thread wrote to the memory location last. Composite
data structures, on the other hand, are not handled atomically. Two
threads both accessing a large data structure can write incompatible data
into different fields. To avoid this, these stuctures can be protected
with special devices called mutexes. Mutexes N<depending on the exact
implementation and semantics, Mutexes can also be known as locks,
spinlocks, semaphores, or critical sections.> are special structures that
a thread can get exclusive access to. Like a baton in a relay race, only
one thread can own a mutex at a time, and by convention only the thread
with the mutex can access the associated data. The composite data
elements that need protecting can each have their own mutex, and when a
thread tries to touch the data it must acquires the mutex first. If
another thread already has the mutex, all other threads must wait before
they can get the mutex and access the data. By default there's very
little data that must be shared between threads, so it's relatively easy
to write thread-safe code if a little thought is given to the program
structure. Thread safety is far too big a topic to cover in this book,
but trust us when we say it's something worth being concerned with.
X<Parrot;native data type;;(see PMCs)>
X<PMCs (Parrot Magic Cookies);Parrot's native data type>
PMCs are complex structures, even the simplest ones. We can't count on
the hardware or even the operating system to provide us atomic access.
Parrot has to provide that atomicity itself, which is expensive. Getting
and releasing a mutex is an inexpensive operations by itself, and has
been heavily optimized by platform vendors because they want threaded
code to run quickly. It's not free, though, and when you consider that
Parrot must access hundreds or even thousands of PMCs for some programs,
any operations that get performed for all accesses can impose a huge
performance penalty.
=head3 External Libraries
Even if your program is thread-safe, and Parrot itself is thread-safe,
that doesn't mean there is no danger. Many libraries that Parrot uses
or that your program taps into through NCI may not be thread safe, and
may crash your program if you attempt to use them in a threaded
environment. Parrot cannot make existing unsafe libraries any safer
N<We can send nagging bug reports to the library developers.>, but at
least Parrot itself won't introduce new problems. Whenever you're using
an external library, you should double-check that it's safe to use with
threading environments. If you aren't using threading in your programs,
you don't need to worry about it.
=head3 Threading Models
When you think about it, there are really three different threading
models. In the first one, multiple threads have no interaction among
themselves. This essentially does with threads the same thing that's
done with processes. This works very well in Parrot, with the
isolation between interpreters helping to reduce the overhead of this
scheme. There's no possibility of data sharing at the user level, so
there's no need to lock anything.
In the second threading model, multiple threads run and pass messages
back and forth between each other. Parrot supports this as well, via
the event mechanism. The event queues are thread-safe, so one thread
can safely inject an event into another thread's event queue. This is
similar to a multiple-process model of programming, except that
communication between threads is much faster, and it's easier to pass
around structured data.
In the third threading model, multiple threads run and share data
between themselves directly. While Parrot can't guarantee that data at
the user level remains consistent, it can make sure that access to shared
data is at least safe. We do this with two mechanisms.
First, Parrot presents an advisory lock system to user code. Any piece
of user code running in a thread can lock a variable. Any attempt to
lock a variable that another thread has locked will block until the
lock is released. Locking a variable only blocks other lock attempts.
It does I<not> block access. This may seem odd, but it's the same scheme
used by threading systems that obey the POSIX thread standard, and has
been well tested in practice.
Secondly, Parrot forces all shared PMCs to be marked as such, and all
access to shared PMCs must first acquire that PMC's private lock. This
is done by installing an alternate VTABLE for shared PMCs, one that
acquires locks on all its parameters. These locks are held only for
the duration of the VTABLE interface call, but ensure that the PMCs
affected by the operation aren't altered by another thread while the
VTABLE operation is in progress.
=head1 Objects
Z<CHP-7-SECT-5>
X<objects;Parrot>
Perl 5, Perl 6, Python, and Ruby are all object-oriented languages in
some form or other, so Parrot has to have core support for objects and
classes. Unfortunately, all these languages have somewhat different
object systems, which made the design of Parrot's object system
somewhat tricky. It turns out that if you draw the abstraction lines
in the right places, support for the different systems is easily
possible. This is especially true if you provide core support for things
like method dispatch that the different object systems can use and
override.
=head2 Generic Object Interfacing
Z<CHP-7-SECT-5.1>
X<PMCs (Parrot Magic Cookies);handling method calls>
Parrot's object system is very simple--in fact, a PMC only has to handle
method calls to be considered an object. Just handling methods covers
well over 90% of the object functionality that most programs use, since
the vast majority of object access is via method calls. This means that
user code that does the following:
object = some_constructor(1, 2, "foo");
object.bar(12);
will work just fine, no matter what language the class that backs
C<object> is written in, if C<object> even has a class backing it. It
could be Perl 5, Perl 6, Python, Ruby, or even Java, C#, or Common
Lisp; it doesn't matter.
Objects may override other functionality as well. For example, Python
objects use the basic PMC property mechanism to implement object
attributes. Both Python and Perl 6 mandate that methods and properties
share the same namespace, with methods overriding properties of the
same name.
=head2 Parrot Objects
Z<CHP-7-SECT-5.2>
X<objects;Parrot>
X<Parrot;objects>
When we refer to Parrot objects we're really talking about Parrot's
default base object system. Any PMC can have methods called on it and
act as an object, and Parrot is sufficiently flexible to allow for
alternate object systems, such as the one Perl 5 uses. In this
section, though, we're talking about what we provide in our standard
object system. Parrot's standard object system is pretty
traditional--it's a class-based system with multiple inheritance,
interface declarations, and slot-based objects.
X<inheritance;in Parrot>
Each object is a member of a class, which defines how the object
behaves. Each class in an object's hierarchy can have one or more
attributes--that is, named slots that are guaranteed to be in each
object of that class. The names are all class-private so there's no
chance of collision. Objects are essentially little fixed-sized
arrays that know what class they belong to. Most of the "smarts" for
an object lives in that object's class. Parrot allows you to add
attributes at run time to a class. If you do, then all objects with
that class in their inheritance hierarchy will get the new attribute
added into it. While this is potentially expensive it's a very useful
feature for languages that may extend a class at run time.
X<multiple inheritance; in Parrot>
Parrot uses a multiple inheritance scheme for classes. Each class can
have two or more parent classes, and each of those classes can have
multiple parents. A class has control over how methods are searched
for, but the default search is a left-most, depth-first search, the
same way that Perl 5 does it. Individual class implementers may
change this if they wish, but only the class an object is instantiated
into controls the search order. Parrot also fully supports correct
method redispatch, so a method may properly call the next method in
the hierarchy even in the face of multiple parents. One limitation we
place on inheritance is that a class is only instantiated in the
hierarchy once, no matter how many times it appears in class and
parent class inheritance lists.
Each class has its own vtableX<vtable>, which all objects of that
class share. This means that with the right vtable methods every
object can behave like a basic PMC type in addition to an object. For
unary operations such as load or store, the default class vtable first
looks for the appropriately named method in the class hierarchy. For
binary operators such as addition and subtraction, it first looks in
the multimethod dispatch table. This is only the default, and
individual languages may make different choices. Objects that
implement the proper methods can also act as arrays or hashes.
Finally, Parrot implements an interface declaration scheme. You may
declare that a class C<does> one or more named interfaces, and later
query objects at run time to see if they implement an interface. This
doesn't put any methods in a class. For that you need to either inherit
from a class that does or implement them by hand. All it does is make a
declaration of what your class does. Interface declarations are
inheritable as well, so if one of your parent classes declares that it
implements an interface then your class will as well. This is used in
part to implement Perl 6's roles.
=head2 Mixed Class-Type Support
Z<CHP-7-SECT-5.3>
X<mixed class-type support in Parrot>
X<classes;Parrot;mixed class support>
The final piece of Parrot's object system is the support for
inheriting from classes of different types. This could be a Perl 6
class inheriting from a Perl 5 class, or a Ruby class inheriting from
a .NET class. It could even involve inheriting from a fully compiled
language such as C++ or Objective C, if proper wrapping is
established.N<Objective C is particularly simple, as it has a fully
introspective class system that allows for run-time class creation.
Inheritance can go both ways between it and Parrot.> As we talked