Podstawy#

Web IDL#

Żeby utworzyć specyfikację (w postaci dokumentu) jakiegokolwiek języka należy przemyśleć w jakiej formie dokument będzie najbardziej użyteczny. Pod pojęciem użyteczności będzie kryła się jego zwarta i zrozumiała treść. Definicje poleceń w całości mogą być opisywane słownie, ale takie rozwiązanie jest niepraktyczne, rozwlekłe i ciężkie w dalszym rozwoju. W związku z tym przy tworzeniu dokumentacji jednego języka stosuje się inny język opisowy, w którym będzie można kompaktowo wyrazić całe zagadnienie.

W3C przy tworzeniu specyfikacji dla Webu od dawna stosuje język Web IDL, który może być używany do opisu interfejsów implementowanych w przeglądarkach internetowych. Web IDL jest wariantem języka IDL, z wieloma usprawnieniami umożliwiającymi opisywanie zachowania obiektów skryptowych w przystępnej formie. Dla Web IDL utworzono osobną specyfikację, obecnie trwają prace nad jej drugim wydaniem.

Przy omawianiu DOM i HTML5 nie sposób nie wspomnieć nic o Web IDL. Specyfikacje DOM i HTML5 są przepełnione wieloma definicjami z Web IDL. Język interfejsowy jest rozbudowany, ale wystarczy poznać jego najważniejsze elementy, a dalsza praca z innymi dokumentami będzie przyjemniejsza.

Specyfikacja Web IDL składa się z dwóch części. Pierwsza część opisuje składnię języka Web IDL. Kompaktowy jej wykaz umieszczono w "Załączniku A". Druga część opisuje, jak definicje napisane w IDL odpowiadają poszczególnym konstrukcjom w ECMAScript 6.

Oczywiście nie opiszę każdego złożenia z dokumentacji Web IDL. Przedstawię kilka kluczowych wariantów, które będą przydatne przy analizie DOM i HTML5. W przypadku bardziej egzotycznych poleceń należy samodzielnie przeszukać specyfikację Web IDL.

Definicje#

Każdy fragment IDL (IDL fragment) może składać się z następujących podstawowych definicji (definitions):

Konstrukcja powyższych definicji została dokładnie opisana w specyfikacji Web IDL.

Nazwy#

Każda podstawowa definicja oraz kilka innych konstrukcji składniowych (np. stałe, atrybuty, operacje, argumenty operacji itd.) posiada identyfikator # (identifier). Identyfikator spełnia rolę nazwy dla konstrukcji i pojawia się gdzieś w jej ramach. Nazwa musi pasować do następującego tokena identyfikatora (wyrażenia regularnego [A-Z_a-z][0-9A-Z_a-z]* - składnia pochodzi z Perla 5).

Poniżej zamieszczam kilka przykładów poprawnego umieszczenia identyfikatorów w poleceniach Web IDL:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Definicje podstawowe
interface interface-identifier { interface-members… };
partial interface interface-identifier { interface-members… };
exception exception-identifier { exception-members… };
dictionary dictionary-identifier { dictionary-members… };
partial dictionary dictionary-identifier { dictionary-members… };
enum enumeration-identifier { enumeration-values… };
callback callback-identifier = callback-signature;

// Atrybuty
interface identifier {
	attribute type attribute-identifier;
};

// Definicje typu
typedef type typedef-identifier;

// Pola wyjątków
exception identifier {
	type exception-member-identifier;
};

// Członkowie słowników
dictionary identifier {
	type dictionary-member-identifier;
};

// Stałe
const type constant-identifier = value;

// Operacje
return-type operation-identifier(arguments…);

// Argumenty operacji
return-type operation-identifier(argument-type argument-identifier,…);

Operacje należące do specjalnego rodzaju operacji nie muszą mieć identyfikatora, np. getter czy setter.

Znak U+005F LOW LINE ("_") przed nazwą identyfikatora może zostać użyty do ucieczki przed zastrzeżonym słowem, np. nazwa "interface" może zostać zadeklarowana w postaci "_interface".

Argumenty w operacjach również mogą posiadać identyfikator, umieszczony zaraz za typem argumentu. Nazwą identyfikatora może być token pasujący do wcześniejszego wzorca lub jeden z symboli ArgumentNameKeyword (np. callback, const, creator itd.). Jeśli używany jest jeden z tych symboli to nie musi być poprzedzany znakiem podkreślnika.

Pewna grupa identyfikatorów jest zastrzeżona (reserved identifiers), i nie może być użyta nawet z podkreślnikiem. Będą to constructor, iterator, toString, toJSON. W przypadku niektórych konstrukcji mogą wystąpić dalsze obostrzenia dla prawidłowych nazw identyfikatorów.

Identyfikator określający jedną z podstawowych konstrukcji nie może być taki sam, jak identyfikator zastosowany w innej podstawowej konstrukcji.

W obrębie fragmentu IDL, odwołanie do definicji nie musi pojawić się koniecznie po deklaracji tej definicji. Poniższy fragment będzie poprawny:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface B : A {
	void f(ArrayOfLongs x);
};

interface A {
};

