لإبقاء تبعية مضمّنة في إضافة Mac أصلية خاصة، اجمع تنفيذها مع إخفاء الرموز، ولا تصدّر عند الربط النهائي إلا نقاط الدخول العامة للإضافة. طبّق هذه الضوابط على الملف الثنائي الذي يحتوي فعليًا على التبعية.
هذا الدليل موجّه إلى مطوّر تطبيقات سطح المكتب لنظام Mac يبني إضافة تُحمّل أثناء التشغيل وتتضمن تبعية بلغة C أو C++. تتمثل المهمة المحددة في الاحتفاظ بتنفيذ الإضافة نفسها مع إتاحة واجهة صغيرة للمضيف. يوضّح مثال اصطناعي كيفية بناء تبعية بلغة C ضمن إضافة بلغة C++ والتحقق من نتيجتها والرموز التي تصدّرها.
أي ملف ثنائي يحتاج إلى تقييد رموزه المصدّرة؟
ابدأ بتحديد موضع الشيفرة الآلية للتبعية. يوفّر الأرشيف الثابت ملفات كائنات للملف الثنائي الجاري ربطه، بينما تظل المكتبة الديناميكية صورة مستقلة. لذلك، لا يمكن لإخفاء الرموز في الإضافة إعادة كتابة جدول التصدير الخاص بمكتبة تبعية dylib مستقلة. توضح Apple هذا الفرق في إرشادات استخدام المكتبات الديناميكية.
| ترتيب التبعية | موضع تطبيق الضبط | ما ينبغي التحقق منه |
|---|---|---|
| تجميع مصدر التبعية ضمن الإضافة | تجميع التبعية والربط النهائي للإضافة | وجود التعريفات الخاصة من دون تصديرها |
| ربط أرشيف ثابت بالإضافة | تجميع الأرشيف متى أمكن، إضافة إلى الربط النهائي للإضافة | تضمين أعضاء الأرشيف المطلوبة؛ وغياب صادرات التبعية |
| مكتبة تبعية dylib مستقلة | البناء الخاص بمكتبة التبعية dylib | واجهتها العامة والواردات المسجلة في الإضافة |
| إنشاء مثيلات من شيفرة التبعية عبر ترويسات C++ | كل هدف يجمّع هذه الترويسات | الدوال المضمنة والقوالب والبيانات المرتبطة التي أُنتجت |
هذا الجدول وسيلة لاتخاذ القرار مستخلصة من حدود التجميع والربط هذه. فعلى سبيل المثال، لا تؤدي قائمة تصدير مرفقة بهدف تغليف تلقائيًا إلى إخفاء الرموز التي ينتجها هدف آخر.
يستخدم macOS أيضًا مساحة أسماء ثنائية المستويين افتراضيًا: تُسجَّل المكتبة التي وفّرت المراجع التي جرى حلّها مقابل مكتبة dylib. أما مساحة الأسماء المسطحة فتغيّر هذا السلوك. وتُضيف التعريفات الخارجية الضعيفة اعتبارًا آخر، إذ يمكن للمحمّل دمجها. توثّق Apple الآليتين في دليل الرابط. لذلك، لا تكفي مطابقة الأسماء وحدها لتحديد سبب استدعاء الإضافة لتنفيذ غير متوقع.
اجمع بين التجميع مع إخفاء الرموز وقائمة تصدير صريحة
تعمل ضوابط المترجم والرابط في مرحلتين مختلفتين. يحدد -fvisibility=hidden من Clang مستوى الرؤية الافتراضي للتعريفات العامة في ملفات المصدر الجاري تجميعها. لكنه لا يعيد تجميع أرشيف موجود. وتوثّق Clang على نحو مستقل الخيار -fvisibility-inlines-hidden لدوال الأعضاء المضمنة في C++؛ تعامل معه بوصفه خيارًا مخصصًا يعتمد مدى ملاءمته على الواجهة. راجع مرجع سطر أوامر Clang.
ضع علامة __attribute__((visibility("default"))) على نقاط الدخول العامة المقصودة. يمكن لسمات مستوى الرؤية تجاوز القيمة الافتراضية للتجميع، لذا افحص ترويسات التبعية بحثًا عن تعليقات تصدير صريحة. تشرح Apple هذا السلوك في التحكم في مستوى رؤية الرموز.
في الربط النهائي، استخدم -exported_symbols_list لسرد الواجهة العامة. يستخدم تنسيق قائمة التصدير الموثّق لدى Apple أسماء رموز الرابط، بما في ذلك الشرطة السفلية الأولى لرموز C. اختر قائمة تصدير أو قائمة للرموز غير المصدّرة بدلًا من الجمع بين الاستراتيجيتين. راجع إرشادات تصميم المكتبات الديناميكية.
بالنسبة إلى واجهة إضافة صغيرة، تجعل قائمة التصدير الصريحة مراجعتها مباشرة: يجب أن يكون لكل اسم مُصدَّر مبرر لكونه عامًا. حافظ على اتساق التعليقات التوضيحية في المصدر مع القائمة؛ فالتعليق يحدد الواجهة البرمجية في الشيفرة، والقائمة تقيّد الملف الثنائي النهائي.
أنشئ مثالًا محليًا كاملًا
يستخدم المثال تبعية بلغة C عن قصد: فهو يوضح ضبط تصدير Mach-O من دون عرض تعريفات C++ غير المتوافقة على أنها أسلوب لغوي قابل للنقل. تُرجع تبعية المضيف 10؛ بينما يُرجع البناء الخاص للإضافة 20. هذه القيم توضيحية.
نفّذ الكتل التالية بالترتيب في نافذة Terminal نفسها على جهاز Mac تتوفر عليه أدوات سطر الأوامر في Xcode. تنشئ الكتل مجلدًا مؤقتًا جديدًا وتبني للمعمارية الافتراضية لسلسلة الأدوات النشطة. جرى فحص المثال، لكن لم يُجمّع أو يُشغّل أثناء هذا البحث لأن البيئة المتاحة كانت Linux من دون أدوات Apple.
أنشئ التبعية والإضافة
set -eu
example_dir=$(mktemp -d)
cd "$example_dir"
cat > dependency.h <<'EOF'
#ifndef DEMO_DEPENDENCY_H
#define DEMO_DEPENDENCY_H
#ifdef __cplusplus
extern "C" {
#endif
int demo_dependency_value(void);
#ifdef __cplusplus
}
#endif
#endif
EOF
cat > dependency.c <<'EOF'
#include "dependency.h"
#ifndef DEP_VALUE
#error DEP_VALUE must be supplied at compilation
#endif
int demo_dependency_value(void) {
return DEP_VALUE;
}
EOF
cat > plugin.cpp <<'EOF'
#include "dependency.h"
extern "C" __attribute__((visibility("default")))
int demo_plugin_value(void) {
return demo_dependency_value();
}
EOF
cat > exports.txt <<'EOF'
_demo_plugin_value
EOF
تزوّد ترويسة التبعية غلاف C++ بإعلان الربط الصحيح للغة C. ويستخدم الغلاف العام أيضًا ربطًا بلغة C، ما يبقي اسم الرابط لنقطة الدخول هذه بسيطًا. تتطلب أسماء C++ عادةً تشويهًا للأسماء؛ تناقش Apple هذا الفرق في التحكم في مستوى رؤية الرموز.
أنشئ المضيف
يحمّل المضيف الإضافة ويحصل على الدالة العامة عبر مقبض المكتبة المُعاد. توثّق Apple دعم dlopen لكل من المكتبات الديناميكية وحزم Mach-O. ويقيّد الخيار RTLD_LOCAL إتاحة الرموز المصدّرة للصورة التي جرى تحميلها؛ لكنه لا يحل محل ضوابط مستوى الرؤية وقت البناء. راجع دليل dlopen من Apple.
cat > host.cpp <<'EOF'
#include "dependency.h"
#include <cstdio>
#include <dlfcn.h>
int main() {
void* handle = dlopen("./DemoPlugin.dylib", RTLD_NOW | RTLD_LOCAL);
if (!handle) {
std::fprintf(stderr, "dlopen: %s\n", dlerror());
return 1;
}
dlerror();
void* address = dlsym(handle, "demo_plugin_value");
const char* error = dlerror();
if (error || !address) {
std::fprintf(stderr, "dlsym: %s\n",
error ? error : "null function address");
dlclose(handle);
return 1;
}
using PluginValue = int (*)(void);
PluginValue plugin_value = reinterpret_cast<PluginValue>(address);
const int host_result = demo_dependency_value();
const int plugin_result = plugin_value();
std::printf("host=%d plugin=%d\n", host_result, plugin_result);
const bool correct = host_result == 10 && plugin_result == 20;
if (dlclose(handle) != 0) {
std::fprintf(stderr, "dlclose: %s\n", dlerror());
return 1;
}
return correct ? 0 : 2;
}
EOF
التجميع والربط
# Build the plug-in's dependency with hidden definitions.
xcrun clang -fvisibility=hidden -DDEP_VALUE=20 \
-c dependency.c -o plugin_dependency.o
xcrun ar rcs libPluginDependency.a plugin_dependency.o
# Compile the public wrapper, then constrain the final exports.
xcrun clang++ -std=c++17 -fvisibility=hidden \
-c plugin.cpp -o plugin.o
xcrun clang++ -dynamiclib plugin.o libPluginDependency.a \
-Wl,-exported_symbols_list,exports.txt \
-o DemoPlugin.dylib
# Give the host a separately compiled implementation.
xcrun clang -DDEP_VALUE=10 -c dependency.c -o host_dependency.o
xcrun clang++ -std=c++17 host.cpp host_dependency.o \
-Wl,-exported_symbol,_demo_dependency_value -o host
./host
الناتج المتوقع هو host=10 plugin=20. يصدّر المضيف صراحةً رمز التبعية الخاص به كي يتضمن المثال تنفيذًا ظاهرًا خارجيًا بالاسم نفسه في C. ومع ذلك، يجب أن تحتوي الإضافة على تنفيذها الخاص.
هذا مثال للتحقق، وليس ادعاءً بأن إزالة الضوابط ستؤدي بالضرورة إلى فشل النتيجة. يمكن لآلية الربط الافتراضية في Mach-O أن تميّز التعريفات بالفعل في كثير من الترتيبات. ما يفيد التحقق منه هو التنفيذ الذي جرى اختياره ونطاق التصدير النهائي.
تحقّق من الصادرات والواردات كلٌّ على حدة
نفّذ هذه الأوامر في مجلد المثال:
xcrun nm -g -U -j DemoPlugin.dylib
xcrun dyld_info -exports DemoPlugin.dylib
xcrun dyld_info -imports DemoPlugin.dylib
xcrun otool -L DemoPlugin.dylib
في الأمر الأول، يحدد -g الرموز الخارجية، ويستبعد -U الرموز غير المعرّفة، ويطبع -j الأسماء. في هذه الإضافة البسيطة، الاسم الخارجي المعرّف المتوقع هو _demo_plugin_value؛ وينبغي ألا يظهر _demo_dependency_value. يوثّق دليل nm من Apple هذه الخيارات ويشرح أن معلومات جدول الرموز قد تختلف عن المعلومات التي يستخدمها المحمّل.
استخدم dyld_info -exports لفحص الصادرات التي يراها المحمّل، و-imports لفحص الرموز المستوردة. ينبغي ألا تظهر دالة التبعية في أي من القائمتين: إذ يُنفّذها هذا الملحق داخليًا على نحو خاص. توثّق دليل dyld_info من Apple خيارات الفحص هذه.
استخدم otool -L للتحقق من تبعيات dylib الفعلية. ينبغي ألا يظهر الأرشيف الثابت على أنه تبعية منفصلة وقت التشغيل. توثّق Apple فحص التبعيات هذا في إرشادات استخدام المكتبات الديناميكية.
في الإضافة الفعلية، أبقِ ثلاثة أسئلة منفصلة: هل أُدرج التنفيذ المطلوب؟ هل صُدّر؟ هل ما زال هناك استدعاء يستورد تنفيذًا منافسًا؟ لا يجيب نجاح البناء وحده عن أي من هذه الأسئلة.
طبّق الضوابط نفسها في Xcode
بالنسبة إلى الهدف الذي ينتج الملف الثنائي النهائي للإضافة، عيّن Exported Symbols File (EXPORTED_SYMBOLS_FILE) إلى المسار النسبي لقائمة التصدير داخل المشروع. فعّل Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN) في هدفي C وC++ اللذين يجمّعان التنفيذ الخاص. توثّق المرجعية الحالية لإعدادات البناء من Apple كلا الإعدادين.
إذا كانت التبعية هدف مكتبة ثابتة مستقلًا، فاضبط ذلك الهدف أيضًا. تغيير إعدادات المترجم للإضافة وحدها يترك كائنات التبعية التي جرى تجميعها مسبقًا كما هي. أعد بناء الأهداف المتأثرة وافحص المنتج الناتج لكل معمارية توزّعها.
يبقى لقائمة التصدير النهائية استخدام مع أرشيف مُسبق البناء. يوثّق دليل الرابط المنشور من Apple أيضًا -load_hidden لمعالجة الرموز العامة في الأرشيف باعتبارها مخفية مع استخدام استخراج الأرشيف المعتاد. هذا الدليل خاص بـ ld-classic؛ لذا تحقّق من وثائق الرابط المثبّتة قبل اختيار هذا الخيار الإضافي. يتحكم -force_load في تضمين أعضاء الأرشيف، وهي مسألة منفصلة. راجع دليل الرابط من Apple.
حافظ على متطلبات واجهات C++ وSwift
لا يصلح مستوى رؤية C++ التعريفات غير المتوافقة
تفرض قاعدة التعريف الواحد في C++ متطلبات على التعريفات بين وحدات الترجمة، بما في ذلك تطابق الرموز ومتطلبات البحث عن الأسماء عندما يُسمح بتعريفات متعددة. لا يعني إخفاء رمز أنه يجوز الجمع بين تعريفات C++ متعارضة. بالنسبة إلى عمليات التنفيذ ذات الإصدارات المستقلة، استخدم مساحات أسماء أو أسماء رموز مختلفة متى أمكن. راجع مسودة C++ لقاعدة التعريف الواحد.
حافظ أيضًا على مستوى الرؤية اللازم لواجهة C++ مقصودة. تشير Apple إلى أن RTTI بين المكتبات، والاستثناءات، والتحويلات الديناميكية، وهوية عناوين الدوال المضمنة، والكائنات الثابتة المشتركة حالات تتطلب عناية. ومن التصاميم العملية إبقاء أنواع التبعية والملكية داخل الإضافة وإتاحة واجهة صغيرة تستخدم قيمًا عددية متفقًا عليها أو مقابض معتمة. راجع التحكم في مستوى رؤية الرموز.
يتحكم استيراد Swift بمستوى وصول مختلف
يقيّد internal import في Swift المواضع التي يمكن فيها الإشارة إلى التصريحات المستوردة. توصي SE-0409، المنفّذة في Swift 6.0، باستبدال استخدامات @_implementationOnly import باستيراد داخلي أو بمستوى أدنى. لكنها لا تحدد آلية لإعادة تسمية تبعيات Mach-O أو تصفية الصادرات. واصل ضبط الملفات الثنائية الأصلية وفحصها على نحو مستقل. راجع معدّلات مستوى الوصول في تصريحات الاستيراد.
إذا كنت تستخدم التوافقية المباشرة بين Swift وC++، فاحتفظ بقيود سلسلة الأدوات: يبدأ الدعم مع Swift 5.9، ويجب أن تستخدم الشيفرة المختلطة مكتبة C++ القياسية نفسها، وتستخدم منصات Apple libc++. كما تفرض أنواع مراجع C++ المستوردة حدًا أدنى لإصدار النشر هو macOS 13.3. لا يغيّر تصفية الصادرات أيًا من هذه المتطلبات. راجع الميزات المدعومة وقيود التوافقية مع C++.
تكون الإضافة جاهزة لاختبار التكامل عندما تظل نقاط الدخول المقصودة متاحة، وتغيب تعريفات التبعية الخاصة عن صادرات المحمّل، وتتوافق الواردات مع المكتبات المفترض استخدامها. اجمع هذه الفحوص مع اختبار سلوكي مثل المثال الاصطناعي، كي تكشف تحديثات التبعيات اللاحقة عن تغييرات الواجهة وتغييرات اختيار التنفيذ معًا.
المصادر
- المرجعية لإعدادات البناء — Apple.
- إرشادات تصميم المكتبات الديناميكية — Apple.
- إرشادات استخدام المكتبات الديناميكية — Apple.
- التحكم في مستوى رؤية الرموز — Apple.
- مرجع وسائط سطر أوامر Clang — مشروع LLVM.
- دليل ld-classic(1) — Apple.
- دليل dlopen(3) — Apple.
- دليل nm(1) — Apple.
- دليل dyld_info(1) — Apple.
- قاعدة التعريف الواحد — مسودة C++، صاغها Tim Song.
- معدّلات مستوى الوصول في تصريحات الاستيراد — مشروع Swift.
- الميزات المدعومة وقيود التوافقية مع C++ — Swift.org.