ข้อผิดพลาดในการออกแบบ API ใน C [ปิด]


10

มีข้อบกพร่องอะไรบ้างที่ผลักดันคุณให้คลั่งไคล้ใน C APIs (รวมถึงห้องสมุดมาตรฐานห้องสมุดบุคคลที่สามและส่วนหัวภายในโครงการ) เป้าหมายคือการระบุข้อผิดพลาดในการออกแบบ API ใน C ดังนั้นผู้ที่เขียนไลบรารี C ใหม่สามารถเรียนรู้จากความผิดพลาดในอดีต

อธิบายว่าเหตุใดข้อบกพร่องจึงไม่ดี (ควรเป็นตัวอย่าง) และพยายามเสนอการปรับปรุง แม้ว่าวิธีการแก้ปัญหาของคุณอาจไม่สามารถใช้งานได้จริงในชีวิตจริง (มันสายเกินไปที่จะแก้ไขstrncpy) แต่ก็ควรให้ความสำคัญกับนักเขียนห้องสมุดในอนาคต

แม้ว่าจุดเน้นของคำถามนี้คือ C APIs แต่ปัญหาที่ส่งผลต่อความสามารถในการใช้ภาษาเหล่านี้ในภาษาอื่นก็สามารถทำได้

โปรดให้หนึ่งข้อบกพร่องต่อคำตอบเพื่อประชาธิปไตยสามารถเรียงลำดับคำตอบ


3
โจอี้คำถามนี้กำลังจะไม่สร้างสรรค์โดยขอให้สร้างรายการสิ่งที่ผู้คนเกลียดชัง มีความเป็นไปได้ที่คำถามนี้จะมีประโยชน์หากคำตอบอธิบายว่าทำไมการปฏิบัติที่ชี้ให้เห็นไม่ดีและให้ข้อมูลโดยละเอียดเกี่ยวกับวิธีการปรับปรุง ด้วยเหตุนี้โปรดย้ายตัวอย่างของคุณจากคำถามไปเป็นคำตอบของมันเองและอธิบายว่าทำไมมันถึงมีปัญหา / วิธีmallocสตริง 'd จะแก้ไขได้อย่างไร ฉันคิดว่าการเป็นตัวอย่างที่ดีกับคำตอบแรกอาจช่วยให้คำถามนี้งอกงามได้ ขอบคุณ!
อดัมเลียร์

1
@ แอนนาเลียร์: ขอบคุณที่บอกฉันว่าทำไมคำถามของฉันเป็นปัญหา ฉันพยายามทำให้มันสร้างสรรค์โดยขอตัวอย่างและเสนอทางเลือกอื่น ฉันเดาว่าฉันต้องการตัวอย่างเพื่อระบุสิ่งที่ฉันมีอยู่ในใจ
Joey Adams

@ Joey Adams ลองดูวิธีนี้ คุณกำลังถามคำถามที่ควรจะ "แก้ปัญหา" C API โดยทั่วไปด้วยวิธีการทั่วไป ที่ไซต์เช่น StackOverflow ได้รับการออกแบบมาให้ทำงานเช่นนั้นพบปัญหาทั่วไปเกี่ยวกับการเขียนโปรแกรมและตอบได้ง่ายขึ้น StackOverflow จะส่งผลให้รายการคำตอบสำหรับคำถามของคุณเป็นธรรมชาติ แต่มีวิธีการที่ค้นหาได้ง่ายขึ้น
Andrew T Finnell

ฉันโหวตให้ปิดคำถามของตัวเอง เป้าหมายของฉันคือมีชุดของคำตอบที่สามารถใช้เป็นรายการตรวจสอบกับไลบรารี C ใหม่ คำตอบทั้งสามนั้นใช้คำทั้งหมดเช่น "ไม่สอดคล้องกัน", "ไร้เหตุผล" หรือ "สับสน" เราไม่สามารถตัดสินได้ว่า API นั้นละเมิดคำตอบใด ๆ หรือไม่
Joey Adams

คำตอบ:


5

ฟังก์ชั่นที่มีค่าส่งคืนที่ไม่สอดคล้องกันหรือไร้เหตุผล สองตัวอย่างที่ดี:

1) ฟังก์ชัน windows บางอย่างที่ส่งกลับค่า HANDLE ให้ใช้ NULL / 0 สำหรับข้อผิดพลาด (CreateThread) บางฟังก์ชันใช้ INVALID_HANDLE_VALUE / -1 สำหรับข้อผิดพลาด (CreateFile)

2) ฟังก์ชัน POSIX 'time' ส่งคืน '(time_t) -1' โดยมีข้อผิดพลาดซึ่งไม่มีเหตุผลจริงๆเนื่องจาก 'time_t' อาจเป็นประเภทที่ลงชื่อหรือไม่ได้ลงนาม