typedef long[] ArrayOfLongs;

Odwołanie do A i ArrayOfLongs nastąpiło wcześniej niż sama ich deklaracja. Jest to jak najbardziej prawidłowe.

Atrybuty rozszerzające#

Definicje podstawowe, członków interfejsów, członków wyjątków, członków słowników oraz argumenty operacji można rozszerzać za pomocą atrybutów rozszerzających (extended attributes). Atrybuty rozszerzające pozwalają powiązać konstrukcję IDL z implementacją w konkretnym języku.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
[extended-attributes]
interface identifier {

	[extended-attributes]
	const type identifier = value;

	[extended-attributes]
	attribute type identifier;

	[extended-attributes]
	return-type identifier(arguments…);

};

Interfejsy#

Podstawową konstrukcją w Web IDL będzie interfejs (interface). Interfejs opisuje pewien stan i zachowanie dla obiektu, który będzie ten interfejs implementował.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface identifier {
	interface-members…
};

Interfejs składa się z identyfikatora (identifier) oraz dowolnej liczby członków interfejsu (interface-members), którymi mogą być stałe, atrybuty oraz operacje umieszczane między nawiasami klamrowymi w deklaracji interfejsu. Atrybuty opisują stan, który każdy obiekt implementujący interfejs powinien posiadać. Operacje opisują zachowania, które mogą być wywoływane na obiekcie. Stałe deklarują nazwane stałe wartości udostępniane dla użytkowników obiektów w systemie.

Dziedziczenie#

Interfejs może dziedziczyć (inherit) po innym interfejsie. Jeśli po identyfikatorze interfejsu następuje znak U+003A COLON (":") oraz kolejny identyfikator, wówczas ten drugi identyfikator wskazuje na interfejs, po którym się dziedziczy. Obiekty implementujące interfejs dziedziczący po innym interfejsie mają dostęp do członków z dziedziczonego interfejsu.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface identifier : identifier-of-inherited-interface {
	interface-members…
};

Kolejność występowania członków jest nieistotna, z wyjątkiem przeciążenia (overloading).

Interfejsy mogą określać członków mających taką samą nazwę jak członkowie z odziedziczonego interfejsu. Od specyfiki wiązania z konkretnym językiem zależy, kiedy przeciążeni członkowie będą dostępni w obiekcie.

Dziedziczenie to kluczowa sprawa. Dzięki niemu można tworzyć zwięzłą dokumentację języków. Także w przypadku praktycznych implementacji dziedziczenie zapewnia wygodną formę ponownego wykorzystania tych samych części, bez zbędnego powielania kodu. Najlepiej przeanalizować poniższy przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface A {
	void f();
	void g();
};

interface B : A {
	void f();
	void g(DOMString x);
};

Mamy dwa interfejsy A i B, przy czym B dziedziczy z A. Interfejs B ma własne operacje f() i g(), które przesłaniają odziedziczone operacje f() i g() z interfejsu A. Akurat w tym wypadku łańcuch dziedziczenia kończy się na interfejsie A, ponieważ on sam nie dziedziczy po żadnym innym interfejsie. W rzeczywistości odziedziczone interfejsy mogą dziedziczyć po innych interfejsach, przez co tworzone będą dłuższe łańcuchy dziedziczenia.

Jeszcze lepszym objaśnieniem będzie implementacja naszego fragmentu IDL w ECMAScripcie:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
[Object.prototype: the Object prototype object][A.prototype: interface prototype object for A][B.prototype: interface prototype object for B][instanceOfB]

Interfejsy A i B będą odpowiednikami obiektów prototypowych A.prototype i B.prototype. Każda nowa instancja obiektu B dziedziczy z B.prototype, który sam dziedziczy z A.prototype. Ponadto A.prototype dziedziczy dodatkowo z Object.prototype, z którego dziedziczą wszystkie obiekty w ECMAScript (tzn. tak było do 3 wydania ECMAScript, w kolejnych można tworzyć obiekty "czyste", czyli takie, które nie dziedziczą po Object.prototype).

Wywołując instanceOfB.f() odwołamy się do f zdefiniowanego w B. Oczywiście wciąż można wywołać f należące do A na instancji B, robimy to np. za pomocą polecenia A.prototype.f.call(instanceOfB).

Zrozumienie dziedziczenia oraz swobodne manewrowanie prototypami (a także funkcjami) to najważniejsza rzecz jaką trzeba się nauczyć przy pracy z ECMAScriptem. Z początku może wydawać się to trudne i niejasne, ale z biegiem czasu będzie chlebem powszednim w codziennym kodowaniu.

Nie wolno tworzyć zapętlonych łańcuchów dziedziczenia, czyli np. interfejs A nie może dziedziczyć po samym sobie, tak samo jak nie może dziedziczyć po interfejsie B, który dziedziczy z A. Niedozwolone jest także jednoczesne dziedziczenie z wielu interfejsów. W danej chwili interfejs może dziedziczyć tylko z pojedynczego interfejsu, który to może dziedziczyć z kolejnego itd.

Interfejs zwrotny#

