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

Ilmari Heikkinen

מבוא

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

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

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

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

תצוגות של מערך שהוקל

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

// 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. האפשרות הזו שימושית במיוחד לאלגוריתמים של עיבוד תמונות בלוח הציור, כי עכשיו כבר לא צריך למהר את המתמטיקה של עיבוד התמונות באופן ידני כדי למנוע חריגה מטווח ה-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 (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-bit unsigned and לאחר שני int של 16 סיביות, ולאחר מכן מערך מטען ייעודי (payload) של צפים ב-32 סיביות. אפשר לנסות לקרוא את הטקסט הזה עם תצוגות מסוג מערך מוקלדות, אבל קצת מסורבל. בעזרת DataView ניתן לקרוא את הכותרת ולהשתמש בתצוגת מערך מקלידים עבור המערך הצף.

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);
}

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

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

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

ל-DataView יש גם שיטות לכתיבת ערכים למאגרי אחסון. השמות של הפרמטרים המגדירים האלה יהיו זהים לשמות של המק"טים, "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), או סדר בייטים, הוא הסדר שבו מספרים של בייטים מרובים מאוחסנים בזיכרון המחשב. המונח big-endian מתאר ארכיטקטורת מעבד (CPU) שמאחסנת קודם את הבייטים המשמעותיים ביותר, Little-endian – הבייט הפחות משמעותי קודם. מידת האותנטיות שבה נעשה שימוש בארכיטקטורת מעבד נתונה היא שרירותית לחלוטין; יש סיבות טובות לבחור בכל אחת מהן. למעשה, אפשר להגדיר חלק מהמעבדים כך שיתמכו בנתונים מפלטפורמות גדולות וקטנות.

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

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

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

נבחן כמה דוגמאות פשוטות. פורמט הקובץ Windows BMP היה בעבר הפורמט הסטנדרטי לאחסון תמונות בימים הראשונים של Windows. בתיעוד שקישור אליו מופיע למעלה ניתן לראות בבירור שכל ערכי המספרים השלמים בקובץ מאוחסנים בפורמט של אות קטנה. זהו קטע קוד שמנתח את ההתחלה של כותרת ה-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. בהדגמה הזו מתבצעת הורדה של נתוני נקודה צפה גולמיים קטנים, שמייצגים מרקמים בטווח דינמי גבוה, ועליו להעלות אותם ל-WebGL. זהו קטע הקוד שמפרש כראוי את ערכי הנקודה הצפה בכל הארכיטקטורות של המעבד (CPU). נניח שהמשתנה "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 כדי להפיק את פורמט הקובץ שאתם יוצרים או משתמשים בו.

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

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

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

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);

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

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 Arrays כדי לקרוא פיקסלים מהקשר 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. עכשיו אפשר לקבל ייצוג של Typed Arrays של הפיקסלים ברכיב בד קנבס. התכונה הזו שימושית, מאחר שמעכשיו אפשר גם ליצור ולערוך מערכי פיקסלים של קנבס בלי להתעסק עם רכיב הקנבס.

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

XMLHttpRequest2

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

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

xhr.responseType = 'arraybuffer';

זכור שעליך להיות מודע לבעיות קצה בעת הורדת נתונים מהרשת! מידע נוסף זמין בקטע 'סופיות' למעלה.

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

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

reader.readAsArrayBuffer(file);

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

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

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

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

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

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

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

אפס עותקים, איזה כיף!

ממשק API של מקור מדיה

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

video.webkitSourceAppend(uint8Array);

WebSockets בינאריים

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

socket.binaryType = 'arraybuffer';

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

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

jDataView

jDataView מטמיע ספריית shim של 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.
}

קידוד מחרוזת

כרגע קשה לעבוד עם מחרוזות ב-Typed Arrays, אבל יש כאן את ספריית המחרוזות (stringencoding) שעוזרת לעשות זאת. קידוד מחרוזות מיישם את מפרט הקידוד של מחרוזת מסוג Typed Array, כך שזאת גם דרך טובה לקבל תחושה לגבי העתיד.

לפניכם דוגמה לשימוש בסיסי בקידוד מחרוזת:

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

BitView.js

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

int של 12 סיביות מתאימות לעבודה עם קואורדינטות מסך, כי בתצוגות שמוצגות בדרך כלל יש פחות מ-4,096 פיקסלים בגודל הארוך יותר. אם תשתמשו ב-Int של 12 ביט במקום ב-32 סיביות, אתם מקבלים הקטנה של 62%. לצורך דוגמה קיצונית יותר, עבדתי עם קובצי shapefile שמשתמשים בצפים של 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 Arrays שקוראת וכותבת סקלר, מחרוזות, מערכים ומבנים של נתונים מ-ArrayBuffers בצורה דמוית קבצים.

דוגמה לקריאה במערך של צפים מ-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 כדי לקרוא במבנים של נתונים. שיטת ReadStruct מקבלת מערך של הגדרת struct שמכיל את הסוגים של חברי ה-struct. יש בה פונקציות קריאה חוזרת לטיפול בסוגים מורכבים וגם טיפול במערכי נתונים ובמבנים מקננים:

// 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]-צמדים. כדי ליצור מבנים מקננים, יש ליצור מערך עבור הסוג. מערכים מוגדרים באמצעות מערך שלושה רכיבים שבו הרכיב השני הוא סוג רכיב המערך והרכיב השלישי הוא אורך המערך (כמספר, כהפניה לשדה שנקרא בעבר או כפונקציית קריאה חוזרת). הרכיב הראשון של הגדרת המערך אינו בשימוש.

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

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 במערך לסוג ה-Native הנדרש.

כדי לתקן את צוואר הבקבוק של המרת הנתונים, ולדימיר ווקיצ'וויק מ-Mozilla כתב את CanvasFloatArray: מערך צף בסגנון C עם ממשק JavaScript. עכשיו תוכלו לערוך את CanvasFloatArray ב-JavaScript ולהעביר אותו ישירות ל-WebGL בלי שתצטרכו לבצע פעולות נוספות בקישור. באיטרציות נוספות, השם CanvasFloatArray השתנה ל-WebGLFloatArray, ששמו השתנה ל-Float32Array ופוצל ל-ArayBuffer גיבוי ולתצוגת Float32Array שהוקלדה כדי לגשת למאגר הזמני. נוספו גם סוגים אחרים לגדלים אחרים של מספרים שלמים ונקודות צפות (floating-point) וכן לווריאציות חתומות/לא חתומות.

שיקולי עיצוב

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

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

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

קובצי עזר