מערכים מוקלדים – נתונים בינאריים בדפדפן

Ilmari Heikkinen

מבוא

מערכי Typed נוספו לדפדפנים רק לאחרונה, בעקבות הצורך בדרך יעילה לטיפול בנתונים בינאריים ב-WebGL. מערך מוגדר הוא מקטע זיכרון עם תצוגה מוגדרת, בדומה לאופן שבו מערכים פועלים ב-C. מכיוון ש-Typed Array מגובה בזיכרון גולמי, מנוע JavaScript יכול להעביר את הזיכרון ישירות לספריות מקוריות בלי צורך להמיר את הנתונים באופן ידני לייצוג מקורי. כתוצאה מכך, מערכי נתונים מוגדרים מניבים ביצועים טובים בהרבה ממערכי JavaScript להעברת נתונים ל-WebGL ולממשקי API אחרים שעוסקים בנתונים בינאריים.

תצוגות מערך מוקלדות פועלות כמו מערכים מסוג יחיד לקטע של ArrayBuffer. יש תצוגות לכל סוגי המספרים הרגילים, עם שמות שמתארים את עצמם, כמו Float32Array,‏ Float64Array,‏ Int32Array ו-Uint8Array. יש גם תצוגה מיוחדת שהחליפה את סוג מערך הפיקסלים ב-ImageData של Canvas: Uint8ClampedArray.

DataView הוא סוג התצוגה השני, והוא מיועד לטיפול בנתונים הטרוגניים. במקום API שדומה למערך, אובייקט DataView מספק API של קבלה/הגדרה לקריאה ולכתיבה של סוגי נתונים שרירותיים בהיסטים שרירותיים של בייטים. DataView מתאים במיוחד לקריאה ולכתיבה של כותרות של קבצים ונתונים אחרים מסוג struct.

עקרונות השימוש הבסיסיים במערכים מוקלדים

תצוגות מערך מוקלדות

כדי להשתמש במערכים מוגדרים, צריך ליצור ArrayBuffer ותצוגה שלו. הדרך הקלה ביותר היא ליצור תצוגת מערך מוקלדת לפי הגודל והסוג הרצויים.

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

יש כמה סוגים שונים של תצוגות של מערכי טיפוסים. לכל השירותים יש את אותו ממשק API, כך שאחרי שמבינים איך משתמשים באחד מהם, אפשר להבין איך משתמשים בכל השאר. בדוגמה הבאה אצור אחת מכל אחת מתצוגות המערך המוקלדות הקיימות.

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

האחרון קצת מיוחד, הוא מצמצם את ערכי הקלט בין 0 ל-255. האפשרות הזו שימושית במיוחד לאלגוריתמים של עיבוד תמונות ב-Canvas, כי עכשיו לא צריך לבצע באופן ידני קליפס (clamp) של החישובים של עיבוד התמונות כדי למנוע זליגה מטווח 8 הביט.

לדוגמה, כך מחילים גורם גאמה על תמונה ששמורה ב-Uint8Array. לא כל כך יפה:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

בעזרת Uint8ClampedArray אפשר לדלג על הלחיצה הידנית:

pixels[i] *= gamma;

הדרך השנייה ליצור תצוגות של מערכי נתונים מסוגים מוגדרים היא ליצור קודם ArrayBuffer ואז ליצור תצוגות שמצביעות עליו. ממשקי ה-API שמקבלים נתונים חיצוניים עוסקים בדרך כלל ב-ArrayBuffers, כך שזו הדרך לקבל תצוגה של מערך ממוין.

var ab = new ArrayBuffer(256); // 256-byte ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

אפשר גם ליצור כמה תצוגות של אותו ArrayBuffer.

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

כדי להעתיק מערך מוקלד למערך מוקלד אחר, הדרך המהירה ביותר היא להשתמש בשיטה של קבוצת המערך המוקלדת. לשימוש כמו memcpy, יוצרים Uint8Arrays למאגרים של התצוגות ומשתמשים ב-set כדי להעתיק את הנתונים.

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