Interfejs używający słowa kluczowego callback nazywany jest interfejsem zwrotnym (callback interface). Interfejsy zwrotne mogą być zaimplementowane przez obiekty użytkownika, ale nie przez obiekty platformy (szczegóły).

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
callback interface identifier {
	interface-members…
};

Interfejs zwrotny nie może dziedziczyć z innego interfejsu niezwrotnego, tak samo jak żaden interfejs niezwrotny nie może dziedziczyć z interfejsu zwrotnego.

Sugeruje się, żeby do definiowania obiektowych argumentów funkcji stosować typ słownikowy zamiast interfejsu zwrotnego, tak jak na poniższym przykładzie:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Gorsze podejście definicji IDL
callback interface Options {
	attribute DOMString? option1;
	attribute DOMString? option2;
	attribute long? option3;
};

interface A {
	void doTask(DOMString type, Options options);
};

// Lepsze podejście definicji IDL
dictionary Options {
	DOMString? option1;
	DOMString? option2;
	long? option3;
};

interface A {
	void doTask(DOMString type, Options options);
};

// Wywołanie w ECMAScript
var a = getA(); // Pobierz instancję A

a.doTask("something", { option1: "banana", option3: 100 });

W poniższym fragmencie IDL zdefiniowano uproszczoną wersję interfejsów DOM, jeden z nich będzie wywołaniem zwrotnym:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface Node {
	readonly attribute DOMString nodeName;
	readonly attribute Node? parentNode;
	Node appendChild(Node newChild);
	void addEventListener(DOMString type, EventListener listener);
};

callback interface EventListener {
	void handleEvent(Event event);
};

Ponieważ interfejs EventListener jest typu zwrotnego, obiekty użytkownika mogą go zaimplementować następująco:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
var node = getNode(); // Pobierz instancję Node

var listener = {
	handleEvent: function(event) {
		...
	}
};
node.addEventListener("click", listener); // Działa prawidłowo.

node.addEventListener("click", function() { ... }); // Też działa prawidłowo

Jednakże obiekty użytkownika nie mogą zaimplementować interfejsu Node:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
var node = getNode(); // Pobierz instancję Node

var newNode = {
	nodeName: "span",
	parentNode: null,
	appendChild: function(newchild) {
		...
	},
	addEventListener: function(type, listener) {
		...
	}
};

node.appendChild(newNode); // To zwróci wyjątek TypeError

Interfejs cząstkowy#

Interfejs może zostać podzielony na wiele części za pomocą definicji cząstkowego interfejsu (partial interface). Identyfikator definicji cząstkowej interfejsu musi być taki sam, jak identyfikator definicji interfejsu. Wszyscy członkowie umieszczeni w poszczególnych interfejsach cząstkowych są członkami tego samego interfejsu.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface SomeInterface {
	interface-members…
};

partial interface SomeInterface {
	interface-members…
};

Kolejność występowania definicji interfejsu oraz jego cząstkowych fragmentów nie ma znaczenia.

Interfejsy cząstkowe można rozszerzać za pomocą atrybutów rozszerzających, ale z wykluczeniem kilku wariantów: [ArrayClass], [Constructor], [ImplicitThis], [NamedConstructor], [NoInterfaceObject].

Polecenie zostało wprowadzone dla redaktorów technicznych dokumentów, którzy mogą podzielić definicje i umieścić w różnych sekcjach dokumentu lub na wielu jego stronach.

Stałe#

Stała (constant) jest deklaracją stosowaną do wiązania stałej wartości z nazwą. Stałe mogą występować w interfejsach oraz wyjątkach.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
const type identifier = value;

Identyfikator stałej nie może być taki sam jak identyfikator innego członka interfejsu w tym samym interfejsie, lub innego członka wyjątku w tym samym wyjątku. Identyfikator nie może być także "prototypem".

Typ stałej nie może być inny niż typ prymitywny lub typ prymitywny nullable. Jeśli jako typ używany jest identyfikator to musi odwoływać się do definicji typu (typedef), której typ może być tylko typem prymitywnym lub typem prymitywnym nullable.

Wartością stałej może być jeden z dwóch literalnych tokenów boolowskich (true i false), token null, token integer, token float, lub jedna z trzech specjalnych zmiennoprzecinkowych wartości stałych (-Infinity, Infinity i NaN).

Wartości te (włączając łańcuchy znakowe) mogą być również stosowane dla domyślnych wartości członków słowników lub dla opcjonalnych argumentów operacji. Niemniej jednak łańcuchy znakowe nie mogą być stosowane jako wartości w stałych.

Faktyczne wartości kryjące się pod poszczególnymi tokenami zostały dokładnie opisane w specyfikacji Web IDL. Dla liczb zmiennoprzecinkowych są one określone w normie IEEE 754 (pojedyncza lub podwójna precyzja). W razie potrzeby należy samodzielnie przeanalizować specyfikację pod kątem różnych typów i ich wartości.

Stałe nie są związane z poszczególnymi instancjami interfejsów w których się pojawiają. To od specyfiki wiążącej z językiem zależy, czy stałe będą eksponowane w instancjach. Wiązanie języka ECMAScript umożliwia jednak dostęp do stałych poprzez obiekty implementujące interfejsy IDL, w których to stałe są zadeklarowane. Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Definicja IDL
interface A {
	const short rambaldi = 47;
};

