לדלג לתוכן

ייצוג והמרת נתונים

שני שדות שנראים כמו bool אינם חייבים להיות באותו גודל או באותו קידוד. ה־BOOL של WinAPI הוא מספר בן 32 ביט, bool של C++ הוא טיפוס עם חוזה אחר, ו־VARIANT_BOOL של COM משתמש בייצוג שבו אמת מיוצגת בדרך מיוחדת. לפני שנכתוב StructLayout נרשום את ה־layout הטבעי ואת כללי ההמרה, ונבדוק offsets משני הצדדים.

נשתמש במבנה פשוט של פרוטוקול C שמכיל שדות ברוחב קבוע. הקוד הראשון הוא תוכנית C קטנה, והשני תוכנית C# נפרדת. שניהם צריכים לדווח על אותם offsets תחת ה־ABI וה־packing שנבחרו.

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
typedef struct result_v1 {
    uint32_t size;
    int32_t ok;
    uint64_t total;
} result_v1;
int main(void) {
    printf("size=%zu ok=%zu total=%zu\n", sizeof(result_v1),
        offsetof(result_v1, ok), offsetof(result_v1, total));
}
using System.Runtime.InteropServices;

Console.WriteLine($"size={Marshal.SizeOf<ResultV1>()} " +
    $"ok={Marshal.OffsetOf<ResultV1>(nameof(ResultV1.Ok))} " +
    $"total={Marshal.OffsetOf<ResultV1>(nameof(ResultV1.Total))}");

[StructLayout(LayoutKind.Sequential)]
struct ResultV1
{
    public uint Size;
    public int Ok;
    public ulong Total;
}

בחרנו int לשדה Ok כדי שהייצוג הבינארי יהיה מפורש ולא ידרוש החלטה סמויה של marshaller. ה־wrapper יכול לחשוף bool שמחשב Ok != 0. אם הפרוטוקול דורש רק אפס או אחד, נבדוק זאת בנפרד; התאמה ל־BOOL אינה מחייבת שכל ערך אמת יהיה דווקא אחד. במבנה הזה offsets צפויים הם 0, 4 ו־8 וגודל 16 תחת ה־ABI הרגיל שנבדק, אבל עדיין נריץ את בדיקת הצד הטבעי במטריצת היעדים שלנו.

ה־LayoutKind.Sequential שומר סדר שדות עם כללי יישור. ה־Pack משנה את מגבלת היישור ואינו "תיקון כללי" לכל חוסר התאמה. אם הצד הטבעי לא משתמש ב־pack(1), הוספת Pack=1 במנוהל יכולה ליצור מבנה אחר לגמרי. ה־LayoutKind.Explicit מאפשר offsets מפורשים, למשל עבור union, אבל ערבוב references מנוהלים עם שדות חופפים דורש מגבלות נוספות ואינו דרך לעקוף את מערכת הטיפוסים.

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

בממשק חדש נעדיף buffer עם אורך על פני הסתמכות על strlen כאשר הנתון יכול להכיל אפסים. עבור פלט בגודל משתנה אפשר להשתמש בפרוטוקול דו־שלבי: קריאה שמחזירה גודל נדרש, הקצאה מוגבלת וקריאה שממלאת. הנתון יכול להשתנות בין שתי הקריאות, ולכן נידרש לתוצאת buffer-too-small ולמספר ניסיונות מוגבל. הקצאה לפי אורך חיצוני בלי תקרת גודל אינה פתרון בטוח רק מפני שהמערך מנוהל.

נבחין גם בין char של C# שהוא יחידת UTF-16 לבין תו Unicode שלם. זוג surrogate יכול לייצג scalar אחד, וקידוד UTF-8 יכול לצרוך מספר byte-ים. המרה של length בין שכבות חייבת לציין יחידות. עבור שמות קבצים ב־WinAPI נשתמש בחוזה UTF-16 המתועד; עבור פרוטוקול C משלנו נוכל לבחור UTF-8 ולדווח אורכים ב־byte-ים.

התיעוד על התאמת marshalling של מבנים ועל שיטות מומלצות ל־interop מסביר את אפשרויות הייצוג. בבדיקת תאימות נרצה sizeof, offsets ו־round trip של ערכי קצה, ולא רק תוכנית שלא קרסה בקלט הראשון.