דוגמת גשר COM שקורא ל-DLL של 64bit מאפליקציית 32bit
· עודכן בתאריך: · Go Komura · COM, פיתוח Windows, 32bit, 64bit
הדרישה לקרוא מאפליקציית 32bit ל-DLL של 64bit היא די טיפוסית ב-Windows. במיוחד כשרוצים להשתמש רק בפונקציונליות של צד ה-64bit תוך שמירה על נכסים קיימים, מבנה של גשר COM נוטה להיות פתרון מעשי.
קהל היעד: מי שמתחזק אפליקציית Windows קיימת של 32bit ורוצה להשתמש ב-DLL או בספרייה של צד ה-64bit. כתוב כך שאפשר לקרוא גם אם רק “שמעתם על COM אבל מעולם לא בניתם דבר בעצמכם”.
סביבה נדרשת: גרסת 64bit של Windows (x64), וסביבת פיתוח שיכולה לכתוב C# (כמו Visual Studio). רישום שרת COM עבור כל המחשב (תחת HKEY_LOCAL_MACHINE) דורש הרשאות מנהל. את התפיסה הבסיסית של COM סיכמנו במאמר “מה זה COM — למה התכנון של COM ב-Windows עדיין יפה”.
תוכן עניינים
מפת הידע של המאמר
אפליקציית 32bit לא יכולה לטעון ישירות DLL של 64bit כ-in-proc COM בגלל אי-התאמת מבנה הביטים, ולכן במאמר הזה מפרידים את העיבוד בצד ה-64bit לתהליך נפרד כ-COM LocalServer (שרת EXE), וקוראים לו בצורה מוקלדת דרך ממשק COM משותף ב-IDL וב-TypeLib. מרשלינג ו-Proxy/Stub מגשרים על הקריאה שחוצה את גבול התהליכים, וההנחה היא רישום כפול הן ל-CLSID והן ל-ProgID, וגם ל-WOW64 Registry Redirector שבו תת-המפתח של ה-CLSID נבדל בין תצוגת 32bit לתצוגת 64bit. רישום עבור כל המחשב דורש הרשאות מנהל, ומכיוון שב-.NET (מגרסה 5 ואילך) ה-EnableComHosting הסטנדרטי מיועד רק ל-in-proc, יש לכתוב בעצמכם את לוגיקת הרישום של שרת ה-Out-of-proc.
flowchart LR
accTitle: מפת הידע של גשר COM בין 32bit ל-64bit
accDescr: תרשים המראה איך המבנה שבו אפליקציית 32bit קוראת ל-DLL של 64bit דרך שרת EXE של COM LocalServer, קשור לספריית טיפוסים, מרשלינג, רישום CLSID/ProgID, ו-WOW64 Registry Redirector.
com_bridge_32_64["גשר COM בין 32 ל-64 סיביות"]
com_localserver["COM LocalServer (שרת COM בתהליך נפרד)"]
type_library["ספריית טיפוסים (TLB)"]
com_marshaling["מרשלינג (marshaling)"]
proxy_stub["Proxy/Stub"]
in_proc_com["In-proc COM (שרת DLL)"]
bitness_match_requirement["דרישת התאמת bitness"]
clsid["CLSID(Class ID)"]
progid["ProgID(Programmatic Identifier)"]
wow64_registry_redirector["מנתב הרישום של WOW64"]
wow6432node["WOW6432Node"]
reg_exe["reg.exe(/reg:32 /reg:64)"]
class_factory_registration["רישום מפעל המחלקות (class factory)"]
admin_rights["הרשאות מנהל"]
regasm["Regasm.exe"]
dotnet[".NET (מ-Core ואילך)"]
dotnet_framework[".NET Framework"]
dotnet_comhost["EnableComHosting/.comhost.dll ב-.NET (5 ואילך)"]
com["COM (Component Object Model)"]
com_bridge_32_64 -->|"משתמש ב"| com_localserver
com_bridge_32_64 -->|"מחייב"| type_library
com_localserver -->|"משתמש ב"| com_marshaling
com_marshaling -->|"משתמש ב"| proxy_stub
in_proc_com -->|"מחייב"| bitness_match_requirement
com_localserver -->|"מצמצם"| bitness_match_requirement
com_localserver -->|"מחייב"| clsid
progid -->|"מחייב"| clsid
com_localserver -.->|"מחייב"| wow64_registry_redirector
clsid -.->|"נשמר ב"| wow6432node
wow64_registry_redirector -->|"מוגדר באמצעות"| reg_exe
com_localserver -->|"מחייב"| class_factory_registration
com_bridge_32_64 -.->|"מחייב"| admin_rights
regasm -->|"שימוש לא מומלץ ל"| dotnet
regasm -->|"מחייב"| dotnet_framework
dotnet_comhost -->|"משתמש ב"| dotnet
dotnet_comhost -->|"שימוש לא מומלץ ל"| com_bridge_32_64
dotnet -.->|"מחייב"| clsid
com -.->|"משתמש ב"| type_library
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 19, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
1. התרחיש
מקרה שבו רוצים להשאיר את האפליקציה הקיימת של 32bit כמות שהיא, אך להשתמש בעיבוד של DLL בן 64bit. אלא שתהליך 32bit אינו יכול לטעון DLL של 64bit. זו מגבלה ברמת מערכת ההפעלה, ולא משהו שניתן לפתור בתחכום.
מצבים נפוצים הם:
- האפליקציה הקיימת של 32bit היא נכס גדול, ואי אפשר להעביר אותה במהירות
- יש פונקציונליות חדשה בצד ה-DLL של 64bit, או שספריות התלות הן 64bit בלבד
- רוצים לקרוא מצד ה-32bit “בצורה מוקלדת”
בשילוב הזה, הדרך לקרוא בתוך אותו תהליך חסומה מלכתחילה.
flowchart TB
accTitle: התרחיש הנתון
accDescr: תרשים המראה שהאפליקציה הקיימת של 32bit רוצה להשתמש בפונקציונליות של DLL בן 64bit, אך תהליך 32bit אינו יכול לטעון DLL של 64bit בגלל מגבלה ברמת מערכת ההפעלה, כך שהדרך לקרוא באותו תהליך חסומה.
app["אפליקציית 32bit קיימת"] --> want["רוצה להשתמש ב-DLL של 64bit"]
want --> deny["אי אפשר לטעון באותו תהליך"]
deny -.-> os["מגבלת מערכת הפעלה, אין עקיפה"]
איור 1: תהליך 32bit אינו יכול לטעון DLL של 64bit, ולכן אין מלכתחילה דרך לקרוא באותו תהליך.
2. שיטת הפתרון
מכאן ואילך מופיע מינוח מקצועי, אז נסדר קודם כמה מילים מינימליות.
| מונח | משמעות |
|---|---|
| In-proc COM (שרת DLL) | טוענים את רכיב ה-COM באותו תהליך כמו הצד הקורא. מהיר, אך לא נטען אם מספר הביטים לא תואם |
| Out-of-proc COM (שרת EXE) | מפעילים את רכיב ה-COM כתהליך נפרד. אפשר לשתף פעולה גם כשמספר הביטים שונה |
| LocalServer | שרת COM שרץ כתהליך נפרד באותו מחשב. רושמים את הנתיב ל-EXE במפתח LocalServer32 ברג’יסטרי |
| IDL / TypeLib | ההגדרה (IDL) של צורת הממשק (שמות שיטות, טיפוסי ארגומנטים), וגרסתו הבינארית (TypeLib). משמשים כדי ששני הצדדים יראו את אותו “חוזה” |
| Marshaling | סידור מחדש של ארגומנטים וערכים מוחזרים לצורה שאפשר לשלוח, כדי לחצות גבול תהליכים. ההפך הוא unmarshaling |
| Proxy / Stub | קוד ייצוג שמבצע בפועל את ה-marshaling. Proxy עומד בצד הקורא, Stub עומד בצד השרת |
| WOW6432Node | המיקום הפיזי שבו מאוחסן תוכן הרג’יסטרי עבור אפליקציות 32bit ב-Windows של 64bit. אותו שם מפתח מכיל תוכן שונה בצד 32bit ובצד 64bit |
יסוד הפתרון הוא הפרדה באמצעות Out-of-proc COM (שרת EXE). קוראים ל-DLL של 64bit מתוך שרת COM (EXE) של 64bit, ואפליקציית ה-32bit משתמשת בו דרך COM.
flowchart TB
accTitle: המבנה הבסיסי של גשר COM
accDescr: תרשים המראה שאפליקציית 32bit קוראת דרך COM לשרת COM של 64bit בתור EXE, והשרת קורא באופן פנימי ל-DLL של 64bit.
a32["אפליקציית 32bit"] -->|"קורא דרך COM"| srv["שרת COM של 64bit(EXE)"]
srv -->|"קורא באופן פנימי"| dll["DLL של 64bit"]
איור 2: את ה-DLL של 64bit ממקמים בתוך שרת EXE של 64bit, ואפליקציית 32bit משתמשת בשרת הזה דרך COM.
הזרימה היא כדלקמן.
- מכינים COM LocalServer (EXE) של 64bit, שקורא באופן פנימי ל-DLL של 64bit
- משתפים ממשק COM (IDL/TypeLib) וחושפים את הטיפוסים
- אפליקציית ה-32bit קוראת ל-COM בצורה “מוקלדת” (תקשורת דרך Proxy/Marshal)
עם זאת, יש גם נקודות לתשומת לב.
- הרישום של 32bit ו-64bit נפרד (כולל WOW6432Node)
- מבנים מותאמים-אישית דורשים תכנון marshaling
- יש תקורת IPC, ולכן צריך להיזהר מקריאות בתדירות גבוהה
במילים אחרות, הדרך הסטנדרטית היא “להעביר את העיבוד של 64bit לתהליך נפרד, ולגשר באמצעות COM”.
flowchart TB
accTitle: שלושת שלבי בניית הגשר
accDescr: תרשים המראה שמכינים COM LocalServer של 64bit, חושפים את הטיפוסים ב-IDL/TypeLib, ואפליקציית 32bit קוראת בצורה מוקלדת - שלושה שלבים שמרכיבים את הגשר.
s1["הכנת COM LocalServer של 64bit"] --> s2["חשיפת טיפוסים ב-IDL / TypeLib"]
s2 --> s3["אפליקציית 32bit קוראת בצורה מוקלדת"]
s3 -.-> ps["התקשורת דרך Proxy / Marshal"]
איור 3: שלושת השלבים — הכנת ה-LocalServer, חשיפת הטיפוסים, והקריאה המוקלדת — יוצרים יחד את הגשר.
3. זרימת העיבוד (תרשים רצף)
להלן הזרימה כאשר אפליקציית 32bit קוראת לעיבוד של DLL בן 64bit.
sequenceDiagram
accTitle: זרימת הקריאה מ-32bit ל-64bit
accDescr: תרשים רצף המראה איך קריאה מאפליקציית 32bit עוברת דרך Proxy, תקשורת בין-תהליכית ו-Stub אל שרת ה-64bit וה-DLL, והתוצאה חוזרת באותו מסלול.
participant App as לקוח 32bit
box rgba(100,100,255,0.1) תשתית ה-marshaling של COM המותקנת מטפלת
participant Proxy as COM Proxy<br/>(צד 32bit)
participant RPC as RPC/IPC<br/>(תקשורת בין-תהליכית)
participant Stub as COM Stub<br/>(צד 64bit)
end
participant Server as שרת COM של 64bit<br/>(EXE)
participant DLL as DLL של 64bit
App->>Proxy: ICalcService.Add(1, 2)
rect rgba(100,100,255,0.1)
Note over Proxy: מבצע marshaling לפרמטרים
Proxy->>RPC: נתונים מסודרים (serialized)
RPC->>Stub: מועברים דרך גבול התהליכים
Note over Stub: מבצע unmarshaling לפרמטרים
end
Stub->>Server: Add(1, 2)
Server->>DLL: קריאה לפונקציה native
DLL-->>Server: תוצאה: 3
Server-->>Stub: תוצאה: 3
rect rgba(100,100,255,0.1)
Note over Stub: מבצע marshaling לערך המוחזר
Stub-->>RPC: תוצאה מסודרת (serialized)
RPC-->>Proxy: מועברת דרך גבול התהליכים
Note over Proxy: מבצע unmarshaling לערך המוחזר
end
Proxy-->>App: תוצאה: 3
איור 4: הקריאה מאפליקציית 32bit עוברת דרך Proxy, תקשורת בין-תהליכית ו-Stub אל שרת ה-64bit וה-DLL, והתוצאה חוזרת באותו מסלול.
נקודות מרכזיות:
- אפליקציית 32bit יכולה לקרוא בבטיחות טיפוסים דרך הממשק
ICalcService - זמן הריצה של COM חוצה את גבול התהליכים באמצעות DLL רשום של Proxy/Stub, marshaler מסוג TypeLib, marshaler סטנדרטי ועוד
- בגלל התקורה של תקשורת בין-תהליכית, עדיף עיבוד באצווה על פני קריאות קטנות
flowchart TB
accTitle: עקרון גודל הקריאה
accDescr: תרשים המראה שלתקשורת בין-תהליכית יש תקורה, כך שקריאות קטנות וחוזרות מצטברות, ואילו ריכוז לעיבוד באצווה מקטין את ההשפעה - וזו הגישה המועדפת.
ipc["תקורת התקשורת הבין-תהליכית"] --> fine["קריאות קטנות חוזרות מצטברות"]
ipc --> batch["ריכוז לעיבוד באצווה מקטין השפעה"]
batch -.-> rec["זו הגישה המועדפת"]
איור 5: לכל חציית גבול תהליכים יש עלות, ולכן עדיף לרכז את הקריאות לגודל גס.
4. קוד לדוגמה (רעיון)
4.1. ממשק משותף, שרת ולקוח
זהו רעיון עקרוני בלבד. כדי שזה יפעל בפועל, יידרש גם הרישום שמתואר בהמשך בסעיף 4.2.
// ממשק משותף (שקול ל-IDL)
[ComVisible(true)]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// COM LocalServer של 64bit (צד ה-EXE)
[ComVisible(true)]
[Guid("1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("KomuraSoft.CalcService")]
public class CalcService : ICalcService
{
public int Add(int a, int b)
{
// כאן קוראים ל-DLL של 64bit
return a + b;
}
}
// צד אפליקציית 32bit (הלקוח)
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService");
var calc = (ICalcService)Activator.CreateInstance(t);
int result = calc.Add(1, 2);
בצורה הזו, צד ה-32bit יכול לטפל בזה “בצורה מוקלדת”. COM משתמש באופן פנימי ב-Proxy/Stub וקורא דרך IPC בשבילכם.
הסיבה שמוסיפים [ProgId("KomuraSoft.CalcService")] היא כדי שהלקוח יוכל למצוא אותו באמצעות Type.GetTypeFromProgID("KomuraSoft.CalcService"). ProgID הוא רק “כינוי קריא לבני אדם”, ומה שבאמת מוצא את השרת הוא רישום ה-CLSID שיוסבר בהמשך.
flowchart LR
accTitle: מ-ProgID עד הגעה לשרת
accDescr: תרשים המראה שה-ProgID שהלקוח מציין הוא רק כינוי קריא לבני אדם, שממנו נגזר CLSID, ורישום ה-CLSID הוא מה שמאתר בפועל את השרת.
progid["ProgID(כינוי קריא לבני אדם)"] --> clsid["רישום CLSID"]
clsid --> srv["השרת נמצא"]
איור 6: ProgID הוא כינוי הכניסה, אבל מה שמצביע בפועל על השרת הוא רישום ה-CLSID.
4.2. שלבי הרישום המינימליים
COM הוא מנגנון שבו “זמן הריצה של COM שולף CLSID רשום ברג’יסטרי ומפעיל אותו”, ולכן קוד שלא נרשם לעולם לא יפעל (Type.GetTypeFromProgID יחזיר null, או ש-CreateInstance יחזיר REGDB_E_CLASSNOTREG). הרישום הדרוש עבור שרת EXE (LocalServer) מצטמצם בסופו של דבר לשלושה מפתחות בלבד.
| מה נרשם | מפתח | ערך |
|---|---|---|
| התאמת ProgID ← CLSID | HKEY_CLASSES_ROOT\KomuraSoft.CalcService\CLSID |
{1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11} |
| CLSID ← נתיב ה-EXE | HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\LocalServer32 |
הנתיב המלא ל-EXE של שרת COM בן 64bit |
| CLSID ← חיפוש הפוך ל-ProgID | HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\ProgID |
KomuraSoft.CalcService |
כאן מסתתרת המלכודת שהיא ממש נושא המאמר הזה. התיעוד של Microsoft קובע במפורש ש-HKEY_LOCAL_MACHINE\SOFTWARE\Classes משותף בין אפליקציות 32bit ל-64bit, בעוד שתת-המפתח CLSID שתחתיו (וגם Interface וכדומה) נפרד בין צד 32bit לצד 64bit (הישות הפיזית בצד 32bit היא WOW6432Node). כלומר, מפתח ה-ProgID נראה משני הצדדים לאחר כתיבה אחת בלבד, אבל אם רישום ה-CLSID לא נכתב גם בתצוגת 32bit וגם בתצוגת 64bit, לקוח 32bit לא ימצא את השרת.
flowchart TB
accTitle: החלק המשותף והחלק הנפרד ברישום
accDescr: תרשים המראה שמפתח ה-ProgID תחת Classes נראה משני הצדדים אחרי כתיבה אחת, בעוד תת-המפתח CLSID נפרד בין תצוגת 32bit ל-64bit ודורש כתיבה לשתיהן, וכשחסר צד 32bit השרת לא נמצא מלקוח 32bit.
progk["מפתח ProgID(תחת Classes)"] --> shared["נראה משני הצדדים"]
clsk["רישום תחת CLSID"] --> v64["נכתב בתצוגת 64bit"]
clsk --> v32["נכתב בתצוגת 32bit"]
v32 -.-> wow["בפועל נמצא ב-WOW6432Node"]
v32 -.-> warn["חסר = לא נראה מ-32bit"]
איור 7: מפתח ה-ProgID משותף, אבל תת-המפתח CLSID נפרד לפי תצוגת 32bit/64bit, ולכן צריך לרשום בשתיהן.
הדרך הבטוחה היא להשתמש בדגלים /reg:32 ו-/reg:64 של הפקודה reg בשורת פקודה עם הרשאות מנהל (Microsoft ממליצה שלא לכתוב את Wow6432Node בעצמכם בנתיב).
:: הריצו בשורת פקודה עם הרשאות מנהל
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
set SERVER=C:\Program Files\KomuraSoft\CalcServer.exe
:: 1) ProgID → CLSID (HKLM\SOFTWARE\Classes ישירות משותף בין 32/64)
reg add "HKLM\SOFTWARE\Classes\%PROGID%\CLSID" /ve /d "%CLSID%" /f
:: 2) CLSID → LocalServer32 ו-ProgID (CLSID נפרד בין 32/64, לכן כותבים לשניהם)
:: בערך של LocalServer32 יש להכניס את הנתיב לקובץ ההרצה כולל המרכאות.
:: אם כותבים /d "%SERVER%", המרכאות נעלמות בניתוח הארגומנטים של reg.exe, והערך
:: נשמר כ-C:\Program Files\... בלבד. COM מפרש זאת כשורת פקודה
:: ולכן הוא מחפש קודם את C:\Program.exe, שנחתך לפני הרווח
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID" /ve /d "%PROGID%" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:32
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID" /ve /d "%PROGID%" /f /reg:32
:: בדקו את הערך שנשמר. אם מוצג "C:\Program Files\..." עם מרכאות, זה תקין
reg query "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /reg:64
המרכאות ב-LocalServer32 אינן רק עניין של סדר וניקיון. בלי מרכאות, C:\Program Files\... ניתן לפירוש כ”C:\Program עם Files\... כארגומנט”. כתוצאה מכך, בסביבה שבה מישהו יכול ליצור קובץ C:\Program.exe, הוא עלול להיות מופעל תחילה. אם הנתיב מכיל רווחים, יש להוסיף מרכאות תמיד.
flowchart TB
accTitle: איך הפרשנות משתנה לפי מרכאות ב-LocalServer32
accDescr: תרשים המראה שאחסון הנתיב ללא מרכאות ב-LocalServer32 מאפשר פרשנות שנחתכת לפני הרווח, כך שקובץ בשם C:\Program.exe עלול להיות מופעל תחילה, בעוד אחסון עם המרכאות מפעיל את קובץ ה-EXE המיועד.
noq["אוחסן בלי מרכאות"] --> cut["אפשרית פרשנות שנחתכת לפני הרווח"]
cut --> evil["C:\Program.exe עלול לרוץ קודם"]
q["אוחסן עם המרכאות"] --> safe["ה-EXE המיועד רץ"]
איור 8: רישום נתיב עם רווח בלי מרכאות פותח פתח להפעלת קובץ הרצה אחר.
ביטול הרישום מתבצע פשוט על ידי מחיקת אותם מפתחות.
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:64
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:32
reg delete "HKLM\SOFTWARE\Classes\%PROGID%" /f
בנוסף, ה-EXE צריך להצהיר בפני COM, בזמן ההפעלה, שהוא “האחראי ל-CLSID הזה”. ב-C/C++ זה נעשה באמצעות CoRegisterClassObject, וב-.NET Framework באמצעות RegistrationServices.RegisterTypeForComClients. רישום הרג’יסטרי מטפל רק עד שלב הפעלת ה-EXE על ידי COM, ולכן אם ההצהרה הזו חסרה, מתקבל כישלון מבלבל שבו ה-EXE מופעל אך לא ניתן ליצור אובייקט.
flowchart TB
accTitle: רישום ברג'יסטרי מול הצהרת ה-EXE
accDescr: תרשים המראה שרישום הרג'יסטרי מטפל רק עד הפעלת ה-EXE על ידי COM, ורק כשה-EXE שהופעל מצהיר שהוא אחראי ל-CLSID אפשר ליצור אובייקט - כשההצהרה חסרה מתקבל כישלון מבלבל של EXE שרץ אך לא יוצר אובייקט.
regd["רישום ברג'יסטרי"] --> boot["COM מפעיל את ה-EXE"]
boot --> ann["ה-EXE מצהיר שהוא אחראי ל-CLSID"]
ann --> ok["ניתן ליצור אובייקט"]
boot -.->|"אם ההצהרה חסרה"| ng["רץ אך לא ניתן ליצור"]
איור 9: רישום הרג’יסטרי מטפל רק עד הפעלת ה-EXE; בלי הצהרת ה-EXE עצמו, לא ניתן ליצור אובייקט.
לשם השוואה, אם רק בודקים במהלך הפיתוח, אפשר לכתוב באותו מבנה תחת HKCU\SOFTWARE\Classes במקום HKLM, ולהירשם בלי הרשאות מנהל (HKEY_CLASSES_ROOT הוא תצוגה מאוחדת של HKLM ו-HKCU). עם זאת, גם HKCU\SOFTWARE\Classes\CLSID מטופל בנפרד עבור 32bit/64bit, כך שעדיין צריך לכתוב לשתי התצוגות.
4.3. השיטה שונה בין .NET Framework ל-.NET (מגרסה 5 ואילך)
הקוד למעלה כתוב ב-C#, אך השלבים משתנים מאוד בהתאם לגרסת .NET שבה משתמשים. אם מערבבים בין השתיים, נתקעים.
| .NET Framework | .NET (Core 3.0 / 5 ומעלה) | |
|---|---|---|
| כלי רישום | קיים RegAsm.exe (אבל הוא יוצר רישום InprocServer32 עבור in-proc, כך שבסופו של דבר צריך לכתוב את LocalServer32 בעצמכם) |
אין כלי מקביל ל-RegAsm |
| חשיפת COM סטנדרטית | מוסיפים attributes להרכבה (assembly) ומריצים RegAsm |
מגדירים <EnableComHosting>true</EnableComHosting> ליצירת *.comhost.dll ורושמים עם regsvr32 (in-proc בלבד) |
| יצירת TypeLib (.tlb) | אפשר ליצור עם TlbExp / RegAsm /tlb |
לא נתמך. כותבים IDL ידנית ומקמפלים ב-MIDL (מ-.NET 6 ואילך אפשר להטמיע את ה-.tlb שנוצר בתוך comhost) |
| ציון CLSID | אופציונלי | עבור מחלקה שנוצרת על ידי COM, חובה לציין CLSID במפורש |
| טיפול ב-AnyCPU | ניתן לשימוש מלקוחות 32bit ו-64bit כאחד | קובץ ה-*.comhost.dll הנלווה הוא בברירת מחדל 64bit, ולכן ניתן לשימוש רק מלקוחות 64bit |
המבנה במאמר הזה (שרת EXE) חורג מהטווח הסטנדרטי של EnableComHosting ב-.NET (מגרסה 5 ואילך), ולכן תצטרכו לכתוב את לוגיקת הרישום בעצמכם. יש דוגמה רשמית של Microsoft בשם OutOfProcCOM, ואם בונים בצד .NET, זו נקודת ההתחלה המומלצת.
flowchart TB
accTitle: הדרך לבניית שרת EXE ב-.NET (מגרסה 5 ואילך)
accDescr: תרשים המראה ששרת ה-EXE של המאמר הזה נמצא מחוץ לטווח EnableComHosting הסטנדרטי ב-.NET 5 ואילך, ולכן צריך לכתוב את הרישום בעצמכם, כשהדוגמה הרשמית OutOfProcCOM משמשת נקודת פתיחה.
exe["שרת EXE ב-.NET(מגרסה 5 ואילך)"] --> range["מחוץ לטווח EnableComHosting הסטנדרטי"]
range --> self["כותבים את הרישום בעצמכם"]
self -.-> smp["הדוגמה הרשמית OutOfProcCOM כנקודת פתיחה"]
איור 10: שרת EXE ב-.NET (מגרסה 5 ואילך) חורג מהפונקציונליות הסטנדרטית, ולכן הרישום נכתב באופן עצמאי כברירת מחדל.
5. קוד לדוגמה מלא
פרסמנו ב-GitHub דוגמה שמממשת את הרעיון שלמעלה בצורה שפועלת בפועל.
Call64bitDLLFrom32bitProc - GitHub
המאגר הזה כולל:
- Call64bitDLLFrom32bitProc/ - COM LocalServer של 64bit (EXE)
- X64DLL/ - DLL של 64bit (העיבוד בפועל)
- X86App/ - לקוח 32bit (WinForms)
- scripts/ - סקריפטים לרישום/ביטול רישום של שרת COM
אם בונים ורושמים לפי השלבים שמתוארים ב-README, אפשר לוודא בפועל שהקריאה מתהליך 32bit ל-DLL של 64bit אכן פועלת.
6. סיכום
גשר COM אינו פתרון-על, אלא מבנה שבו מובחן בבירור בין משימות שמתאימות למשימות שלא. לפני שמחליטים לאמץ אותו, נסו להתאים את המקרה שלכם לטבלה הבאה.
| מקרים מתאימים | מקרים לא מתאימים |
|---|---|
| אי אפשר לבנות מחדש את גוף אפליקציית ה-32bit (עלות השינוי לא משתלמת) | ניתן לבנות מחדש את צד ה-32bit ל-64bit מלכתחילה (זה הפתרון הקצר ביותר) |
| הקריאות בגודל גס (למשל תמונה אחת בקריאה, קובץ אחד בקריאה) | קריאות קטנות ותכופות של אלפי-עשרות אלפי פעמים (התקורה של IPC הופכת דומיננטית) |
| הנתונים המועברים הם מספרים, מחרוזות, מערכים — טיפוסים קלים ל-marshaling | מעבירים בכמות גדולה מצביעים גולמיים או מבנים מותאמים-אישית מורכבים |
| רוצים שגוף האפליקציה יישאר בחיים גם אם העיבוד בצד 64bit קורס (הפרדת תהליכים היא יתרון) | לא רוצים לכתוב לוגיקת התאוששות שמטפלת בקריסה או הפעלה מחדש של השרת |
| רוצים לשמור על קריאות מוקלדות (IntelliSense ובדיקה בזמן קומפילציה) | מספיק עיבוד באצווה חד-פעמי, והעברה דרך קלט/פלט סטנדרטי או קובץ מספיקה |
כתמונת המראה של השורה האחרונה, שווה תמיד לשקול גם את החלופה הפשוטה של “להפוך את העיבוד בצד ה-64bit ל-EXE קונסולי רגיל, ולתקשר דרך ארגומנטים וקבצים”. גשר COM משתלם כאשר רוצים לשמור על קריאות מוקלדות, וכאשר רוצים לקרוא שוב ושוב לשרת ששומר מצב.
flowchart TB
accTitle: נקודת ההפרדה בין גשר COM לחלופה הפשוטה
accDescr: תרשים המראה שאם נדרשת קריאה מוקלדת או שמירת מצב על ידי שרת שנקרא שוב ושוב, גשר COM עוזר; אם מספיק עיבוד באצווה חד-פעמי, שווה לשקול חלופה פשוטה של EXE קונסולי עם ארגומנטים וקבצים.
q{"נדרשת קריאה מוקלדת או שמירת מצב?"} -->|"כן"| br["גשר COM"]
q -->|"לא"| alt["EXE קונסולי עם ארגומנטים וקבצים"]
איור 11: הצורך בקריאה מוקלדת ובשמירת מצב הוא מה שמפריד בין גשר COM לחלופה הפשוטה.
כפעולות הבאות, מומלץ לפעול בסדר הזה:
- קודם כול לשכפל (clone) את מאגר הדוגמה מסעיף 5, לבנות ולרשום לפי ה-README, וליצור אצלכם מצב אחד שפועל בפועל.
- לבחור פונקציה אחת בלבד מה-DLL בן 64bit שלכם, ולהוסיף לה שיטה אחת בממשק המקביל ל-
ICalcServiceבדוגמה, ולוודא שהיא עוברת. - אחרי שזה עובד, למדוד את מספר הקריאות ואת כמות הנתונים בכל קריאה. אם מקבלים כאן מראש את ההחלטה התכנונית “לרכז לגודל גס” (איחוד כמה קריאות לאחת), חוזרים אחורה פחות בהמשך.
7. מקורות
- סקירה כללית של Component Object Model (COM) https://learn.microsoft.com/en-us/windows/win32/com/component-object-model–com–portal
- רישום COM LocalServer32 https://learn.microsoft.com/en-us/windows/win32/com/localserver32
- יסודות ממשקי COM https://learn.microsoft.com/en-us/windows/win32/com/the-component-object-model
- COM Interop (שימוש מ-.NET) https://learn.microsoft.com/en-us/dotnet/standard/native-interop/cominterop
- WOW64 Registry Redirector (
HKLM\SOFTWARE\Classesמשותף, תת-המפתחCLSIDנפרד בין 32/64) https://learn.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys - חשיפת רכיבי .NET (Core / 5 ומעלה) ל-COM https://learn.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מה זה Reg-Free COM - שימוש ב-COM בלי רישום
המאמר מסדר את היסודות של Reg-Free COM - תפקיד ה-activation context וה-manifest, היתרונות, המגבלות, וקריטריוני ההחלטה בפועל.
בניית פלט דוחות Excel - COM, Open XML, תבנית
פלט דוחות Excel משתנה מהותית לפי השאלה אם מפעילים את Excel אוטומטית, יוצרים xlsx ישירות, או משמרים VBA קיים. המאמר מסדר את קריטריוני הבחי...
מה זה COM / ActiveX / OCX — הסבר מרוכז על ההבדלים והקשרים
מהו COM, מהו ActiveX ומהו OCX — סקירה מעשית של ההבדלים והקשרים ביניהם, הזיקה ל-OLE, איפה זה נמצא בשימוש, ואיך כדאי להתייחס לזה היום.
איך מתייחסים היום ל-ActiveX / OCX — טבלת החלטה: לשמר, לעטוף או להחליף
כשמוצאים ActiveX / OCX, מסודר כאן איך לבחור בין לשמר, לעטוף או להחליף — כולל 32bit / 64bit, רישום, תלות בדפדפן ותחזוקת ספקים.
מבוא ל-Media Foundation — מבינים את ה-API מנקודת המבט של COM
המאמר מסביר מה זה Media Foundation, יחד עם המונחים הבסיסיים של ה-API למדיה ב-Windows כמו COM, HRESULT, IMFSourceReader ו-MFT, בסדר שכדא...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
העברת ActiveX
החלטות אם לשמר, לעטוף או להחליף רכיבי COM / ActiveX / OCX.
יכולת פעולה הדדית בין 32 ל-64 סיביות
תאימות 32/64 סיביות, גבולות native והחלטות תכנון ב-Windows.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
שימוש חוזר והעברה של נכסים קיימים
מדובר בגישור אל צד ה-64bit תוך שמירה על נכסי 32bit קיימים, נושא שקשור ישירות למינוף נכסים קיימים ולתמיכה במעבר.
ייעוץ טכני וסקירת תכנון
אם תרצו לגבש מראש את גשר ה-COM ואת אופן חלוקת גבולות התהליכים, אפשר לשקול זאת כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- האם אפשר לקרוא ישירות מאפליקציית 32bit ל-DLL של 64bit?
- לא אפשרי. תהליך 32bit אינו יכול לטעון DLL של 64bit — זו מגבלה ברמת מערכת ההפעלה, ולא משהו שאפשר לעקוף בתחכום. מכיוון שהדרך לקרוא בתוך אותו תהליך חסומה מלכתחילה, נדרש מבנה שמפריד את העיבוד בצד ה-64bit לתהליך נפרד.
- איך אפשר להשתמש בפונקציונליות של DLL בן 64bit מתוך אפליקציית 32bit?
- הדרך הסטנדרטית היא הפרדה באמצעות Out-of-proc COM (שרת EXE). קוראים ל-DLL של 64bit מתוך COM LocalServer (EXE) של 64bit, ואפליקציית ה-32bit משתמשת בו בצורה מוקלדת דרך ממשק COM. חושפים את הטיפוסים באמצעות שיתוף ממשק COM (IDL/TypeLib), וזמן הריצה של COM חוצה את הגבול באמצעות Proxy/Stub ותקשורת בין-תהליכית.
- מהן נקודות תשומת הלב במבנה של גשר COM?
- יש שלוש נקודות עיקריות: הרישום של 32bit ו-64bit נפרד (כולל WOW6432Node), מבנים מותאמים-אישית דורשים תכנון marshaling, ובגלל התקורה של תקשורת בין-תהליכית יש להיזהר מקריאות תכופות וקטנות. עדיף לרכז לעיבוד באצווה מאשר לשלוח כמות גדולה של קריאות קטנות.
- יש קוד לדוגמה שבאמת פועל?
- כן. במאגר Call64bitDLLFrom32bitProc ב-GitHub מפורסמת ערכת דוגמה מלאה, הכוללת COM LocalServer (EXE) של 64bit, DLL של 64bit, לקוח 32bit (WinForms) וסקריפטים לרישום/ביטול רישום של שרת COM. אם בונים ורושמים לפי ההוראות ב-README, אפשר לוודא שהקריאה מתהליך 32bit ל-DLL של 64bit אכן פועלת.