// Wywołanie w ECMAScript
A.rambaldi;
instanceOfA.rambaldi;

W specyfikacji Web IDL nie zdefiniowano żadnych atrybutów rozszerzających specjalnie przeznaczonych dla stałych, aczkolwiek samo rozszerzenie stałych jest możliwe.

Na koniec jeszcze kilka przykładowych definicji stałych różnego typu:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface Util {
	const boolean DEBUG = false;
	const octet LF = 10;
	const unsigned long BIT_MASK = 0x0000fc00;
	const float AVOGADRO = 6.022e23;
};

exception Problem {
	const short ERR_UNKNOWN = 0;
	const short ERR_OUT_OF_MEMORY = 1;

	short errorCode;
};

Atrybuty#

Atrybut (attribute) jest członkiem interfejsu, który służy do deklarowania pól danych z odpowiednim typem i identyfikatorem. Wartość atrybutu można pobrać i w niektórych przypadkach nawet zmienić. Istnieją dwa rodzaje atrybutów:

Jeśli atrybut nie ma w deklaracji słowa static, jest atrybutem regularnym, w przeciwnym razie jest atrybutem statycznym.

To od specyfiki wiązania z językiem zależy, czy możliwe będzie odczytywanie lub ustawianie wartości atrybutu statycznego poprzez instancje interfejsu. Ta sama uwaga będzie dotyczyła wywoływania operacji statycznych.

Atrybuty Web IDL będą utożsamiane z właściwościami (properties) obiektów ECMAScript.

Identyfikator atrybutu nie może być taki sam jak identyfikator innego członka interfejsu w tym samym interfejsie. Nie może być także "prototypem".

Typ atrybutu w deklaracji pojawia się zaraz po słowie attribute. Jeśli typ jest identyfikatorem lub identyfikatorem ze znakiem ? za nim, to identyfikator musi wskazywać na interfejs, wyliczenie (enumeration), funkcję zwrotną (callback function) albo definicję typu (typedef).

Atrybut jest tylko do odczytu, jeśli słowo readonly występuje przed słowem attribute. Obiekt implementujący interfejs ze zdefiniowanym atrybutem tylko do odczytu nie pozwoli na zmianę jego wartości. To od specyfiki wiązania z językiem zależy, czy taka akcja jest zabroniona, ignorowana lub zgłaszany jest wyjątek.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
readonly attribute type identifier;

Atrybut regularny, który nie jest tylko do odczytu, może dziedziczyć swój getter z interfejsu przodka. Dzięki temu atrybut tylko do odczytu z interfejsu przodka może zostać zmieniany w interfejsie pochodnym. Atrybut którego getter jest dziedziczony musi być tego samego typu jak atrybut dziedziczący, i słowo inherit nie może występować w atrybucie tylko do odczytu i atrybucie statycznym.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface Ancestor {
	readonly attribute TheType theIdentifier;
};

interface Derived : Ancestor {
	inherit attribute TheType theIdentifier;
};

Kiedy w deklaracji atrybutu regularnego pojawia się słowo stringifier, oznacza to, że obiekty implementujące interfejs powinny zwracać wartość tego atrybutu przy zamianie obiektów na postać tekstową. Polecenie może być stosowane jedynie dla atrybutów typu DOMString (szczegóły).

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface Ancestor {
	stringifier attribute DOMString identifier;
};

Polecenie lepiej zrozumieć na konkretnym przykładzie:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Definicja IDL
[Constructor]
interface Student {
	attribute unsigned long id;
	stringifier attribute DOMString name;
};

// Wywołanie w ECMAScript
var s = new Student();
s.id = 12345678;
s.name = 'Arek';

var greeting = 'Witam, ' + s + '!'; // Teraz greeting == 'Witam, Arek!'
// s.name wywołane zostało niejawnie, wystarczyło wprowadzić samo s

Kolejny przykład fragmentu IDL prezentuje definicję interfejsu z niestandardowym zachowaniem zamiany na postać tekstową:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
[Constructor]
interface Student {
	attribute unsigned long id;
	attribute DOMString? familyName;
	attribute DOMString givenName;

	stringifier DOMString ();
};

Obiekty implementujące interfejs Student są zamieniane na tekst następująco; jeśli wartością atrybutu familyName jest null, postacią tekstową obiektu będzie wartość atrybutu givenName. W przeciwnym razie, jeśli wartością atrybutu familyName nie jest null, postacią tekstową obiektu będzie połączenie wartości atrybutu givenName, pojedynczego znaku spacji, oraz wartości atrybutu familyName.

Przykładowa implementacja w ECMAScript :

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
var s = new Student();
s.id = 12345679;
s.familyName = 'Szparek';
s.givenName = 'Arek';

var greeting = 'Witam ' + s; // Teraz greeting == 'Witam Arek Szparek'

Jest to ciekawe zachowanie pozwalające zrozumieć zasady automatycznej konwersji obiektów na tekst.

Operacje#