2
ที่จริงแล้ว time_t คือ (ปกติ) ลงนาม อย่างไรก็ตามการเรียก 31 ธันวาคม 1969 "ไม่ถูกต้อง" ค่อนข้างไร้เหตุผล ผมคิดว่ายุค 60s ขรุขระ :-) จริงจังในการแก้ปัญหาจะเป็นที่จะกลับรหัสข้อผิดพลาดและผ่านผลผ่านตัวชี้เช่น: และint time(time_t *out); BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);
Joey Adams

เผง มันแปลกถ้า time_t คือไม่ได้ลงนามและถ้า time_t มีการลงนามก็จะทำให้หนึ่งที่ไม่ถูกต้องเวลาในช่วงกลางของมหาสมุทรของคนที่ถูกต้องนั้น
David Schwartz

4

ฟังก์ชั่นหรือพารามิเตอร์ที่มีชื่อที่ไม่อธิบายหรือทำให้เกิดความสับสน ตัวอย่างเช่น:

1) CreateFile ใน Windows API ไม่ได้สร้างไฟล์จริงๆมันสร้างตัวจัดการไฟล์ มันสามารถสร้างไฟล์เช่นเดียวกับ 'เปิด' ถ้าถามถึงผ่านพารามิเตอร์ พารามิเตอร์นี้มีค่าที่เรียกว่า 'CREATE_ALWAYS' และ 'CREATE_NEW' โดยที่ชื่อนั้นไม่ได้บอกใบ้ถึงความหมายของมัน ('CREATE_ALWAYS' หมายความว่าไฟล์ล้มเหลวหรือไม่หรือสร้างไฟล์ใหม่ที่ด้านบนของไฟล์หรือไม่ 'CREATE_NEW' หมายความว่าสร้างไฟล์ใหม่เสมอและล้มเหลวหากไฟล์มีอยู่แล้วหรือไม่หรือสร้างใหม่ ไฟล์ด้านบนของมัน?)

2) pthread_cond_wait ใน POSIX pthreads API ซึ่งแม้ชื่อจะเป็นการรออย่างไม่มีเงื่อนไข


1
condในpthread_cond_waitไม่ได้หมายความว่า "เงื่อนไขรอ" มันหมายถึงความจริงที่ว่าคุณกำลังรอบนตัวแปรสภาพ
Jonathon Reinhart

ขวาก็รออย่างไม่มีเงื่อนไขสำหรับเงื่อนไข
David Schwartz

3

ชนิดทึบแสงที่ส่งผ่านอินเทอร์เฟซเป็นการจัดการชนิดที่ถูกลบ แน่นอนว่าปัญหาคือคอมไพเลอร์ไม่สามารถตรวจสอบรหัสผู้ใช้เพื่อหาประเภทอาร์กิวเมนต์ที่ถูกต้อง

สิ่งนี้มาในรูปแบบและรสชาติที่หลากหลายรวมถึง แต่ไม่ จำกัด เพียง:

  • void* การละเมิด

  • ใช้intเป็นตัวจัดการทรัพยากร (ตัวอย่าง: ไลบรารี CDI)

  • อาร์กิวเมนต์ที่พิมพ์อย่างสตริง

ประเภทที่แตกต่างกันมากขึ้น (= ไม่สามารถใช้แทนกันได้อย่างสมบูรณ์) ถูกแมปกับประเภทที่ถูกลบประเภทเดียวกันที่เลวร้ายยิ่ง แน่นอนการรักษาคือการให้ตัวชี้ทึบแสง typesafe ตามบรรทัดของ (ตัวอย่าง C):

typedef struct Foo Foo;
typedef struct Bar Bar;

Foo* createFoo();
Bar* createBar();

int doSomething(Foo* foo);
void somethingElse(Foo* foo, Bar* bar);

void destroyFoo(Foo* foo);
void destroyBar(Bar* bar);

2

ฟังก์ชั่นที่มีสตริงที่ไม่สอดคล้องกันและมักจะส่งกลับข้อตกลงที่ยุ่งยาก

ตัวอย่างเช่นgetcwdขอบัฟเฟอร์ที่ผู้ใช้ระบุและขนาด ซึ่งหมายความว่าแอปพลิเคชันต้องตั้งค่าขีด จำกัด โดยพลการบนความยาวไดเรกทอรีปัจจุบันหรือทำสิ่งนี้ ( จาก CCAN ):

 /* *This* is why people hate C. */
len = 32;
cwd = talloc_array(ctx, char, len);
while (!getcwd(cwd, len)) {
    if (errno != ERANGE) {
        talloc_free(cwd);
        return NULL;
    }
    cwd = talloc_realloc(ctx, cwd, char, len *= 2);
}

โซลูชันของฉัน: ส่งคืนmallocสตริง ed มันง่ายแข็งแกร่งและไม่มีประสิทธิภาพน้อยลง ยกเว้นแพลตฟอร์มฝังตัวและระบบเก่าmallocจริง ๆ แล้วค่อนข้างเร็ว


