الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

[ تمت الإجابة ]ما هو استخدام extern C

بدأه مصطفى 36a2 في 18 ديسمبر 2012 · 8 رد · 1,906 مشاهدة · في الأسئلة المجابة
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

السلام عليكم ...

مررت عدة مرات في اليومين الماضيين على عبارة

extern "C"
{

ثم يبدأ بعدها تعريف المكتبة أو عمليات معينة ...

سؤالي قسمان :

القسم الأول : كيف نستخدم التعليمة extern ?

القسم الثاني : ما معنى العبارة في الكود السابق ؟

وشكرا ..

#3
اقتباس
القسم الأول : كيف نستخدم التعليمة extern ?

داخل لغة ++C يوجد نوعين من linkage هما internal linkage و external linkage.

المقصود بعملية الربط للمتغيرات هو طبيعة المساحة التخزينية التى سيتم التعامل معها فهل هي مختلفة من ملف لأخر ام انها نفس المساحة التخزينية لكل الملفات؟

عندما تقوم بتعريف متغيرات عامه داخل translation unit بإستخدام الكلمة static فأنت تعلم المترجم ان المساحة التخزينية التى سيتم إنشائها لهم تمثل هذه المتغيرات فقط و أن أى translation unit أخرى يمكنها إستخدام نفس إسم المتغير للإعلان عن مساحات تخزينية خاصة بهم. إستخدام الكلمة static أثناء تعريف او التصريح عن المتغير العام يعني أنه سيتم إستخدام الـ internal linkage له.

الوضع الإفتراضي للمتغيرات العامه داخل الـ translation unit يكون external linkage و هذا يعنى أن المتغيرات العامه كلها تكون بـ extern سواء كتبت او لا.

جميع المتغيرات العامه يتم حجز لها مساحة سواء قمت بوضع قيمة مبدئية لها او لم تضعهم و بالتالي إذا كان لديك ملفان translation units و بهم متغيران عامان بنفس الإسم ستنجح عملية الترجمه و لكن الـ linker سيفشل لوجود متغيرين بنفس الإسم و بالتالي إحداهما لابد من ان يتم الإعلان عنه بـ extern و ان يكون تصريح فقط أى لا تضع به قيمة لكي تنجح العملية.

مثال 1:

// file a.cc
int a = 10;

// file b.cc
int a;

هذا المثال خاطئ لأن كلا المتغيرين سيتم تعريف مساحة تخزينية لهم و لكلاهما له external linkage و بالتالي الـ linker سيعترض على وجود مساحتين تخزينيتين لنفس الإسم.

مثال 2:

// file a.cc
extern int a = 10;

// file b.cc
int a;

هذا المثال مثل السابق و الفرق انك وضعت extern بشكل صريح.

مثال 3:

// file a.cc
int a = 10;

// file b.cc
extern int a;

هذا المثال صحيح حيث سيتم إنشاء المساحة التخزينية بالملف a و سيتم الربط على هذه المساحة من الملف b.

مثال 4:

// file a.cc
extern int a = 10;

// file b.cc
extern int a;

نفس المثال السابق و الفرق انك وضعت extern بشكل صريح.

مثال 5:

// file a.cc
extern int a = 10;

// file b.cc
extern int a = 5;

نفس المثال الأول لأنه المترجم يقوم بوضع قيمة افتراضية للمتغيرات و الفرق انك بدلا من ان تتركه يضع صفر للمتغير المعرف بالملف b قمت انت بوضع قيمة خاصة بك و ايضا قمت بإستخدام extern بشكل صريح و حيث ان كلاهما متغير عام فلا تأثير لها لأنها الإفتراضية.

اقتباس
القسم الثاني : ما معنى العبارة في الكود السابق ؟

تعني ببساطة ان المترجم سيتخدم الـ name decoration الخاص بلغة السي مع ما يلي extern C و السبب ان لديك مكتبة قديمه تريد التعامل معها او انك تكتب دوال ستستخدمها فيما بعد مع مترجم لغة السي.

و الله ولي التوفيق

2

مدونتي: C++ Tips and Tricks

#4

اذا كنت تجيد الانجليزية

extern C

extern C

تم تعديل هذه المشاركة بواسطة fouad2008 في 19 ديسمبر 2012 في 13:10

#5

شكرا أخي Sn@cker

رابط مفيد بالفعل وإجابات ممتازة ... ولكن ألا ينقص الأمثلة في الكود عبارة include

في بداية الملف الثاني دوماً ...أو أي تنبيه لعملية الربط بين الملفين ؟

______________________________________

شكرا أستاذ @C++er ...

إذا يمكننا تلخيص extern بأنها تصريح عن وجود تعريف واحد للمتحول بمكان ما والمسؤول عن التأكد من وجودها

هو عملية الربط LINK وليست عملية الترجمة Compile

وفقا لما ذكرته : عندي سؤال ... إذا كانت الحالة الافتارضية للمتحولات هي extern

اقتباس

الوضع الإفتراضي للمتغيرات العامه داخل الـ translation unit يكون external linkage و هذا يعنى أن المتغيرات العامه كلها تكون بـ extern سواء كتبت او لا.

لم أقتنع بهذه العبارة ..فهذا يعني أن وجودها مثل عدمها ..

أي أن المثالين الخاطئ والصحيح ...متطابقان؟!!

 //المثال الخاطئ
 // file a.cc
int a = 10;

// file b.cc
int a;

إذا كانت الحالة الافتراضية لهما extern فهل المثال التالي يطابق الأول؟

 //المثال الصحيح
 // file a.cc
extern int a = 10;

// file b.cc
extern int a;

وبالتالي أرى أن الحالة الافتراضية ليست extern فما ردك ؟

المثال التي من أفضل الأمثلة التي رأيتها

حيث يتم استخدام المتحولات قبل تعريفها

 #include <stdio.h>
int main(void)
{
extern int first, last; /* use global vars */
printf("%d %d", first, last);
return 0;
}
/* global definition of first and last */
int first = 10, last = 20;

من الامور الهامة الواجب ذكرها في موضوع الextern ... مال الفرق بينها وبين أي متحول آخر في ملف آخر

ونقوم بعمل include بين الملف الأول والملف الثاني !!

الفرق هو أنه لدينا خياران في حالة extern فإما أن نقوم بعمل include للملف الذي يحوي تعاريف المتحولات ...

أو أن نقوم بعمل LINK لهذا الملف بحيث يكون مرتبط (روحياً) بالملف الذي استدعاه

أرجو التصحيح إن كنت أخطأت فيما قلت ...

والسلام عليكم ورحمة الله

#6

هناك فرق بين extern و extern "C" الأولى عامة وموجودة في C و C++ والأخرى خاصة بـC++ ولاتوفرها C.

تدعم C++ خاصية زيادة تحميل الدوال function overloading بمعنى أنّه يمكن لعدة دوال أن تتشارك في نفس الإسم، كي تتمكن مصرفات الـC++ من عمل هذا فإنها تعدّل في أسماء الدوال لتنتج دوال مختلفة. مثلاً لديك هذا البرنامج:

int my_function(int p);
char my_function(char p);
long my_function(long p);

int main(int argc, char **argv)
{
	my_function(1); 	/* 'int my_function(int p)' ستستدعي */
	my_function('a');   /* 'char my_function(char p)' ستستدعي */
	my_function(1L);	/* 'long my_function(long p)' ستستدعي */

	return 0;
}

int my_function(int p)
{
	return p;
}

char my_function(char p)
{
	return p;
}

long my_function(long p)
{
	return p;
}

عند تصريف البرنامج تحت gcc سيولد:

(gdb) disassemble main
Dump of assembler code for function main:
0x08048474 <main+0>:	push   %ebp
0x08048475 <main+1>:	mov	%esp,%ebp
0x08048477 <main+3>:	and	$0xfffffff0,%esp
0x0804847a <main+6>:	sub	$0x10,%esp
0x0804847d <main+9>:	movl   $0x1,(%esp)
0x08048484 <main+16>:	call   0x80484a8 <_Z11my_functioni> ; لاحظ أنّ أسماء الدوال إختلفت
0x08048489 <main+21>:	movl   $0x61,(%esp)
0x08048490 <main+28>:	call   0x80484b0 <_Z11my_functionc>
0x08048495 <main+33>:	movl   $0x1,(%esp)
0x0804849c <main+40>:	call   0x80484c2 <_Z11my_functionl>
0x080484a1 <main+45>:	mov	$0x0,%eax
0x080484a6 <main+50>:	leave  
0x080484a7 <main+51>:	ret  
End of assembler dump.
(gdb)

كما ترى فمصرف الـC++ يقوم بتعديل أسماء تلك الدوال في كامل البرنامج. الكلمة extern "C" تعطل هذا التصرف حيث تحافظ الدالة على إسمها:

extern "C" int my_function(int p); /* ألغينا التلاعب بإسم الدالة عنها */
char my_function(char p);
long my_function(long p);

int main(int argc, char **argv)
{
	my_function(1); 	/* 'int my_function(int p)' ستستدعي */
	my_function('a');   /* 'char my_function(char p)' ستستدعي */
	my_function(1L);	/* 'long my_function(long p)' ستستدعي */

	return 0;
}

int my_function(int p)
{
	return p;
}

char my_function(char p)
{
	return p;
}

long my_function(long p)
{
	return p;
}

البرنامج الناتج:

(gdb) disassemble main
Dump of assembler code for function main:
0x08048474 <main+0>:	push   %ebp
0x08048475 <main+1>:	mov	%esp,%ebp
0x08048477 <main+3>:	and	$0xfffffff0,%esp
0x0804847a <main+6>:	sub	$0x10,%esp
0x0804847d <main+9>:	movl   $0x1,(%esp)
0x08048484 <main+16>:	call   0x80484a8 <my_function>  ; ستحافظ الدالة على إسمها
0x08048489 <main+21>:	movl   $0x61,(%esp)
0x08048490 <main+28>:	call   0x80484b0 <_Z11my_functionc>
0x08048495 <main+33>:	movl   $0x1,(%esp)
0x0804849c <main+40>:	call   0x80484c2 <_Z11my_functionl>
0x080484a1 <main+45>:	mov	$0x0,%eax
0x080484a6 <main+50>:	leave  
0x080484a7 <main+51>:	ret   
End of assembler dump.
(gdb) q

لاحظ أنه لن يُمكنك إستخدام extern "C" مع أكثر من دالة وسعطيك المصرف خطأ بأنه لايمكن لدالتين مخلفتين أن تتشاركان نفس الإسم (كما قلت أن المصرف يضطر أن يتلاعب بالإسم كي يتمكن من زيادة تحميل الدالة).

يمكن إستخدام extern "C" بطريقتين:

extern "C" void function1(void);

// أو

extern "C" {
void function2(void);
};

لكن الطريقة الأفضل لإستخدامها:

#ifdef __cplusplus /* أولاً تحقق من أن هذا المصرف مصرف C++ */
extern "C" {   	/* إذا كان كذلك فستفعل extern "C" { */
#endif

void function(void); /* ضع الدوال التي تريدها */

#ifdef __cplusplus /* إختم جملة extern "C" { */
};
#endif

غالباً لن تحتاج لإستخدام extern "C" في البرامج العادية. لكن كثيراً ماتستخدم عند بناء المكتبات.

في ويندوز يمكن وضع مجموعة دوال في مكتبة تتشارك دوالها عدة عمليات وذلك للحفاظ على الذاكرة. كي تعمل هذا يُمكنك إستخدام التوجيه __declspec(dllexport) مع الدوال التي تريد تصديرها وحينها يُمكنك مثلاً تحميل مكتبة الـdll هذه بإستخدام LoadLibrary والحصول على عنوان الدالة بإستخدام GetProccAddress أو الربط الدينامكي معها.

لكن هناك مشكلة تسببها مصرفات الـC++، لو صرفت هذا البرنامج كبرنامج C++ مع gcc :

__declspec(dllexport) int my_function(int p);

__declspec(dllexport) int my_function(int p)
{
	return p;
}

وبنيته كـdll:

>g++ mylib.cpp -shared -o mylib.dll

وحاولت تحميل الدالة my_function منها فلن تستطيع:

#include <windows.h>
#include <stdio.h>

int main(int argc, char **argv)
{
	HMODULE hMyLib;
	int (*my_function)(int);

	hMyLib = LoadLibrary(TEXT("mylib.dll"));
	if( hMyLib == NULL ) {
    	printf("Error: Cannot load mylib.dll\n");
    	return -1;
	}

	my_function = (int (*)(int)) GetProcAddress(hMyLib, "my_function");
	if( my_function == NULL ) {
    	printf("Error: Cannot find the address of 'my_function'\n");
    	FreeLibrary(hMyLib);
    	return -1;
	}

	my_function(1);

	FreeLibrary(hMyLib);
	return 0;
}

>gcc loader.c -o loader
>loader.exe
Error: Cannot find the address of 'my_function'
>

سبب ذلك أنّ إسم الدالة my_functuon سيصدر كـZ11my_functioni:

post-231926-054781000 1355918764_thumb.p

وهذا ليس مانريده. حل هذه المشكلة يكون بإستخدام extern "C":

#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) int my_function(int p);
#ifdef __cplusplus
};
#endif