Operacja (operation) jest członkiem interfejsu, która definiuje zachowanie mogące być wywoływane na obiektach implementujących interfejs. Istnieją trzy rodzaje operacji:

Jeśli operacja ma identyfikator, ale nie słowo static, jest operacją regularną. Jeśli operacja ma w deklaracji jedno ze słów specjalnych, jest operacją specjalną.

Operacje Web IDL będą utożsamiane z metodami (methods) obiektów ECMAScript.

Jeśli operacja nie ma identyfikatora, to musi być zadeklarowana jako operacja specjalna przy użyciu jednego ze słów specjalnych.

Identyfikator operacji regularnej lub statycznej nie może być taki sam jak identyfikator stałej lub atrybutu w tym samym interfejsie, ale może być taki sam jak identyfikatory innych operacji (pozwala to na przeciążanie). Identyfikator operacji statycznej nie może być "prototypem" oraz nie może być taki sam jak identyfikator operacji regularnej zdefiniowanej w tym samym interfejsie.

Zwracany typ (return type) operacji umieszcza się przed identyfikatorem. Specjalny typ void oznacza, że operacja nie zwraca żadnej wartości. Jeśli zwracany typ jest identyfikatorem ze znakiem ? za nim, to identyfikator musi wskazywać na interfejs, słownik, wyliczenie (enumeration), funkcję zwrotną (callback function) albo definicję typu (typedef).

Argumenty operacji # umieszcza się w nawiasach w deklaracji. Każdy pojedynczy argument jest określony przez typ oraz identyfikator. Identyfikatory argumentów mogą być także jednym ze słów kluczowych i nie potrzebują wstawiania znaku ucieczki w takim przypadku. Identyfikator każdego argumentu nie może być taki sam jak identyfikator innego argumentu w tej samej deklaracji operacji.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
return-type identifier(type identifier, type identifier, …);

Każdy argument może zostać rozszerzony za pomocą atrybutów rozszerzających, dzięki czemu możliwa jest kontrola sposobu umieszczania wartości w argumencie w poszczególnych językach.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
return-type identifier([extended-attributes] type identifier, [extended-attributes] type identifier, …);

Prosty przykład fragmentu IDL z deklaracją operacji regularnych w interfejsie:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface Dimensions {
	attribute unsigned long width;
	attribute unsigned long height;
};

exception NoPointerDevice { };

interface Button {

	// Operacja niepobierająca żadnego argumentu i zwracająca typ boolean
	boolean isMouseOver();

	// Przeciążanie operacji
	void setDimensions(Dimensions size);
	void setDimensions(unsigned long width, unsigned long height);
};

Operacja może przyjmować zmienną liczbę argumentów # (variadic), wystarczy umieścić wielokropek ... po ostatnim typie argumentu. Dzięki takiej deklaracji operacja może zostać wywołana z dowolną liczbą argumentów po tym ostatnim argumencie. Te dodatkowe argumenty będą takiego samego typu jak ostatni argument rozszerzający deklaracje. Ostatni argument może zostać pominięty przy wywoływaniu operacji. Argument nie może zostać zadeklarowany przy użyciu ..., chyba że jest ostatnim argumentem na liście argumentów operacji.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
return-type identifier(type... identifier);
return-type identifier(type identifier, type... identifier);

Prosty przykład fragmentu IDL oraz dowiązanie w ECMAScript:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Definicja IDL
interface IntegerSet {
	readonly attribute unsigned long cardinality;

	void union(long... ints);
	void intersection(long... ints);
};

// Wywołanie w ECMAScript
var s = getIntegerSet(); // Pobierz instancję IntegerSet

s.union(); // Brak argumentów zgodnych z 'ints'
s.union(1, 4, 7); // Trzy argumenty zgodne z 'ints'

Operacja może przyjmować argumenty opcjonalne (optional arguments) oznaczone słowem optional. Ostatni argument w operacji o zmiennej liczbie argumentów również jest uważany za argument opcjonalny. Deklaracja argumentu jako opcjonalny oznacza, że wartość argumentu może zostać pominięta przy wywoływaniu operacji. Argumentu nie można deklarować jako opcjonalnego, chyba że wszystkie kolejne argumenty operacji również są opcjonalne. Także ostatni argument w operacji nie może być deklarowany jako opcjonalny jeśli operacja jest operacją o zmiennej liczbie argumentów.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
return-type identifier(type identifier, optional type identifier);

Opcjonalny argument może mieć zdefiniowaną wartość domyślną (default value) za pomocą znaku U+003D EQUALS SIGN ("=") i wartości umieszczonych za identyfikatorem argumentu. Ostatni opcjonalny argument w operacji o dowolnej liczbie argumentów nie może mieć zdefiniowanej wartości domyślnej. Wartość domyślna to wartość, którą należy przyjąć, gdy operacja jest wywoływana z pominięciem odpowiadającego argumentu.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
return-type identifier(type identifier, optional type identifier = value);

Jeśli opcjonalny argument ma wartość domyślną, wówczas wszystkie poprzednie opcjonalne argumenty również muszą mieć wartość domyślną.

