ทำไมเราต้องใช้:
extern "C" {
#include <foo.h>
}
โดยเฉพาะ:
เมื่อใดที่เราควรใช้
เกิดอะไรขึ้นในคอมไพเลอร์ / ลิงเกอร์ระดับที่ต้องการให้เราใช้มัน?
วิธีการรวบรวม / เชื่อมโยงนี้จะแก้ปัญหาที่เราต้องใช้มันได้อย่างไร?
ทำไมเราต้องใช้:
extern "C" {
#include <foo.h>
}
โดยเฉพาะ:
เมื่อใดที่เราควรใช้
เกิดอะไรขึ้นในคอมไพเลอร์ / ลิงเกอร์ระดับที่ต้องการให้เราใช้มัน?
วิธีการรวบรวม / เชื่อมโยงนี้จะแก้ปัญหาที่เราต้องใช้มันได้อย่างไร?
คำตอบ:
C และ C ++ คล้ายกันมาก แต่แต่ละคอมไพล์เป็นชุดโค้ดที่แตกต่างกันมาก เมื่อคุณรวมไฟล์ส่วนหัวที่มีคอมไพเลอร์ C ++ คอมไพเลอร์จะคาดหวังรหัส C ++ อย่างไรก็ตามหากเป็นส่วนหัว C ดังนั้นคอมไพเลอร์คาดหวังว่าข้อมูลที่อยู่ในไฟล์ส่วนหัวจะถูกรวบรวมในรูปแบบที่แน่นอน - C ++ 'ABI' หรือ 'Application Binary Interface' ดังนั้นตัวเชื่อมโยงจึงเกิดขึ้น สิ่งนี้ดีกว่าในการส่งข้อมูล C ++ ไปยังฟังก์ชันที่คาดว่าข้อมูล C
(ในการเข้าสู่ความเป็นจริงอย่างแท้จริง ABI ของ C ++ โดยทั่วไปจะเรียกชื่อฟังก์ชั่น / วิธีการของพวกเขาดังนั้นการโทรprintf()
โดยไม่ตั้งค่าสถานะต้นแบบเป็นฟังก์ชัน C นั้น C ++ จะสร้างรหัสการโทรจริง_Zprintf
รวมถึงอึพิเศษในตอนท้าย )
ดังนั้น: ใช้extern "C" {...}
เมื่อรวมส่วนหัว ac - มันง่ายมาก มิฉะนั้นคุณจะมีรหัสไม่ตรงกันในการรวบรวมและ linker จะทำให้หายใจไม่ออก สำหรับส่วนหัวอย่างไรก็ตามคุณไม่จำเป็นต้องมีextern
เพราะส่วนหัวของระบบ C ส่วนใหญ่จะอธิบายถึงความจริงที่ว่าพวกเขาอาจรวมอยู่ในรหัส C ++ และextern
รหัสของพวกเขาอยู่แล้ว
#ifdef __cplusplus extern "C" { #endif
ดังนั้นเมื่อรวมจากไฟล์ C ++ พวกเขาจะยังคงเป็นส่วนหัว C
extern "C" กำหนดว่าสัญลักษณ์ในไฟล์วัตถุที่สร้างควรมีชื่ออย่างไร หากมีการประกาศฟังก์ชันโดยไม่มี "C" ภายนอกชื่อสัญลักษณ์ในไฟล์วัตถุจะใช้ชื่อ C ++ นี่คือตัวอย่าง
รับทดสอบ C ชอบ:
void foo() { }
การรวบรวมและการแสดงรายการสัญลักษณ์ในไฟล์อ็อบเจ็กต์ให้:
$ g++ -c test.C
$ nm test.o
0000000000000000 T _Z3foov
U __gxx_personality_v0
ฟังก์ชั่น foo นั้นเรียกว่า "_Z3foov" สตริงนี้มีข้อมูลประเภทสำหรับประเภทผลตอบแทนและพารามิเตอร์เหนือสิ่งอื่นใด ถ้าคุณเขียน test.C แบบนี้แทน:
extern "C" {
void foo() { }
}
จากนั้นรวบรวมและดูสัญลักษณ์:
$ g++ -c test.C
$ nm test.o
U __gxx_personality_v0
0000000000000000 T foo
คุณจะได้รับการเชื่อมโยง C ชื่อของฟังก์ชั่น "foo" ในไฟล์วัตถุเป็นเพียง "foo" และมันไม่มีข้อมูลประเภทแฟนซีทั้งหมดที่มาจากชื่อ mangling
โดยทั่วไปคุณจะมีส่วนหัวภายใน extern "C" {} หากรหัสที่ไปด้วยถูกคอมไพล์ด้วยคอมไพเลอร์ C แต่คุณพยายามเรียกมันจาก C ++ เมื่อคุณทำเช่นนี้คุณกำลังบอกคอมไพเลอร์ว่าการประกาศทั้งหมดในส่วนหัวจะใช้ C linkage เมื่อคุณเชื่อมโยงรหัสของคุณไฟล์. o ของคุณจะมีการอ้างอิงถึง "foo" ไม่ใช่ "_Z3fooblah" ซึ่งหวังว่าจะตรงกับสิ่งที่อยู่ในไลบรารีที่คุณเชื่อมโยงอยู่
ห้องสมุดที่ทันสมัยส่วนใหญ่จะวางยามรอบส่วนหัวดังกล่าวเพื่อให้มีการประกาศสัญลักษณ์ด้วยการเชื่อมโยงที่ถูกต้อง เช่นในหัวกระดาษมาตรฐานคุณจะพบ:
#ifdef __cplusplus
extern "C" {
#endif
... declarations ...
#ifdef __cplusplus
}
#endif
สิ่งนี้ทำให้แน่ใจว่าเมื่อรหัส C ++ มีส่วนหัวสัญลักษณ์ในไฟล์วัตถุของคุณตรงกับสิ่งที่อยู่ในไลบรารี C คุณควรใส่ "C" {} ภายนอกไว้รอบ ๆ ส่วนหัว C ของคุณหากเก่าแล้วและไม่มีการ์ดเหล่านี้อยู่แล้ว
ใน C ++ คุณสามารถมีเอนทิตีต่าง ๆ ที่ใช้ชื่อร่วมกันได้ ตัวอย่างเช่นนี่คือรายการของฟังก์ชั่นทั้งหมดที่ชื่อfoo :
A::foo()
B::foo()
C::foo(int)
C::foo(std::string)
เพื่อที่จะแยกความแตกต่างระหว่างพวกเขาทั้งหมดคอมไพเลอร์ C ++ จะสร้างชื่อที่ไม่ซ้ำสำหรับแต่ละชื่อในกระบวนการที่เรียกว่าชื่อ -mangling หรือการตกแต่ง คอมไพเลอร์ C ไม่ได้ทำเช่นนี้ นอกจากนี้คอมไพเลอร์ C ++ แต่ละตัวอาจทำสิ่งนี้แตกต่างกัน
extern "C" บอกคอมไพเลอร์ C ++ ว่าจะไม่ดำเนินการใด ๆ กับชื่อ mangling ในรหัสภายในวงเล็บปีกกา สิ่งนี้อนุญาตให้คุณเรียกใช้ฟังก์ชัน C จากภายใน C ++
มันเกี่ยวข้องกับวิธีที่คอมไพเลอร์ต่างกันทำชื่อ - mangling คอมไพเลอร์ C ++ จะยั่วชื่อของสัญลักษณ์ที่ส่งออกจากไฟล์ส่วนหัวในวิธีที่แตกต่างอย่างสิ้นเชิงกว่า C คอมไพเลอร์จะดังนั้นเมื่อคุณพยายามที่จะเชื่อมโยงคุณจะได้รับข้อผิดพลาด linker บอกว่ามีสัญลักษณ์ที่ขาดหายไป
ในการแก้ไขปัญหานี้เราแจ้งให้คอมไพเลอร์ C ++ ทำงานในโหมด "C" ดังนั้นจึงทำการเรนเดอร์ชื่อในลักษณะเดียวกับที่คอมไพเลอร์ C ต้องการ หลังจากทำเช่นนั้นข้อผิดพลาด linker ได้รับการแก้ไข
C และ C ++ มีกฎที่แตกต่างกันเกี่ยวกับชื่อของสัญลักษณ์ สัญลักษณ์เป็นวิธีที่ตัวเชื่อมโยงรู้ว่าการเรียกใช้ฟังก์ชัน "openBankAccount" ในไฟล์วัตถุหนึ่งที่สร้างโดยคอมไพเลอร์เป็นการอ้างอิงถึงฟังก์ชันที่คุณเรียกว่า "openBankAccount" ในไฟล์วัตถุอื่นที่ผลิตจากไฟล์ต้นฉบับอื่น (หรือเทียบเท่า) ผู้รวบรวม สิ่งนี้ช่วยให้คุณสามารถสร้างโปรแกรมจากไฟล์ต้นฉบับมากกว่าหนึ่งไฟล์ซึ่งช่วยลดความยุ่งยากเมื่อทำงานกับโครงการขนาดใหญ่
ใน C กฎนั้นง่ายมากสัญลักษณ์ทั้งหมดอยู่ในช่องว่างของชื่อเดียวอย่างไรก็ตาม ดังนั้นจำนวนเต็ม "ถุงเท้า" จึงถูกจัดเก็บเป็น "ถุงเท้า" และฟังก์ชัน count_socks จะถูกเก็บไว้เป็น "count_socks"
ตัวเชื่อมโยงถูกสร้างขึ้นสำหรับ C และภาษาอื่น ๆ เช่น C ด้วยกฎการตั้งชื่อสัญลักษณ์อย่างง่ายนี้ ดังนั้นสัญลักษณ์ในตัวเชื่อมโยงจึงเป็นเพียงสตริงธรรมดา
แต่ใน C ++ ภาษาให้คุณมีเนมสเปซและโพลีมอร์ฟิซึมและสิ่งต่าง ๆ ที่ขัดแย้งกับกฎง่ายๆ ฟังก์ชัน polymorphic ทั้งหกของคุณที่เรียกว่า "เพิ่ม" จำเป็นต้องมีสัญลักษณ์ที่แตกต่างกันมิฉะนั้นไฟล์วัตถุอื่นจะถูกใช้อย่างผิดปกติ สิ่งนี้ทำได้โดย "mangling" (นั่นคือศัพท์เทคนิค) ชื่อของสัญลักษณ์
เมื่อเชื่อมโยงรหัส C ++ กับไลบรารี C หรือรหัสคุณต้องมี "C" ภายนอกที่เขียนใน C เช่นไฟล์ส่วนหัวสำหรับไลบรารี C เพื่อบอกคอมไพเลอร์ C ++ ของคุณว่าชื่อสัญลักษณ์เหล่านี้ไม่ได้ถูกจัดเรียงไว้ในขณะที่ส่วนที่เหลือของ รหัสหลักสูตร C ++ ของคุณจะต้องมีการจัดการมิฉะนั้นมันจะไม่ทำงาน
เมื่อใดที่เราควรใช้
เมื่อคุณเชื่อมโยง C libaries เข้ากับอ็อบเจ็กต์ไฟล์ C ++
เกิดอะไรขึ้นในคอมไพเลอร์ / ลิงเกอร์ระดับที่ต้องการให้เราใช้มัน?
C และ C ++ ใช้รูปแบบที่แตกต่างกันสำหรับการตั้งชื่อสัญลักษณ์ สิ่งนี้บอกให้ linker ใช้รูปแบบของ C เมื่อทำการลิงก์ในไลบรารีที่กำหนด
วิธีการรวบรวม / เชื่อมโยงนี้จะแก้ปัญหาที่เราต้องใช้มันได้อย่างไร?
การใช้รูปแบบการตั้งชื่อ C ช่วยให้คุณอ้างอิงสัญลักษณ์รูปแบบ C มิฉะนั้นตัวเชื่อมโยงจะลองใช้ C ++ - สัญลักษณ์รูปแบบที่ใช้ไม่ได้
คุณควรใช้ extern "C" ทุกครั้งที่คุณรวมฟังก์ชั่นการกำหนดส่วนหัวที่อยู่ในไฟล์ที่คอมไพล์โดยคอมไพเลอร์ C ที่ใช้ในไฟล์ C ++ (ไลบรารี C มาตรฐานจำนวนมากอาจรวมการตรวจสอบนี้ไว้ในส่วนหัวเพื่อให้ง่ายสำหรับนักพัฒนา)
ตัวอย่างเช่นถ้าคุณมีโครงการที่มี 3 ไฟล์, util.c, util.h และ main.cpp และทั้งไฟล์. c และ. cpp จะถูกคอมไพล์ด้วยคอมไพเลอร์ C ++ (g ++, cc, ฯลฯ ) ดังนั้นมันจึงไม่ใช่ ' ไม่จำเป็นจริงๆและอาจทำให้เกิดข้อผิดพลาด linker หากกระบวนการสร้างของคุณใช้คอมไพเลอร์ C ปกติสำหรับ util.c จากนั้นคุณจะต้องใช้ "C" ภายนอกเมื่อรวมถึง util.h
สิ่งที่เกิดขึ้นคือ C ++ เข้ารหัสพารามิเตอร์ของฟังก์ชันในชื่อ นี่คือการทำงานของฟังก์ชันโอเวอร์โหลด สิ่งที่มีแนวโน้มที่จะเกิดขึ้นกับฟังก์ชั่น C คือการเพิ่มขีดล่าง ("_") ไปยังจุดเริ่มต้นของชื่อ หากไม่มีการใช้ภายนอก "C" ตัวเชื่อมโยงจะค้นหาฟังก์ชันชื่อ DoSomething @@ int @ float () เมื่อชื่อจริงของฟังก์ชันคือ _DoSomething () หรือเพียง DoSomething ()
การใช้ extern "C" แก้ปัญหาดังกล่าวโดยบอกคอมไพเลอร์ C ++ ว่าควรมองหาฟังก์ชั่นที่เป็นไปตามหลักการตั้งชื่อ C แทนที่จะเป็น C ++
คอมไพเลอร์ C ++ สร้างชื่อสัญลักษณ์ที่แตกต่างจาก C คอมไพเลอร์ ดังนั้นหากคุณพยายามโทรไปยังฟังก์ชั่นที่อยู่ในไฟล์ C ซึ่งคอมไพล์เป็นรหัส C คุณต้องบอกคอมไพเลอร์ C ++ ว่าชื่อสัญลักษณ์ที่พยายามแก้ไขดูแตกต่างจากค่าเริ่มต้น มิฉะนั้นขั้นตอนการเชื่อมโยงจะล้มเหลว
extern "C" {}
สร้างสั่งคอมไพเลอร์ที่จะไม่ดำเนินการ mangling ชื่อประกาศภายในวงเล็บ โดยทั่วไปคอมไพเลอร์ C ++ "เพิ่ม" ชื่อฟังก์ชั่นเพื่อให้พวกเขาเข้ารหัสข้อมูลประเภทเกี่ยวกับข้อโต้แย้งและค่าตอบแทน; นี้เรียกว่าชื่อแหลกเหลว extern "C"
สร้างป้องกันไม่ให้ mangling
โดยทั่วไปจะใช้เมื่อรหัส C ++ ต้องเรียกใช้ไลบรารีภาษา C มันอาจจะใช้เมื่อเปิดเผยฟังก์ชัน C ++ (จากตัวอย่างเช่น DLL) ไปยังไคลเอนต์ C
ใช้เพื่อแก้ไขปัญหาเกี่ยวกับการตั้งชื่อ extern C หมายถึงฟังก์ชั่นอยู่ใน "C-style API" แบบแบน
ถอดรหัสg++
ไบนารีที่สร้างขึ้นเพื่อดูว่าเกิดอะไรขึ้น
เพื่อทำความเข้าใจว่าเหตุใดจึงextern
มีความจำเป็นสิ่งที่ดีที่สุดที่ควรทำคือเข้าใจสิ่งที่เกิดขึ้นในรายละเอียดในไฟล์วัตถุด้วยตัวอย่าง:
main.cpp
void f() {}
void g();
extern "C" {
void ef() {}
void eg();
}
/* Prevent g and eg from being optimized away. */
void h() { g(); eg(); }
คอมไพล์ด้วยเอาท์พุทGCC 4.8 Linux ELF :
g++ -c main.cpp
ถอดรหัสตารางสัญลักษณ์:
readelf -s main.o
ผลลัพธ์ประกอบด้วย:
Num: Value Size Type Bind Vis Ndx Name
8: 0000000000000000 6 FUNC GLOBAL DEFAULT 1 _Z1fv
9: 0000000000000006 6 FUNC GLOBAL DEFAULT 1 ef
10: 000000000000000c 16 FUNC GLOBAL DEFAULT 1 _Z1hv
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z1gv
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND eg
การตีความ
เราเห็นว่า:
ef
และeg
ถูกเก็บไว้ในสัญลักษณ์ที่มีชื่อเดียวกับในรหัส
สัญลักษณ์อื่น ๆ ถูกทำให้ยุ่งเหยิง เรามาแก้ปัญหาให้พวกมัน
$ c++filt _Z1fv
f()
$ c++filt _Z1hv
h()
$ c++filt _Z1gv
g()
สรุป: ทั้งสองประเภทสัญลักษณ์ต่อไปนี้ไม่ได้ถูกทำให้ยุ่งเหยิง:
Ndx = UND
) เพื่อให้ที่ลิงก์หรือเวลารันจากไฟล์อ็อบเจ็กต์อื่นดังนั้นคุณจะต้องใช้extern "C"
ทั้งคู่เมื่อโทร:
g++
ให้คาดหวังว่าจะมีสัญลักษณ์ที่ไม่มีข้อผิดพลาดเกิดขึ้นgcc
g++
เพื่อสร้างสัญลักษณ์ unmangled สำหรับgcc
ใช้สิ่งที่ไม่ทำงานในภายนอกค
เห็นได้ชัดว่าฟีเจอร์ C ++ ใด ๆ ที่ต้องใช้ชื่อ mangling จะไม่ทำงานภายในextern C
:
extern "C" {
// Overloading.
// error: declaration of C function ‘void f(int)’ conflicts with
void f();
void f(int i);
// Templates.
// error: template with C linkage
template <class C> void f(C i) { }
}
C ที่รันได้น้อยที่สุดจากตัวอย่าง C ++
เพื่อประโยชน์ของความสมบูรณ์และการออกใหม่ให้ดูที่: วิธีการใช้ไฟล์ต้นฉบับ C ในโครงการ C ++?
การเรียก C จาก C ++ นั้นง่ายมาก: ฟังก์ชัน C แต่ละตัวมีสัญลักษณ์ที่ไม่เป็นไปได้เพียงสัญลักษณ์เดียวเท่านั้นดังนั้นจึงไม่จำเป็นต้องทำงานเพิ่มเติม
main.cpp
#include <cassert>
#include "c.h"
int main() {
assert(f() == 1);
}
CH
#ifndef C_H
#define C_H
/* This ifdef allows the header to be used from both C and C++. */
#ifdef __cplusplus
extern "C" {
#endif
int f();
#ifdef __cplusplus
}
#endif
#endif
ซีซี
#include "c.h"
int f(void) { return 1; }
วิ่ง:
g++ -c -o main.o -std=c++98 main.cpp
gcc -c -o c.o -std=c89 c.c
g++ -o main.out main.o c.o
./main.out
ไม่มีextern "C"
ลิงก์ล้มเหลวด้วย:
main.cpp:6: undefined reference to `f()'
เพราะg++
คาดว่าจะได้พบกับ mangled f
ซึ่งgcc
ไม่ได้ผลิต
ตัวอย่างบน GitHub
C ++ ที่รันได้น้อยที่สุดจากตัวอย่าง C
การเรียก C ++ จากนั้นยากขึ้นนิดหน่อย: เราต้องสร้างแต่ละฟังก์ชั่นที่ไม่ใช่แบบที่แยกกันของแต่ละฟังก์ชั่นที่เราต้องการเปิดเผย
ที่นี่เราแสดงให้เห็นถึงวิธีการเปิดเผยฟังก์ชั่น C ++ เกินพิกัดถึง C
main.c
#include <assert.h>
#include "cpp.h"
int main(void) {
assert(f_int(1) == 2);
assert(f_float(1.0) == 3);
return 0;
}
cpp.h
#ifndef CPP_H
#define CPP_H
#ifdef __cplusplus
// C cannot see these overloaded prototypes, or else it would get confused.
int f(int i);
int f(float i);
extern "C" {
#endif
int f_int(int i);
int f_float(float i);
#ifdef __cplusplus
}
#endif
#endif
cpp.cpp
#include "cpp.h"
int f(int i) {
return i + 1;
}
int f(float i) {
return i + 2;
}
int f_int(int i) {
return f(i);
}
int f_float(float i) {
return f(i);
}
วิ่ง:
gcc -c -o main.o -std=c89 -Wextra main.c
g++ -c -o cpp.o -std=c++98 cpp.cpp
g++ -o main.out main.o cpp.o
./main.out
ไม่extern "C"
ว่าจะล้มเหลวด้วย:
main.c:6: undefined reference to `f_int'
main.c:7: undefined reference to `f_float'
เนื่องจากg++
สัญลักษณ์ที่สร้างเป็น mangled ซึ่งgcc
ไม่สามารถหาได้
ตัวอย่างบน GitHub
ทดสอบใน Ubuntu 18.04