__declspec(dllexport) int my_function(int p)
{
	return p;
}

>g++ mylib.cpp -shared -o mylib.dll
>loader.exe
> /* لايوجد خطأ */

على لينكس وأنظمة يونكس عموماً إذا أردت بناء shared object ، يشبه الـdll في ويندوز، فيلزمك أن تستخدم extern "C" كي تستطيع تحميل تلك المكتبة لبرنامجك. مثلاً هذا البرنامج :

#include <stdio.h>

int my_function(int p);

int my_function(int p)
{
	return p * 2;
}

لو بنيته كـshared object بمصرف C++ وحاولت تحميله بإستخدام dlsym:

#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char **argv)
{
	void *objh;
	int (*my_function)(int);

	objh = dlopen("./libfoo.so", RTLD_LAZY);
	if(! objh ) {
    	fprintf(stderr, "Error: %s\n", dlerror());
    	return -1;
	}

	if(! (my_function = dlsym(objh, "my_function")) ) {
    	fprintf(stderr, "Error: %s\n", dlerror());
    	dlclose(objh);
    	return -1;
	}

	printf("%d\n", my_function(2));

	dlclose(objh);
	return 0;
}

فسيعطيني خطأ أنه لايمكنه إيجاد الدالة my_function:

$ g++ -shared example.cpp -o libfoo.so 
$ gcc loader.c -o loader -ldl
$ ./loader 
Error: ./libfoo.so: undefined symbol: my_function
$