Operacje Web IDL nie wspierają wywołań pomijających opcjonalne argumenty, chyba że wszystkie kolejne opcjonalne argumenty również zostaną pominięte. To samo będzie dotyczyć ECMAScript.

Prosty przykład fragmentu IDL z deklaracją operacji:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Operacja może zostać wywołana z dwoma różnymi długościami listy argumentu
interface ColorCreator {
	object createColor(float v1, float v2, float v3, optional float alpha);
};

// Odpowiadający interfejs z dwoma przeciążonymi operacjami
interface ColorCreator {
	object createColor(float v1, float v2, float v3);
	object createColor(float v1, float v2, float v3, float alpha);
};

Kolejny przykład z równoważnymi deklaracjami operacji specjalnych:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface Dictionary {
	readonly attribute unsigned long propertyCount;

	// Operacje specjalne z identyfikatorem
	getter float getProperty(DOMString propertyName);
	setter void setProperty(DOMString propertyName, float propertyValue);
};

interface Dictionary {
	readonly attribute unsigned long propertyCount;

	float getProperty(DOMString propertyName);
	void setProperty(DOMString propertyName, float propertyValue);

	// Operacje specjalne bez identyfikatora
	getter float (DOMString propertyName);
	setter void (DOMString propertyName, float propertyValue);
};

Słowniki#

Słownik (dictionary) to definicja używana do określenia typu danych tablicy asocjacyjnej w stałym, uporządkowanym zestawie par klucz-wartość (key-value), nazywanych członkami słownika # (dictionary members), gdzie wartości to ciągi tekstowe a klucze są odpowiedniego typu określanego w definicji.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
dictionary identifier {
	dictionary-members…
};

Słowniki są zawsze przekazywane przez wartość (passed by value). W dowiązaniach językowych, gdzie słownik jest reprezentowany prze obiekt jakiegoś typu, przekazanie słownika do obiektu platformy nie spowoduje utworzenia referencji do słownika trzymanej przez ten obiekt. Podobnie, każdy słownik zwracany z obiektu platformy będzie jedynie kopią i może być modyfikowany, a zmiany nie będą widoczne dla obiektu platformy.

Słowniki mogą dziedziczyć (inherit) z innych słowników. Jeśli po identyfikatorze słownika następuje dwukropek i identyfikator, to ten drugi identyfikator wskazuje na słownik z którego się dziedziczy. Identyfikator musi wskazywać na słownik.

Nie wolno tworzyć zapętlonych łańcuchów dziedziczenia, czyli np. słownik A nie może dziedziczyć po samym sobie, tak samo jak nie może dziedziczyć po słowniku B, który dziedziczy z C.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
dictionary Base {
	dictionary-members…
};

dictionary Derived : Base {
	dictionary-members…
};

Wartości danego słownika mogą mieć pary klucz-wartość odpowiadające członkom tego słownika, jak i członkom po dziedziczonych słownikach. Określenie wartości słownika powoduje, że przy próbie pobrania wartości członek słownika jest obecny (present), w przeciwnym razie jest nieobecny. Członkowie słownika mogą opcjonalnie mieć wartość domyślną (default value), która jest wartością używaną dla członka słownika podczas przekazywania wartości do obiektu platformy, kiedy wartość nie została podana. Członkowie słownika z wartościami domyślnymi są zawsze uważani za obecnych.

Każdy członek słownika jest określony prze typ, po którym następuje identyfikator. Identyfikator jest nazwą klucza pary klucz-wartość. Jeśli typ jest identyfikatorem ze znakiem ?, to identyfikator musi wskazywać na interfejs, słownik, wyliczenie, funkcję zwrotną lub definicję typu.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
dictionary identifier {
	type identifier;
};

Jeśli po identyfikatorze następuje znak U+003D EQUALS SIGN ("=") i wartość, to nadaje to członkowi słownika jego wartość domyślną.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
dictionary identifier {
	type identifier = value;
};

Dopuszczalne typy i wartości zostały dokładnie opisane w specyfikacji WebIDL.

Tak samo jak w przypadku interfejsów, IDL dla słowników można podzielić na kilka części za pomocą cząstkowych definicji słowników (partial dictionary definitions). Identyfikator cząstkowej definicji słownika musi być taki sam, jak identyfikator definicji słownika. Wszyscy członkowie umieszczenie w każdej cząstkowej definicji słownika uważani są za członków samego słownika.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
dictionary SomeDictionary {
	dictionary-members…
};

partial dictionary SomeDictionary {
	dictionary-members…
};

Polecenie zostało wprowadzone dla redaktorów technicznych dokumentów, którzy mogą podzielić definicje i umieścić w różnych sekcjach dokumentu lub na wielu jego stronach.

Kolejność członków słownika danego słownika jest taka, że odziedziczeni członkowie słownika są sortowani przed nieodziedziczonymi członkami, oraz członkowie słownika w jednej definicji słownikowej (włączając cząstkowe definicje słownika) są sortowani leksykograficznie przez punkty kodowe Unikodu swoich identyfikatorów.

Prosty przykład definicji słowników z dziedziczeniem:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
dictionary B : A {
	long b;
	long a;
};

dictionary A {
	long c;
	long g;
};