DataView

כדי להשתמש ב-ArrayBuffers שמכילים נתונים מסוגים הטרוגניים, הדרך הקלה ביותר היא להשתמש ב-DataView למאגר הנתונים הזמני. נניח שיש לנו פורמט קובץ עם כותרת עם int ללא סימן באורך 8 ביט, ואחריה שני int באורך 16 ביט, ואחריה מערך של נתוני עומס שימושי של ערכים מסוג float באורך 32 ביט. אפשר לקרוא את זה חזרה באמצעות תצוגות של מערכי טיפוסים, אבל זה קצת קשה. באמצעות DataView אנחנו יכולים לקרוא את הכותרת ולהשתמש בתצוגת מערך ממוין של מערך ה-float.

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

בדוגמה שלמעלה, כל הערכים שקראתי הם big-endian. אם הערכים במאגר הם little-endian, אפשר להעביר את הפרמטר האופציונלי littleEndian ל-getter:

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

חשוב לזכור שתצוגות של מערכי טיפים תמיד מוצגות בסדר הבייטים המקורי. המטרה היא להגביר את המהירות שלהם. כדאי להשתמש ב-DataView כדי לקרוא ולכתוב נתונים שבהם endianness תהיה בעיה.

ב-DataView יש גם שיטות לכתיבת ערכים למאגרי נתונים זמניים. השמות של ה-setters האלה נקבעים באותו אופן כמו השמות של ה-getters, "set" ואחריו סוג הנתונים.

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

דיון על אנדיזציה

Endianness, או סדר הבייטים, הוא הסדר שבו מספרים עם כמה ביטים מאוחסנים בזיכרון המחשב. המונח large-endian מתאר ארכיטקטורת מעבד (CPU) שמאחסנת את הבייט המשמעותי ביותר תחילה; lit-endian, והבייט הכי פחות משמעותי. בחירת סוג ה-endianness שמופעלת בארכיטקטורה נתונה של מעבד היא שרירותית לחלוטין. יש סיבות טובות לבחור בכל אחת מהאפשרויות. למעשה, אפשר להגדיר מעבדים מסוימים כך שיתמכו גם בנתונים מסוג big-endian וגם בנתונים מסוג little-endian.

למה צריך להתייחס ל-endianness? הסיבה לכך פשוטה. כשקוראים או כותבים נתונים מהדיסק או מהרשת, צריך לציין את endianness של הנתונים. כך אפשר להבטיח שהנתונים יפורשו בצורה נכונה, ללא קשר לסוג ה-endianness של המעבד שפועל איתם. בעולם שלנו, שמבוסס יותר ויותר על רשתות, חיוני לספק תמיכה הולמת בכל סוגי המכשירים, עם endian גדול או קטן, שעשויים להידרש לעבוד עם נתונים בינאריים שמגיעים משרתים או משותפים אחרים ברשת.

ממשק DataView מיועד לקרוא ולכתוב נתונים בקבצים וברשת, או מהם. DataView פועל על נתונים עם סדר ביטים קצוב. צריך לציין את האנדים, בין אם גדולה או קטנה, בכל גישה של כל ערך, כדי להבטיח שתקבלו תוצאות עקביות ונכונות בקריאה או בכתיבה של נתונים בינאריים, לא משנה מהי סביבת המעבד (CPU) שבה הדפדפן פועל.