لحل هذه المشكلة يلزم تصديره مع extern "C" أولاً:

#include <stdio.h>

#ifdef __cplusplus
extern "C" {
#endif

int my_function(int p);

#ifdef __cplusplus
};
#endif


int my_function(int p)
{
	return p * 2;
}

$ g++ -shared example.cpp -o libfoo.so 
$ ./loader 
4
$
المرفقات
Untitled.png

تم تعديل هذه المشاركة بواسطة Mr.B في 19 ديسمبر 2012 في 15:16

3
#7

شكرا لك أخي MR.B أجبت عن سؤالي الثاني بشكل كامل ...

اقتباس
#ifdef __cplusplus /* أولاً تحقق من أن هذا المصرف مصرف C++ */
extern "C" {    /* إذا كان كذلك فستفعل extern "C" { */
#endif

void function(void); /* ضع الدوال التي تريدها */

#ifdef __cplusplus /* إختم جملة extern "C" { */
};
#endif

فهمتها تماماً ...أي أنك تستدعي طريقة التصريف الخاصة بلغة C للأشياء المعرفة ضمن القوسين ...

وهناك حسب ما قرأت يمكن أن تكتب أي لغة لو كان المترجم يدعمها ...

مثلاً "extern "Fortran أو ما شابه ذلك ... (لو افترضنا ان المترجم يعرف آلية تصريفها) ...