dictionary C : B {
	long e;
	long f;
};

partial dictionary A {
	long h;
	long d;
};

Kolejność członków słownika dla wartości słownika typu C będzie następująca: c, d, g, h, a, b, e, f. Zwracam uwagę, że najpierw występują posortowane wartości odziedziczone. Sortowanie wartości występuje także między członkami słownika tej samej definicji.

Słowniki są zobowiązane do porządkowania swoich członków ponieważ w niektórych dowiązaniach językowych zaobserwowano zachowanie, w którym przekazanie wartości słownika do obiektu platformy zależy od porządku, w jakim członkowie słownika są pobierani. Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Definicja IDL
interface Something {
	void f(A a);
};

// Wywołanie w ECMAScript
var something = getSomething(); // Pobierz instancję Something
var x = 0;

var dict = { };
Object.defineProperty(dict, "d", { get: function() { return ++x; } });
Object.defineProperty(dict, "c", { get: function() { return ++x; } });

something.f(dict);

Porządek, w którym członkowie słownika są pobierani określa, jakie wartości będą zwracane. Skoro porządek dla A definiuje c i następnie d, wartość dla c będzie równa 1 i wartość dla d będzie wynosić 2.

Identyfikator członka słownika nie może być taki sam jak inny członek słownika zdefiniowany w słowniku lub odziedziczony z innych słowników.

Słowniki nie mogą być używane jako typy w atrybutach, stałych oraz polach wyjątków.

Słowniki można rozszerzyć za pomocą jednego atrybutu rozszerzającego [Constructor]. Możliwe jest także rozszerzanie członków słownika za pomocą [Clamp] i [EnforceRange].

Jednym z zastosowań typów słownikowych jest zezwolenie na umieszczenie szeregu dodatkowych argumentów do operacji, bez narzucania im kolejności, w jakiej są umieszczane przy wywołaniu. Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
[Constructor]
interface Point {
	attribute float x;
	attribute float y;
};

dictionary PaintOptions {
	DOMString? fillPattern = "black";
	DOMString? strokePattern = null;
	Point position;
};

interface GraphicsContext {
	void drawRectangle(float width, float height, optional PaintOptions options);
};

W implementacji ECMAScriipt tego fragmentu IDL, obiekt można przekazać przez opcjonalny słownik PaintOptions:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
var ctx = getGraphicsContext(); // Pobierz instancję GraphicsContext

// Narysuj prostokąt
ctx.drawRectangle(300, 200, { fillPattern: "red", position: new Point(10, 10) });

Zarówno fillPattern i strokePattern dają domyślne wartość, więc jeśli są one pomijane, definicja drawRectangle zakłada, że te wartości domyślne zostały przekazane.

Funkcje zwrotne#

Funkcja zwrotna (callback function) jest definicją wykorzystywaną do deklaracji typu funkcyjnego.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
callback identifier = return-type (arguments…);

Można również przeanalizować podobnie nazwane interfejsy zwrotne.

Identyfikator po lewej stronie znaku równości w równaniu nadaje funkcji zwrotnej nazwę. Zwracany typ i lista argumentów po prawej stronie znaku równości w równaniu określa typ funkcji zwrotnej.

Funkcje zwrotne nie mogą być używane jako typy w stałych.

Funkcje zwrotne można rozszerzyć za pomocą jednego atrybutu rozszerzającego [TreatNonCallableAsNull].

Poniższy fragment IDL definiuje funkcję zwrotną używaną na API, która wywołuje funkcję zdefiniowaną przez użytkownika, kiedy operacja jest zakończona.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Definicja IDL
callback AsyncOperationCallback = void (DOMString status);

interface AsyncOperations {
	void performOperation(AsyncOperationCallback whenFinished);
};

// Wywołanie w ECMAScript
var ops = getAsyncOperations(); // Pobierz instancję AsyncOperations

ops.performOperation(function(status) { // Obiekt funkcyjny jest umieszczany jako argument operacji
	window.alert("Operation finished, status is " + status + ".");
});

Definicje typu#

Definicja typu (typedef) jest definicją wykorzystywaną do deklaracji nowej nazwy dla typu. Ta nowa nazwa nie jest rozwijana przez dowiązanie językowe, jest natomiast często używana jak skrót odwołujący do typu w IDL.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
typedef type identifier;

Typ dla nowej nazwy określony zostaje za słowem typedef, oraz identyfikator dla nowej nazwy określony zostaje za typem.

Typ nie może wskazywać na siebie samego lub na inny typedef.

Atrybuty rozszerzające można podawać bezpośrednio po słowie typedef, i wszelkie takie atrybuty rozszerzające będą stosowane w konstrukcjach wykorzystujących typedef.

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
typedef [extended-attributes] type identifier;

Definicja typu nie może być użyta jako typ w konstrukcji, jeśli atrybut rozszerzający, który pojawia się po słowie typedef jest niedozwolony bezpośrednio w tej konstrukcji. Oznacza to, że dwa poniższe przykłady są prawidłowe:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
// Dłuższy zapis
typedef [Clamp] octet Value;

interface A {
	void setColor(Value r, Value g, Value b);
};