בדרך כלל, כשהאפליקציה קוראת נתונים בינאריים משרת, צריך לסרוק אותם פעם אחת כדי להמיר אותם למבני הנתונים שבהם האפליקציה משתמשת באופן פנימי. בשלב הזה צריך להשתמש ב-DataView. לא כדאי להשתמש בתצוגות של מערך מוקלד עם מספר בייטים (Int16Array , Uint16Array וכו') ישירות עם נתונים שנשלפו דרך XMLHttpRequest, FileReader או כל ממשק API אחר של קלט/פלט, כי תצוגות המערך המוקלדות משתמשות בישות המקורית של המעבד (CPU). בהמשך נסביר על כך.

נבחן כמה דוגמאות פשוטות. פורמט הקובץ Windows BMP היה הפורמט הסטנדרטי לאחסון תמונות בימים הראשונים של Windows. במסמך המקושר למעלה מצוין בבירור שכל ערכי המספרים השלמים בקובץ מאוחסנים בפורמט little-endian. לפניכם קטע קוד שמנתח את תחילת הכותרת של קובץ ה-BMP באמצעות הספרייה DataStream.js שמצורפת למאמר הזה:

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

הנה דוגמה נוספת, הפעם מהדגמה של עיבוד תמונה בטווח דינמי גבוה בפרויקט הדוגמאות של WebGL. ההדגמה הזו מורידה נתונים גולמיים מנקודה צפה (floating-endian) קטנים שמייצגים מרקמים בטווח דינמי גבוה, וצריך להעלות אותם ל-WebGL. זהו קטע הקוד שמפרש בצורה נכונה את ערכי הנקודה הצפה בכל הארכיטקטורות של המעבדים. נניח שהמשתנה arrayBuffer הוא ArrayBuffer שהורדתם מהשרת באמצעות XMLHttpRequest:

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

הכלל הוא: כשמקבלים נתונים בינאריים משרת האינטרנט, מבצעים סריקה אחת שלהם באמצעות DataView. קוראים את הערכים המספריים הנפרדים ושומרים אותם במבנה נתונים אחר, אובייקט JavaScript (כמויות קטנות של נתונים מובְנים) או תצוגת מערך ממוין (כמויות גדולות של נתונים). כך תוכלו לוודא שהקוד יפעל כמו שצריך בכל סוגי המעבדים. אפשר גם להשתמש ב-DataView כדי לכתוב נתונים לקובץ או לרשת, וחשוב לציין את הארגומנט littleEndian באופן מתאים לשיטות השונות של set כדי ליצור את פורמט הקובץ שאתם יוצרים או משתמשים בו.

חשוב לזכור שלכל הנתונים שעוברים ברשת יש פורמט ו-endianness (לפחות לערכים של כמה בייטים). חשוב להגדיר בבירור ולתעד את הפורמט של כל הנתונים שהאפליקציה שולחת ברשת.

ממשקי API לדפדפנים שמשתמשים במערכים מוקלדים

אציג סקירה כללית קצרה של ממשקי ה-API השונים של הדפדפנים שמשתמשים כרגע במערכים מוגדרים. החיתוך הנוכחי כולל את WebGL, Canvas, Web Audio API, XMLHttpRequests, WebSockets, Web Workers, Media Source API ו-File APIs. ברשימת ממשקי ה-API אפשר לראות ש-Typed Arrays מתאימים לעבודה עם מולטימדיה שמושפעת מאוד מהביצועים, וגם להעברת נתונים בצורה יעילה.

WebGL

השימוש הראשון ב-Typed Arrays היה ב-WebGL, שם הוא משמש להעברת נתוני מאגר ונתוני תמונה. כדי להגדיר את התוכן של אובייקט מאגר של WebGL, משתמשים בקריאה gl.bufferData()‎ עם מערך Typed.

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

מערכי Typed משמשים גם להעברת נתוני טקסטורה. בדוגמה הבאה מוסבר איך מעבירים תוכן של טקסטורה באמצעות מערך Typed.

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

צריך גם מערכי Typed כדי לקרוא פיקסלים מההקשר של WebGL.

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

קנבס בדו-ממד

לאחרונה, אובייקט Canvas ImageData הותאם לעבודה עם מפרט Typed Arrays. עכשיו אפשר לקבל ייצוג של הפיקסלים ברכיב הקנבס באמצעות Typed Arrays. זה שימושי כי עכשיו אפשר גם ליצור ולערוך מערכי פיקסלים של קנבס בלי להתעסק עם רכיב הקנבס.

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

XMLHttpRequest קיבל הגדלה של המערך Typed ועכשיו אפשר לקבל תגובת Typed Array במקום לנתח מחרוזת JavaScript לתוך מערך מוקלד. זה שימושי מאוד להעברת נתונים שאוחזרו ישירות ל-API של מולטימדיה ולניתוח קבצים בינאריים שאוחזרו מהרשת.

כל מה שצריך לעשות הוא להגדיר את responseType של אובייקט XMLHttpRequest ל-'arraybuffer'.

xhr.responseType = 'arraybuffer';

חשוב לזכור: כשמורידים נתונים מהרשת, חייבים להיות מודעים לבעיות הקשורות למתחרים. עיינו בקטע למעלה על שם מקומי.

ממשקי API של קבצים

ה-FileReader יכול לקרוא את תוכן הקובץ כ-ArrayBuffer. לאחר מכן אפשר לצרף תצוגות של מערך מוקלד ו-DataViews למאגר הנתונים הזמני כדי לשנות את התוכן שלו.

reader.readAsArrayBuffer(file);

חשוב גם לזכור את endianness. אתם יכולים לעיין בקטע 'סקירה כללית'.

אובייקטים שניתנים להעברה

אובייקטים שניתנים להעברה ב-postMessage הופכים את העברת הנתונים הבינאריים לחלונות אחרים ולעובדי אינטרנט במהירות רבה יותר. כששולחים אובייקט ל-Worker כ-Transferable, אי אפשר לגשת לאובייקט בשרשור השליחה, ו-Worker המקבל מקבל את הבעלות על האובייקט. כך אפשר לבצע הטמעה עם אופטימיזציה גבוהה, שבה הנתונים שנשלחים לא מועתקים, אלא רק הבעלות על מערך ה-Typed מועברת לנמען.

כדי להשתמש באובייקטים שניתן להעביר עם Web Workers, צריך להשתמש בשיטה webkitPostMessage ב-worker. השיטה webkitPostMessage פועלת בדיוק כמו postMessage, אבל היא מקבלת שני ארגומנטים במקום רק אחד. הארגומנט השני שנוסף הוא מערך של אובייקטים שרוצים להעביר לעובד.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

כדי לקבל את האובייקטים בחזרה מה-worker, ה-worker יכול להעביר אותם בחזרה לשרשור הראשי באותו אופן.

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

אפס עותקים, יש!

Media Source API

לאחרונה נוספו לרכיבי המדיה גם כמה תכונות של מערך מוגדר (Typed Array) בצורת Media Source API. אפשר להעביר ישירות מערך Typed שמכיל נתוני וידאו לאלמנט וידאו באמצעות webkitSourceAppend. כך רכיב הווידאו מצרף את נתוני הסרטון אחרי הסרטון הקיים. האפשרות SourceAppend נהדרת לשימוש במודעות מעברון, בפלייליסטים, בסטרימינג ובשימושים אחרים שבהם רוצים להפעיל כמה סרטונים באמצעות רכיב וידאו אחד.

video.webkitSourceAppend(uint8Array);

WebSockets בינאריים

אפשר גם להשתמש במערכים מוגדרים עם WebSockets כדי להימנע מהצורך להפוך את כל הנתונים למחרוזת. נהדרת לכתיבת פרוטוקולים יעילים ולצמצום נפח התנועה ברשת.

socket.binaryType = 'arraybuffer';

איזה מזל! סיכום של סקירת ה-API. עכשיו נעבור לספריות של צד שלישי לטיפול במערכים מוגדרים.

ספריות של צד שלישי

jDataView

jDataView מטמיע תוסף DataView לכל הדפדפנים. בעבר, DataView הייתה תכונה של WebKit בלבד, אבל עכשיו יש תמיכה בה ברוב הדפדפנים האחרים. צוות המפתחים של Mozilla עובד על תיקון שיאפשר להפעיל את DataView גם ב-Firefox.

איתמר ביטמן מצוות קשרי המפתחים של Chrome כתב דוגמה קטנה לקורא תגי MP3 ID3 שמשתמש ב-jDataView. דוגמה לשימוש מתוך הפוסט בבלוג:

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

stringencoding

כרגע קשה לעבוד עם מחרוזות במערכים מוגדרים, אבל אפשר להיעזר בספריית stringencoding. ב-Stringencoding מוטמע המפרט המוצע לקידוד מחרוזות של מערך מוגדר, כך שזו גם דרך טובה לקבל מושג לגבי מה שעומד לקרות.

דוגמה בסיסית לשימוש ב-stringencoding:

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

כתבתי ספרייה קטנה למניפולציה של מערכים מסוג BitView.js בשם BitView.js. כפי שהשם כתוב, הוא דומה ל-DataView, אבל הוא פועל עם ביטים. באמצעות BitView אפשר לקבל ולהגדיר את הערך של ביט במרווח ביט נתון ב-ArrayBuffer. ב-BitView יש גם שיטות לאחסון ולטעינה של מספרים שלמים באורך 6 ו-12 ביט בשינויי ביט שרירותיים.

מספרים שלמים של 12 ביט נוחים לעבודה עם קואורדינטות מסך, כי בדרך כלל במסכים יש פחות מ-4,096 פיקסלים בצד הארוך יותר. שימוש ב-ints של 12 ביט במקום ב-ints של 32 ביט מאפשר להפחית את הגודל ב-62%. דוגמה קיצונית יותר: עבדתי עם קובצי Shapefile שמשתמשים ב-floats של 64 ביט לצורך הקואורדינטות, אבל לא הייתי צריך את הדיוק הזה כי המודל היה אמור להופיע רק בגודל המסך. המעבר לקואורדינטות בסיס של 12 ביט עם דלתא של 6 ביט כדי לקודד שינויים מהקואורדינטה הקודמת צמצם את גודל הקובץ לעשירית. כאן אפשר לראות הדגמה של הנושא.

דוגמה לשימוש ב-BitView.js:

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

אחד מהדברים המעניינים ביותר במערכים מסוגים מסוימים הוא שהם מאפשרים לטפל בקלות רבה יותר בקבצים בינאריים ב-JavaScript. במקום לנתח מחרוזת תו אחר תו ולהמיר את התווים באופן ידני למספרים בינאריים וכו', עכשיו אפשר לקבל ArrayBuffer באמצעות XMLHttpRequest ולעבד אותו ישירות באמצעות DataView. כך קל יותר לטעון בקובץ MP3 ולקרוא את תגי המטא-נתונים לשימוש בנגן האודיו. לחלופין, אפשר לטעון קובץ shapefile ולהפוך אותו למודל WebGL. אפשר גם לקרוא את תגי ה-EXIF מקובץ JPEG ולהציג אותם באפליקציית המצגת.

הבעיה ב-ArrayBuffer XHRs היא שקשה לקרוא מהמאגר נתונים שדומים למבנה. DataView טוב לקריאת מספר מספרים בבת אחת באופן בטוח לשימוש פנימי, ותצוגות של מערך מסוגננות מתאימות לקריאת מערכים של מספרים אנדיאניים מותאמים לגודל הרכיב. מה שהרגנו חסר הוא דרך לקרוא מערכים ומבנים של נתונים בצורה נוחה ובטוחה לקצה. מזינים את DataStream.js.

DataStream.js היא ספרייה של מערכי Typed שמאפשרת לקרוא ולכתוב סקלר, מחרוזות, מערכים ומבנים של נתונים מ-ArrayBuffers באופן שדומה לקובץ.

דוגמה לקריאה של מערך של ערכים מסוג float מ-ArrayBuffer:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

המקום שבו DataStream.js שימושי במיוחד הוא בקריאת נתונים מורכבים יותר. נניח שיש לכם שיטה שקוראת סמנים של JPEG:

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

לחלופין, אפשר להשתמש ב-method DataStream.readStruct כדי לקרוא במבני נתונים. שיטת Read Along מקבלת מערך הגדרת struct שמכיל את הסוגים של חברי ה-struct. יש לו פונקציות קריאה חוזרת (callback) לטיפול בסוגים מורכבים, ולטיפול במערכים של נתונים ובמבנים מוטמעים:

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

כפי שאפשר לראות, הגדרת המבנה היא מערך שטוח של צמדים [name, type]. כדי ליצור מבנים מורכבים בתצוגת עץ, צריך להשתמש במערך של הסוג. מגדירים מערכי נתונים באמצעות מערך בן שלושה רכיבים, כאשר הרכיב השני הוא סוג רכיב המערך והרכיב השלישי הוא אורך המערך (כמספר, כהפניה לשדה שנקרא בעבר או כפונקציית קריאה חוזרת (callback)). הרכיב הראשון של הגדרת המערך אינו בשימוש.

הערכים האפשריים לסוג הם:

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to '*', elements are read from the
                        DataStream until a read fails.

כאן אפשר לראות דוגמה פעילה לקריאת מטא-נתונים של קובצי JPEG. הדגמה משתמשת ב-DataStream.js לקריאת המבנה ברמת התג של קובץ ה-JPEG (יחד עם ניתוח חלקי של EXIF), וב-jpg.js לפענוח ולהצגת התמונה בפורמט JPEG ב-JavaScript.

ההיסטוריה של מערכים מוקלדים

המערכים המודעים לטיפוסים הופיעו בשלב מוקדם של ההטמעה של WebGL, כשגילינו שהעברת מערכים של JavaScript למנהל הגרפיקה גורמת לבעיות בביצועים. במערכים של JavaScript, הקישור ל-WebGL היה צריך להקצות מערך מקומי ולמלא אותו על ידי סריקה של מערך ה-JavaScript והמרה של כל אובייקט JavaScript במערך לסוג המקומי הנדרש.

כדי לפתור את צוואר הבקבוק של המרת הנתונים, Vladimir Vukicevic מ-Mozilla כתב את CanvasFloatArray: מערך של משתני float בסגנון C עם ממשק JavaScript. עכשיו אפשר לערוך את CanvasFloatArray ב-JavaScript ולהעביר אותו ישירות ל-WebGL בלי לבצע עבודה נוספת בקישור. באיטרציות נוספות, השם של CanvasFloatArray השתנה ל-WebGLFloatArray, שהשם השתנה ל-Float32Array ופוצל ל-ArrayBuffer ולתצוגת Float32Array שהוקלדה כדי לגשת למאגר הנתונים הזמני. הוספנו גם סוגים לגדלים אחרים של מספרים שלמים ומספרים עשרוניים, ולוריאציות עם סימן או ללא סימן.

שיקולים לגבי התכנון

מאז ומתמיד, העיצוב של מערכי Typed נקבע על סמך הצורך להעביר נתונים בינאריים לספריות מקוריות בצורה יעילה. לכן, תצוגות מערכי הנתונים המודעים לסוג פועלות על נתונים מותאמים בסדר ביטים מקורי של מעבד המארח. ההחלטות האלה מאפשרות ל-JavaScript להגיע לביצועים מקסימליים במהלך פעולות כמו שליחת נתוני קודקודים לכרטיס הגרפיקה.

DataView תוכננה במיוחד לקלט/פלט (I/O) בקובץ וברשת, שבהם הנתונים תמיד מאוגדים באופן מקומי, וייתכן שהם לא יתאימו לביצועים מקסימליים.

הפיצול בתכנון בין הרכבת נתונים בזיכרון (באמצעות תצוגות מערך מוגדרות-סוג) לבין קלט/פלט (באמצעות DataView) היה מכוון. מנועי JavaScript מודרניים מבצעים אופטימיזציה משמעותית של תצוגות מערכי הטיפוסים, ומשיגים ביצועים גבוהים בפעולות מספריות באמצעותן. רמות הביצועים הנוכחיות של תצוגות מערכי הטיפוסים התאפשרו בזכות החלטת התכנון הזו.

קובצי עזר