4
ฉันจะไม่เรียกวิธีปฏิบัติที่ไม่ดีนี้ฉันจะเรียกวิธีปฏิบัติที่ดีนี้ 1) เป็นเรื่องธรรมดามากที่โปรแกรมเมอร์ไม่ควรประหลาดใจ 2) ออกจากการจัดสรรไปยังผู้โทรซึ่งไม่รวมความเป็นไปได้มากมายของข้อบกพร่องการรั่วไหลของหน่วยความจำ 3) เข้ากันได้กับบัฟเฟอร์ที่ถูกจัดสรรแบบคงที่ 4) ทำให้ฟังก์ชั่นทำความสะอาดใช้งานฟังก์ชั่นการคำนวณสูตรทางคณิตศาสตร์บางอย่างไม่ควรเกี่ยวข้องกับสิ่งที่ไม่เกี่ยวข้องทั้งหมดเช่นการจัดสรรหน่วยความจำแบบไดนามิก คุณคิดว่า main ได้รับการทำความสะอาด แต่ฟังก์ชั่นที่ได้รับ messier 5) malloc ไม่ได้รับอนุญาตแม้แต่ในระบบหลายระบบ

@Lundin: ปัญหาคือมันนำไปสู่โปรแกรมเมอร์ที่สร้างข้อ จำกัด ฮาร์ดโค้ดที่ไม่จำเป็นและพวกเขาต้องพยายามอย่างหนักที่จะไม่ทำ (ดูตัวอย่างด้านบน) มันเป็นเรื่องที่ดีสำหรับสิ่งที่ต้องการsnprintf(buf, 32, "%d", n)ที่ระยะเวลาในการส่งออกเป็นที่คาดหมาย (แน่นอนไม่เกิน 30 เว้นแต่intเป็นจริงอย่างมากต่อระบบของคุณ) แท้จริงแล้ว malloc ไม่สามารถใช้ได้ในหลาย ๆ ระบบ แต่สำหรับสภาพแวดล้อมเดสก์ท็อปและเซิร์ฟเวอร์เป็นไปได้และทำงานได้ดีจริงๆ
Joey Adams

แต่ปัญหาคือฟังก์ชั่นในตัวอย่างของคุณไม่มีข้อ จำกัด ฮาร์ดโค้ด รหัสเช่นนี้ไม่ได้เป็นเรื่องธรรมดา ที่นี่หลักรู้สิ่งต่าง ๆ เกี่ยวกับความยาวของบัฟเฟอร์ที่ฟังก์ชันควรทราบ ทุกอย่างแสดงถึงการออกแบบโปรแกรมที่ไม่ดี หลักดูเหมือนจะไม่ทราบว่าฟังก์ชั่น getcwd ทำอะไรได้บ้างดังนั้นจึงใช้การจัดสรร "กำลังดุร้าย" เพื่อหาคำตอบ บางส่วนของอินเทอร์เฟซระหว่างโมดูลที่ getcwd อยู่และตัวเรียกถูกทำให้ยุ่งเหยิง นั่นไม่ได้หมายความว่าวิธีการเรียกฟังก์ชั่นนี้จะไม่ดีจากประสบการณ์ที่ตรงกันข้ามแสดงว่ามันดีสำหรับเหตุผลที่ฉันระบุไว้แล้ว

1

ฟังก์ชั่นที่รับ / คืนค่าข้อมูลชนิดผสมตามค่าหรือใช้การเรียกกลับ

ยิ่งแย่กว่านั้นถ้ากล่าวว่าประเภทเป็นสหภาพหรือมีบิตฟิลด์

จากมุมมองของผู้โทร C สิ่งเหล่านี้ใช้ได้จริง แต่ฉันไม่ได้เขียนใน C หรือ C ++ ยกเว้นว่าจำเป็นต้องใช้ดังนั้นฉันมักจะโทรผ่าน FFI FFIs ส่วนใหญ่ไม่สนับสนุนสหภาพหรือบิตฟิลด์และบางส่วน (เช่น Haskell และ MLton) ไม่สามารถรองรับ structs ที่ส่งผ่านตามค่า สำหรับสิ่งที่สามารถจัดการกับค่าโครงสร้างอย่างน้อย Common Lisp และ LuaJIT ถูกบังคับให้เข้าสู่เส้นทางที่ช้า - Common Foreign Interface Interface ของ Lisp ต้องทำการเรียกช้าผ่าน libffi และ LuaJIT ปฏิเสธที่จะรวบรวมเส้นทางของรหัสที่มีการโทร ฟังก์ชั่นที่อาจโทรกลับไปยังโฮสต์ยังทำให้เกิดเส้นทางที่ช้าใน LuaJIT, Java และ Haskell ด้วย LuaJIT ที่ไม่สามารถรวบรวมการโทรดังกล่าวได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.