إذا فهي أمر للمصرف باستخدام إعدادات خاصة لا أكثر ...وبالتالي لا معنى لدراستها دون معرفة بالمصرفات والفروق بينها ...

جزاك الله خيرا

#8
اقتباس
إذا كانت الحالة الافتراضية لهما extern فهل المثال التالي يطابق الأول؟

لا و السبب ذكرته فى المشاركه:

اقتباس
جميع المتغيرات العامه يتم حجز لها مساحة سواء قمت بوضع قيمة مبدئية لها او لم تضعهم و بالتالي إذا كان لديك ملفان translation units و بهم متغيران عامان بنفس الإسم ستنجح عملية الترجمه و لكن الـ linker سيفشل لوجود متغيرين بنفس الإسم و بالتالي إحداهما لابد من ان يتم الإعلان عنه بـ extern و ان يكون تصريح فقط أى لا تضع به قيمة لكي تنجح العملية.

عندما تقوم بالتصريح عن متغير مع وجود extern حينها يعلم المترجم ان هذا المتغير تم تعريفه بملف أخر و يتم إستخدامه هنا اما لو وضعت له قيمه فإن المترجم يعرف ان هذا المتغير تم تعريفه هنا و يمكن الربط عليه من ملف اخر.

و الله ولي التوفيق

2

مدونتي: C++ Tips and Tricks

#9

أسجل إعجابي لبراعة Mr.B ... يشي فهمك وجهدك أنك ستتفوق وستظهر بقوة. أشد على يدك +1 ... على فكرة تذكرني بالعضو JAAS. smile.gif

+1 أيضا لرد C++er الجميل.

1

" إن الله كتب الإحسان على كل شيء"

::

الإرادة ... تحقق السيادة.

مواضيع مشابهة