// Skrócony zapis
interface A {
	void setColor([Clamp] octet r, [Clamp] octet g, [Clamp] octet b);
};

Aczkolwiek obydwie poniższe operacje są zapisane nieprawidłowo:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
typedef [Clamp] octet Value;

interface A {
	Value getRedComponent();
	void setRedComponent([EnforceRange] Value r);
};

Argument rozszerzający [Clamp] nie może być użyty w zwracanej wartości przez operację a także nie może być użyty w połączeniu z [EnforceRange].

Atrybuty rozszerzające mające zastosowanie w konstrukcji ze względu na pojawiającą się referencję typedef są wstawiane za tymi, które są jednoznacznie określone w konstrukcji. Choć należy zauważyć, że żaden z atrybutów rozszerzających zawartych w specyfikacji, który może być użyty w typedef, nie jest wrażliwy na kolejność występowania.

W specyfikacji Web IDL nie zdefiniowano żadnych atrybutów rozszerzających specjalnie przeznaczonych dla definicji typów, aczkolwiek samo rozszerzenie definicji typów jest możliwe. To, co do tej pory omawialiśmy dotyczyło jedynie rozszerzania typów w definicji typów.

W poniższym fragmencie IDL stosujemy definicję typu, aby móc odwoływać się do krótszego identyfikatora zamiast dłuższego zapisu typu sekwencyjnego:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
interface Point {
	attribute float x;
	attribute float y;
};

typedef sequence<Point> Points; // Krótka nazwa 'Points'

interface Widget {
	boolean pointWithinBounds(Point p);
	boolean allPointsWithinBounds(Points ps);
};

Obiekty implementujące interfejsy#

W danej implementacji zestawu fragmentów IDL, obiekt może być określany jako obiekt platformy (platform object), obiekt użytkownika (user object), lub żaden z nich. Istnieją trzy rodzaje obiektów uważanych za obiekty platformy:

W przeglądarce, dla przykładu, implementacja przeglądarkowa obiektów DOM (implementujące interfejsy takie jak Node i Document), które zapewniają dostęp do zawartości strony dla uruchomionego ECMAScript na stronie, będą obiektami platformy. Te obiekty mogą być obiektami hosta, zaimplementowanymi w języku takim jak C++, albo mogą być natywnymi obiektami ECMAScript. Niezależnie od tego, implementacja danego zbioru fragmentów IDL musi być w stanie rozpoznać wszystkie obiekty platformy, które są tworzone przez implementację. Może być to zrobione poprzez jakiś wewnętrzny stan, który rejestruje, czy dany obiekt jest rzeczywiście obiektem platformy dla tej implementacji, czy może poprzez obserwowanie, że obiekt jest implementowany przez wewnętrzną klasę C++. Jak dokładnie obiekty platformy są rozpoznawane w implementacji danego zbioru fragmentów IDL zależy od specyfiki implementacji.

Wszystkie inne obiekty w systemie nie będą traktowane jak obiekty platformy. Załóżmy na przykład, że strona otwarta w przeglądarce ładuje bibliotekę ECMASCript, która implementuje rdzeń DOM. Ta biblioteka będzie uważana za inną implementację niż tak, którą zapewnia implementacja przeglądarki. Obiekty tworzone za pomocą biblioteki ECMAScript, które implementują interfejs Node nie będą traktowane jak obiekty platformy, które implementują interfejs Node przez implementację przeglądarki.

Obiekty użytkownika to te, które są tworzone przez autorów, i które implementują interfejsy zwrotne pozwalające wywoływać zdefiniowane operacje lub wysyłać i odbierać wartości do programów autorów poprzez manipulacje atrybutami obiektów. Na stronie internetowej obiekt ECMAScript implementujący interfejs EventListener, który jest używany do rejestracji wywołania zwrotnego dla zdarzeń DOM, uznawany jest za obiekt użytkownika.

Należy pamiętać, że obiekty użytkowników mogą implementować tylko interfejsy zwrotne oraz obiekty platformy mogą implementować tylko interfejsy niezwrotne.

Podsumowanie#

Tyle powinno wystarczyć do rozpoczęcia samodzielnej analizy specyfikacji Web IDL. Język definiujący/opisujący interfejsy jest podstawą przy analizie kolejnych specyfikacji, które będą w dużej mierze na nim bazować. W razie czego postaram się stopniowo wypełniał rozdział o zagadnienia, które osobiście musiałem rozszyfrować. Jeśli brakuje istotnych poleceń lub przedstawiona treść jest błędna proszę do mnie napisać.

Pasek społecznościowy

SPIS TREŚCI AKTUALNEJ STRONY

Podstawy (H1) Web IDL (H2) Definicje (H3) Nazwy (H3) Atrybuty rozszerzające (H3) Interfejsy (H3) Dziedziczenie (H4) Interfejs zwrotny (H4) Interfejs cząstkowy (H4) Stałe (H3) Atrybuty (H3) Operacje (H3) Słowniki (H3) Funkcje zwrotne (H3) Definicje typu (H3) Obiekty implementujące interfejsy (H3) Podsumowanie (H3)