เหตุใดจึงต้องใช้ "การจัดการ" แบบทึบซึ่งต้องใช้การคัดเลือกใน Public API แทนที่จะเป็นตัวชี้ struct


27

ฉันกำลังประเมินห้องสมุดซึ่งปัจจุบัน API สาธารณะมีลักษณะเช่นนี้:

libengine.h

/* Handle, used for all APIs */
typedef size_t enh;


/* Create new engine instance; result returned in handle */
int en_open(int mode, enh *handle);

/* Start an engine */
int en_start(enh handle);

/* Add a new hook to the engine; hook handle returned in h2 */
int en_add_hook(enh handle, int hooknum, enh *h2);

ทราบว่าenhเป็นที่จับทั่วไปใช้เป็นหมายเลขอ้างอิงไปยังประเภทข้อมูลที่แตกต่างกัน ( เครื่องมือและตะขอ )

ภายใน APIs เหล่านี้ส่วนใหญ่แน่นอน "การจัดการ" กับโครงสร้างภายในที่พวกเขาได้malloc:

engine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, *enh handle)
{
    struct engine *en;

    en = malloc(sizeof(*en));
    if (!en)
        return -1;

    // ...initialization...

    *handle = (enh)en;
    return 0;
}

int en_start(enh handle)
{
    struct engine *en = (struct engine*)handle;

    return en->start(en);
}

ส่วนตัวฉันเกลียดการซ่อนสิ่งที่อยู่เบื้องหลังtypedefs โดยเฉพาะอย่างยิ่งเมื่อมันลดความปลอดภัยของประเภท (ให้ข้อenhฉันจะรู้ได้อย่างไรว่ามันหมายถึงอะไรจริง ๆ ?)

ดังนั้นฉันจึงส่งคำขอดึงแนะนำการเปลี่ยนแปลง API ต่อไปนี้ (หลังจากปรับเปลี่ยนทั้งห้องสมุดเพื่อให้สอดคล้อง):

libengine.h

struct engine;           /* Forward declaration */
typedef size_t hook_h;    /* Still a handle, for other reasons */


/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);

/* Start an engine */
int en_start(struct engine *en);

/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);

แน่นอนว่าสิ่งนี้ทำให้การใช้งาน API ภายในดูดีขึ้นมากกำจัดการปลดเปลื้องและการรักษาความปลอดภัยประเภทไปยัง / จากมุมมองของผู้บริโภค

libengine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, struct engine **en)
{
    struct engine *_e;

    _e = malloc(sizeof(*_e));
    if (!_e)
        return -1;

    // ...initialization...

    *en = _e;
    return 0;
}

int en_start(struct engine *en)
{
    return en->start(en);
}

ฉันชอบสิ่งนี้ด้วยเหตุผลดังต่อไปนี้:

อย่างไรก็ตามเจ้าของโครงการหยุดชะงักที่คำขอดึง (ถอดความ):

struct engineส่วนตัวผมไม่ชอบความคิดของการเปิดเผยนั้น ฉันยังคิดว่าวิธีปัจจุบันสะอาดและเป็นมิตรมากขึ้น

ตอนแรกฉันใช้ประเภทข้อมูลอื่นสำหรับการจัดการเบ็ด แต่จากนั้นตัดสินใจที่จะเปลี่ยนไปใช้enhดังนั้นการจัดการทุกประเภทจึงใช้ชนิดข้อมูลเดียวกันร่วมกันเพื่อให้ง่ายขึ้น หากสิ่งนี้สับสนเราสามารถใช้ประเภทข้อมูลอื่นได้อย่างแน่นอน

มาดูกันว่าคนอื่น ๆ คิดอย่างไรกับข่าวประชาสัมพันธ์นี้

ขณะนี้ไลบรารีนี้อยู่ในช่วงเบต้าส่วนตัวดังนั้นจึงไม่มีรหัสผู้บริโภคมากนักที่จะต้องกังวล (ยัง) นอกจากนี้ฉันยังทำให้งงชื่อเล็กน้อย


ทึบแสงจัดการอย่างไรดีกว่าโครงสร้างทึบแสงที่มีชื่อ?

หมายเหตุ: ฉันถามคำถามนี้ที่การตรวจสอบโค้ดซึ่งมันถูกปิด


1
ฉันได้แก้ไขชื่อเรื่องไปยังสิ่งที่ฉันเชื่อว่าชัดเจนกว่าเป็นการแสดงถึงแกนหลักของคำถามของคุณ อย่าลังเลที่จะยกเลิกถ้าฉันตีความผิด
Ixrec

1
@Ixrec ดีกว่านี้ขอบคุณ หลังจากเขียนคำถามทั้งหมดฉันหมดความสามารถในการคิดชื่อที่ดี
Jonathon Reinhart

คำตอบ:


33

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

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

สิ่งหนึ่งที่ควรสังเกต - API ใหม่ของคุณทำให้รหัสผู้ใช้ซับซ้อนขึ้นเล็กน้อยเนื่องจากแทนที่จะเขียน:

enh en;
en_open(ENGINE_MODE_1, &en);

ตอนนี้พวกเขาต้องการไวยากรณ์ที่ซับซ้อนมากขึ้นเพื่อประกาศหมายเลขอ้างอิงของพวกเขา:

struct engine* en;
en_open(ENGINE_MODE_1, &en);

อย่างไรก็ตามวิธีการแก้ปัญหาค่อนข้างง่าย:

struct _engine;
typedef struct _engine* engine

และตอนนี้คุณสามารถเขียนตรงไปตรงมา:

engine en;
en_open(ENGINE_MODE_1, &en);

ฉันลืมที่จะพูดถึงว่าห้องสมุดอ้างว่าทำตามสไตล์การเข้ารหัสของ Linuxซึ่งเกิดขึ้นกับสิ่งที่ฉันทำตามด้วย ในนั้นคุณจะเห็นว่าโครงสร้างการพิมพ์ที่ผิดเพี้ยนเพียงเพื่อหลีกเลี่ยงการเขียนstructนั้นหมดไปอย่างชัดเจน
Jonathon Reinhart

@ JonathonReinhart เขาพิมพ์ตัวชี้ไปที่ structไม่ใช่ตัวมันเอง
วงล้อประหลาด

@ JonathonReinhart และที่จริงแล้วอ่านลิงค์นั้นฉันเห็นว่าสำหรับ "วัตถุทึบแสงทั้งหมด" ได้รับอนุญาต (บทที่ 5 กฎก)
เฟืองล้อประหลาด

ใช่ แต่เฉพาะในกรณีที่หายากเป็นพิเศษ ฉันเชื่อโดยสุจริตว่ามีการเพิ่มเพื่อหลีกเลี่ยงการเขียนโค้ด mm ทั้งหมดใหม่เพื่อจัดการกับ pte typedefs ดูที่รหัสล็อคการหมุน มันเป็นส่วนโค้งที่เฉพาะเจาะจงอย่างสมบูรณ์ (ไม่มีข้อมูลทั่วไป) แต่พวกเขาไม่เคยใช้ typedef
Jonathon Reinhart

8
ฉันชอบtypedef struct engine engine;และการใช้engine*: FILE*หนึ่งชื่อน้อยแนะนำและที่ทำให้มันเห็นได้ชัดก็จับเหมือน
Deduplicator

16

ดูเหมือนจะมีความสับสนทั้งสองด้านที่นี่:

  • การใช้วิธีการจัดการไม่จำเป็นต้องใช้ประเภทการจัดการเดียวสำหรับการจัดการทั้งหมด
  • การเปิดเผยstructชื่อไม่เปิดเผยรายละเอียด (มีอยู่จริงเท่านั้น)

มีข้อดีในการใช้หมายเลขอ้างอิงแทนตัวชี้เปล่าในภาษาอย่าง C เนื่องจากการส่งตัวชี้อนุญาตการจัดการโดยตรงของ pointee (รวมถึงการเรียกไปยังfree) ในขณะที่การจัดการจุดจับต้องให้ไคลเอ็นต์ผ่าน API เพื่อดำเนินการใด ๆ .

อย่างไรก็ตามวิธีการของการจัดการที่มีชนิดเดียวที่กำหนดผ่าน a typedefไม่ปลอดภัยและอาจทำให้เกิดความโศกเศร้ามากมาย

ข้อเสนอแนะส่วนตัวของฉันจะเป็นการย้ายไปยังที่จับที่ปลอดภัยชนิดซึ่งฉันคิดว่าจะทำให้คุณทั้งคู่พอใจ นี่คือความสำเร็จค่อนข้างง่าย:

typedef struct {
    size_t id;
} enh;

typedef struct {
    size_t id;
} oth;

ตอนนี้ไม่มีใครสามารถผ่านได้โดยไม่ตั้งใจ2ขณะที่ไม่มีใครสามารถผ่านมือจับด้ามไม้กวาดซึ่งคาดว่าจะมีการจัดการกับเครื่องยนต์


ดังนั้นฉันจึงส่งคำขอดึงแนะนำการเปลี่ยนแปลง API ต่อไปนี้ (หลังจากแก้ไขไลบรารีทั้งหมดเพื่อให้สอดคล้อง)

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


1
ขอขอบคุณ. คุณไม่ได้เข้าไปเกี่ยวข้องกับที่จับ ฉันได้ติดตั้ง API แบบใช้มือจับตามจริงซึ่งไม่เคยมีพอยน์เตอร์ชี้แม้ว่าผ่าน typedef มันเกี่ยวข้องกับ ~ ค้นหาราคาแพงของข้อมูลที่เข้ามาของการเรียก API ทุก - วิธีมากเช่นลินุกซ์มีลักษณะขึ้นจากstruct file int fdนี่เป็น overkill สำหรับ IMO ไลบรารี่โหมดผู้ใช้อย่างแน่นอน
Jonathon Reinhart

@ JonathonReinhart: อืมเนื่องจากห้องสมุดมีมือจับอยู่แล้วฉันไม่รู้สึกว่าต้องขยายตัว อันที่จริงมีหลายวิธีจากการแปลงตัวชี้เป็นจำนวนเต็มเป็นการมี "พูล" และใช้ ID เป็นคีย์ คุณสามารถสลับวิธีการระหว่าง Debug (การค้นหา ID + สำหรับการตรวจสอบ) และ Release (เพียงแค่ตัวชี้ที่แปลงเป็นความเร็ว)
Matthieu M.

การใช้ดัชนีตารางจำนวนเต็มซ้ำจริง ๆ จะประสบปัญหา ABAที่วัตถุ (ดัชนี3) เป็นอิสระจากนั้นวัตถุใหม่ถูกสร้างขึ้นและน่าเสียดายที่ได้รับมอบหมายดัชนี3อีกครั้ง พูดง่ายๆคือมันเป็นเรื่องยากที่จะมีกลไกอายุการใช้งานวัตถุที่ปลอดภัยใน C ยกเว้นการนับการอ้างอิง (พร้อมกับข้อตกลงเกี่ยวกับการเป็นเจ้าของร่วมกับวัตถุ) ทำไว้ในส่วนที่ชัดเจนของการออกแบบ API
ร. ว.

2
@rwong: มันเป็นเพียงปัญหาในรูปแบบไร้เดียงสา; คุณสามารถรวมตัวนับยุคได้อย่างง่ายดายเช่นเมื่อระบุหมายเลขอ้างอิงเก่าคุณจะได้รับช่วงเวลาที่ไม่ตรงกัน
Matthieu M.

1
@Jonathon คำแนะนำเรื่องการเข้าถึงล่วงหน้า: คุณสามารถพูดถึง "กฎนามแฝงที่เข้มงวด" ในคำถามของคุณเพื่อช่วยคัดท้ายการอภิปรายไปยังประเด็นที่สำคัญกว่า
ร. ว.

3

นี่คือสถานการณ์ที่จำเป็นต้องใช้มือจับทึบแสง

struct SimpleEngine {
    int type;  // always SimpleEngine.type = 1
    int a;
};

struct ComplexEngine {
    int type;  // always ComplexEngine.type = 2
    int a, b, c;
};

int en_start(enh handle) {
    switch(*(int*)handle) {
    case 1:
        // treat handle as SimpleEngine
        return start_simple_engine(handle);
    case 2:
        // treat handle as ComplexEngine
        return start_complex_engine(handle);
    }
}

เมื่อไลบรารีมีชนิดของโครงสร้างอย่างน้อยสองชนิดที่มีส่วนหัวเดียวกันของเขตข้อมูลเช่น "ประเภท" ด้านบนชนิดโครงสร้างเหล่านี้อาจพิจารณาว่ามีโครงสร้างหลักทั่วไป (เช่นคลาสพื้นฐานใน C ++)

คุณสามารถกำหนดส่วนหัวเป็น "struct engine" เช่นนี้

struct engine {
    int type;
};

struct SimpleEngine {
    struct engine base;
    int a;
};

struct ComplexEngine {
    struct engine base;
    int a, b, c;
};

int en_start(struct engine *en) { ... }

แต่เป็นการตัดสินใจที่เลือกได้เพราะความจำเป็นในการใช้งานคาสท์ไม่ว่าจะใช้เอ็นจิน struct

ข้อสรุป

ในบางกรณีมีเหตุผลว่าทำไมจึงใช้ตัวจัดการทึบแสงแทน opaque ที่มีชื่อว่า


ฉันคิดว่าการใช้สหภาพทำให้สิ่งนี้ปลอดภัยกว่าการปลดเปลื้องที่อันตรายไปสู่ทุ่งนา ลองดูส่วนนี้ที่ฉันรวบรวมไว้แสดงตัวอย่างที่สมบูรณ์
Jonathon Reinhart

แต่ที่จริงแล้วการหลีกเลี่ยงswitchในตอนแรกโดยใช้ "ฟังก์ชั่นเสมือนจริง" อาจเป็นอุดมคติและแก้ปัญหาทั้งหมดได้
Jonathon Reinhart

การออกแบบที่สำคัญของคุณนั้นซับซ้อนกว่าที่ฉันแนะนำ แน่นอนว่ามันช่วยให้การหล่อน้อยลงพิมพ์ปลอดภัยและฉลาดขึ้น แต่แนะนำรหัสและประเภทเพิ่มเติม ในความคิดของฉันดูเหมือนจะยุ่งยากเกินกว่าที่จะได้รับความปลอดภัย ฉันและบางทีผู้เขียนห้องสมุดตัดสินใจที่จะติดตามKISSแทนที่จะเป็นประเภทความปลอดภัย
Akio Takahashi

ถ้าคุณต้องการให้มันง่ายจริง ๆ คุณสามารถละเว้นการตรวจสอบข้อผิดพลาดได้อย่างสมบูรณ์เช่นกัน!
Jonathon Reinhart

ในความคิดของฉันความเรียบง่ายในการออกแบบเป็นที่ต้องการมากกว่าการตรวจสอบข้อผิดพลาดบางอย่าง ในกรณีนี้การตรวจสอบข้อผิดพลาดดังกล่าวมีอยู่ในฟังก์ชัน API เท่านั้น นอกจากนี้คุณสามารถลบการบล็อกประเภทโดยใช้สหภาพ แต่โปรดจำไว้ว่าการรวมกลุ่มนั้นไม่ปลอดภัยตามประเภท
Akio Takahashi

2

ประโยชน์ที่ชัดเจนที่สุดของวิธีการจัดการคือคุณสามารถปรับเปลี่ยนโครงสร้างภายในโดยไม่ทำลาย API ภายนอก ได้รับคุณยังต้องแก้ไขซอฟต์แวร์ไคลเอ็นต์ แต่อย่างน้อยคุณไม่ได้เปลี่ยนอินเทอร์เฟซ

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

เนื่องจากคุณจะให้โครงสร้างแก่ลูกค้าของคุณอยู่แล้วคุณต้องเสียสละความปลอดภัยเล็กน้อย (ซึ่งยังสามารถตรวจสอบได้ที่รันไทม์) สำหรับ API ที่ง่ายกว่าแม้ว่าจะต้องใช้การคัดเลือก


5
"คุณสามารถปรับเปลี่ยนโครงสร้างภายในโดยไม่ต้อง .. " - คุณยังสามารถใช้วิธีการส่งต่อประกาศ
user253751

วิธีการ "ประกาศไปข้างหน้า" ไม่ได้ต้องการให้คุณประกาศลายเซ็นประเภทหรือไม่ และลายเซ็นประเภทนั้นยังคงไม่เปลี่ยนแปลงหรือไม่ถ้าคุณเปลี่ยน structs?
Robert Harvey

ประกาศไปข้างหน้าจะต้องให้คุณประกาศชื่อของประเภท - โครงสร้างมันยังคงซ่อนอยู่
Idan Arye

ถ้าเป็นเช่นนั้นจะไม่ได้บังคับใช้โครงสร้างประเภทอะไร
Robert Harvey

6
@ RobertHarvey โปรดจำไว้ว่า - นี่คือ C ที่เรากำลังพูดถึง ไม่มีเมธอดดังนั้นนอกเหนือจากชื่อและโครงสร้างไม่มีสิ่งอื่นใดกับชนิด ถ้ามันไม่ได้บังคับใช้โครงสร้างก็จะได้รับจะเหมือนกับการประกาศปกติ จุดของการเปิดเผยชื่อโดยไม่บังคับใช้โครงสร้างคือคุณสามารถใช้ชนิดนั้นในฟังก์ชันลายเซ็นได้ แน่นอนว่าถ้าไม่มีโครงสร้างคุณอาจใช้พอยน์เตอร์กับประเภทเท่านั้นเนื่องจากคอมไพเลอร์ไม่สามารถรู้ขนาดของมันได้ แต่เนื่องจากไม่มีพอยน์เตอร์ชี้ชัดใน C ที่ใช้พอยน์เตอร์นั้นดีพอสำหรับการพิมพ์แบบสแตติกเพื่อปกป้องคุณ
Idan Arye

2

Deja Vu

ทึบแสงจัดการอย่างไรดีกว่าโครงสร้างทึบแสงที่มีชื่อ?

ฉันพบสถานการณ์เดียวกันแน่นอนโดยมีความแตกต่างเล็กน้อยเท่านั้น เรามี SDK หลายอย่างเช่นนี้:

typedef void* SomeHandle;

ข้อเสนอของฉันเพียงเพื่อให้ตรงกับประเภทภายในของเรา:

typedef struct SomeVertex* SomeHandle;

สำหรับบุคคลที่สามที่ใช้ SDK มันไม่ควรสร้างความแตกต่างใด ๆ มันเป็นประเภททึบแสง ใครสน? ไม่มีผลกับ ABI * หรือความเข้ากันได้ของแหล่งที่มาและการใช้ SDK รุ่นใหม่จะต้องมีปลั๊กอินที่จะคอมไพล์ใหม่

* โปรดทราบว่าตามที่ gnasher ชี้ไปที่จริงอาจมีบางกรณีที่ขนาดของบางอย่างเช่นตัวชี้ไปยัง struct และ void * อาจมีขนาดแตกต่างกันในกรณีที่มันจะส่งผลกระทบต่อ ABI เหมือนเขาฉันไม่เคยพบเจอมาก่อนเลย แต่จากมุมมองดังกล่าวที่สองสามารถปรับปรุงการพกพาได้ในบริบทที่คลุมเครือดังนั้นนั่นเป็นอีกเหตุผลหนึ่งที่ให้ความสำคัญกับข้อที่สองแม้ว่าอาจจะเป็นที่ถกเถียงกันสำหรับคนส่วนใหญ่

ข้อบกพร่องของบุคคลที่สาม

นอกจากนี้ฉันมีสาเหตุมากกว่าความปลอดภัยประเภทสำหรับการพัฒนาภายใน / การดีบัก เรามีนักพัฒนาปลั๊กอินจำนวนมากที่มีข้อบกพร่องในรหัสของพวกเขาเพราะทั้งสองจัดการที่คล้ายกัน ( PanelและPanelNewเช่น) ทั้งคู่ใช้void*typedef สำหรับการจัดการของพวกเขาและพวกเขาบังเอิญผ่านในการจัดการที่ไม่ถูกต้องไปยังสถานที่ที่ผิดvoid*สำหรับทุกอย่าง. ดังนั้นมันจึงก่อให้เกิดข้อบกพร่องที่ด้านข้างของผู้ใช้SDK ข้อผิดพลาดของพวกเขายังทำให้ทีมพัฒนาภายในเสียเวลาอย่างมากเนื่องจากพวกเขาจะส่งรายงานข้อผิดพลาดเกี่ยวกับข้อบกพร่องใน SDK ของเราและเราจะต้องแก้ไขข้อผิดพลาดของปลั๊กอินและพบว่ามันเกิดขึ้นจริงจากข้อผิดพลาดในปลั๊กอิน ไปยังสถานที่ที่ไม่ถูกต้อง (ซึ่งได้รับอนุญาตอย่างง่ายดายโดยไม่มีการเตือนเมื่อจับทุกครั้งเป็นนามแฝงสำหรับvoid*หรือsize_t) ดังนั้นเราจึงไม่จำเป็นต้องเสียเวลาในการให้บริการแก้จุดบกพร่องสำหรับบุคคลที่สามเนื่องจากความผิดพลาดที่เกิดขึ้นในส่วนของพวกเขาเพราะเราต้องการความบริสุทธิ์ทางความคิดในการซ่อนข้อมูลภายในทั้งหมดแม้แต่ชื่อภายในของเราstructsเท่านั้น

การรักษา Typedef

ความแตกต่างคือฉันเสนอว่าเราจะทำtypedefยังไม่ให้ลูกค้าเขียนstruct SomeVertexซึ่งจะมีผลต่อความเข้ากันได้ของแหล่งที่มาสำหรับการเปิดตัวปลั๊กอินในอนาคต ในขณะที่ฉันชอบความคิดที่ไม่ได้พิมพ์ออกไปstructใน C เป็นการส่วนตัวจากมุมมองของ SDK แต่สิ่งที่typedefสามารถช่วยได้เนื่องจากประเด็นทั้งหมดคือความทึบ ดังนั้นฉันขอแนะนำให้ผ่อนคลายมาตรฐานนี้เฉพาะสำหรับ API ที่เปิดเผยต่อสาธารณะ สำหรับลูกค้าที่ใช้ SDK มันไม่สำคัญว่าตัวจัดการจะเป็นตัวชี้ไปยังโครงสร้างหรือเลขจำนวนเต็ม ฯลฯ สิ่งเดียวที่สำคัญสำหรับพวกเขาก็คือตัวจัดการสองตัวที่แตกต่างกันจะไม่ใช้นามแฝงชนิดข้อมูลเดียวกัน ผ่านไปอย่างไม่ถูกต้องในการจัดการที่ผิดไปยังสถานที่ที่ผิด

ข้อมูลประเภท

ที่ใดที่สำคัญที่สุดเพื่อหลีกเลี่ยงการแคสติ้งก็คือคุณ devs ภายใน สุนทรียศาสตร์แบบนี้ในการซ่อนชื่อภายในทั้งหมดไว้ใน SDK นั้นคือสุนทรียะเชิงแนวคิดบางอย่างที่มีค่าใช้จ่ายจำนวนมากในการสูญเสียข้อมูลทุกประเภทและทำให้เราต้องพรมโรยหน้าผู้ใช้ของเราเพื่อรับข้อมูลสำคัญ ในขณะที่โปรแกรมเมอร์ C ควรคุ้นเคยกับเรื่องนี้เป็นส่วนใหญ่ใน C การต้องการสิ่งนี้โดยไม่จำเป็นเพียงแค่ขอปัญหา

แนวคิดเชิงอุดมคติ

โดยทั่วไปคุณต้องการระวังสำหรับนักพัฒนาประเภทเหล่านั้นที่วางแนวคิดเกี่ยวกับความบริสุทธิ์เหนือความต้องการในชีวิตประจำวัน สิ่งเหล่านี้จะทำให้การบำรุงรักษาโค้ดเบสของคุณอยู่ในระดับสูงในการค้นหาอุดมคติ Utopian ทำให้ทั้งทีมหลีกเลี่ยงโลชั่นผิวเกรียมเพราะถูกแดดในทะเลทรายเพราะกลัวว่าไม่เป็นธรรมชาติและอาจทำให้เกิดการขาดวิตามินดีในขณะที่ลูกเรือครึ่งหนึ่งกำลังจะตายจากมะเร็งผิวหนัง

ค่ากำหนดของผู้ใช้

แม้จากจุดยืนที่เข้มงวดของผู้ใช้ที่ใช้ API พวกเขาจะชอบ buggy API หรือ API ที่ทำงานได้ดี แต่จะเปิดเผยชื่อบางชื่อที่พวกเขาแทบจะไม่สนใจในการแลกเปลี่ยน? เพราะนั่นคือการแลกเปลี่ยนในทางปฏิบัติ การสูญเสียข้อมูลประเภทที่ไม่จำเป็นนอกบริบททั่วไปนั้นเป็นการเพิ่มความเสี่ยงของข้อผิดพลาดและจาก codebase ขนาดใหญ่ในการตั้งค่าทั่วทั้งทีมเป็นเวลาหลายปี หากคุณเพิ่มความเสี่ยงของข้อบกพร่องอย่างมากโอกาสที่คุณจะได้รับข้อบกพร่องเพิ่มขึ้นอย่างน้อย ไม่ต้องใช้เวลานานในการจัดตั้งทีมขนาดใหญ่เพื่อค้นหาว่าความผิดพลาดของมนุษย์ทุกประเภทที่จินตนาการได้ในที่สุดจะเปลี่ยนจากศักยภาพสู่ความเป็นจริง

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

ข้อเสนอ

ดังนั้นฉันขอแนะนำให้ประนีประนอมที่นี่ซึ่งจะยังคงให้ความสามารถในการรักษาผลประโยชน์การแก้จุดบกพร่องทั้งหมด:

typedef struct engine* enh;

... แม้กระทั่งค่าใช้จ่ายในการพิมพ์ออกไปสิ่งstructนั้นจะฆ่าเราจริง ๆ ไหม? อาจจะไม่ดังนั้นฉันขอแนะนำวิธีปฏิบัติบางอย่างในส่วนของคุณเช่นกัน แต่มีมากขึ้นสำหรับนักพัฒนาที่ต้องการแก้ไขจุดบกพร่องให้ยากขึ้นโดยใช้size_tที่นี่และชี้ไปที่ / จากจำนวนเต็มโดยไม่มีเหตุผลดียกเว้นซ่อนข้อมูลเพิ่มเติมซึ่งมีอยู่ 99 % ซ่อนอยู่กับผู้ใช้และไม่สามารถทำอันตรายได้มากกว่าsize_tนี้


1
มันแตกต่างกันเล็กน้อย : ตามมาตรฐาน C ตัวชี้ "ชี้ไปที่โครงสร้าง" ทั้งหมดมีรูปแบบเหมือนกันดังนั้น "ตัวชี้ไปยังจุดรวม" จึงทำเช่นนั้น "โมฆะ *" และ "ตัวอักษร *" แต่ตัวชี้เป็นโมฆะและตัวชี้ เพื่อ struct "สามารถมีขนาดต่างกัน () และ / หรือการแสดงที่แตกต่าง ในทางปฏิบัติฉันไม่เคยเห็นสิ่งนี้
gnasher729

@ gnasher729 เช่นเดียวกันบางทีฉันควรมีสิทธิ์ได้รับส่วนที่เกี่ยวกับการสูญเสียการพกพาที่อาจเกิดขึ้นในการหล่อไปvoid*หรือsize_tกลับเป็นเหตุผลอื่นเพื่อหลีกเลี่ยงการหล่ออย่างล้นเหลือ ฉันข้ามมันไปเพราะฉันไม่เคยเห็นมันในทางปฏิบัติเพราะแพลตฟอร์มที่เรากำหนดเป้าหมาย (ซึ่งเป็นแพลตฟอร์มเดสก์ท็อปเสมอ: linux, OSX, Windows)

1
เราลงเอยด้วยtypedef struct uc_struct uc_engine;
Jonathon Reinhart

1

ฉันสงสัยว่าเหตุผลที่แท้จริงคือความเฉื่อยนั่นเป็นสิ่งที่พวกเขาทำมาตลอดและมันใช้งานได้ดีทำไมจึงเปลี่ยน

เหตุผลหลักที่ฉันเห็นคือมือจับทึบแสงช่วยให้นักออกแบบวางอะไรไว้ข้างหลังมันไม่ใช่แค่โครงสร้าง หาก API ส่งคืนและยอมรับประเภททึบแสงหลายประเภทพวกเขาทั้งหมดจะดูเหมือนกันกับผู้โทรและไม่เคยมีปัญหาการคอมไพล์หรือคอมไพล์ใหม่ที่จำเป็นหากการพิมพ์ดีมีการเปลี่ยนแปลง หากการเปลี่ยนแปลง en_NewFlidgetTwiddler (การจัดการ ** newTwiddler) เพื่อส่งกลับตัวชี้ไปยัง Twiddler แทนการจัดการ API จะไม่เปลี่ยนแปลงและรหัสใหม่ใด ๆ จะใช้ตัวชี้โดยไม่ต้องใช้ตัวจัดการก่อนที่จะใช้ตัวจัดการ เช่นกันไม่มีอันตรายจากระบบปฏิบัติการหรือสิ่งอื่นใดอย่างเงียบ ๆ "แก้ไข" ตัวชี้ถ้ามันผ่านข้ามขอบเขต

แน่นอนว่าข้อเสียของมันก็คือผู้โทรสามารถป้อนข้อมูลอะไรก็ได้ คุณมีสิ่ง 64 บิต? ใส่ลงในสล็อต 64 บิตในการเรียก API และดูว่าเกิดอะไรขึ้น

en_TwiddleFlidget(engine, twiddler, flidget)
en_TwiddleFlidget(engine, flidget, twiddler)

คอมไพล์ทั้งสอง แต่ฉันเดิมพันเพียงหนึ่งในนั้นทำในสิ่งที่คุณต้องการ


1

ฉันเชื่อว่าทัศนคตินั้นเกิดจากปรัชญาที่มีมาช้านานเพื่อปกป้อง API ไลบรารี C จากการละเมิดโดยผู้เริ่มต้น

โดยเฉพาะอย่างยิ่ง,

  • ผู้เขียนห้องสมุดรู้ว่ามันเป็นตัวชี้ไปยัง struct และรายละเอียดของ struct สามารถมองเห็นได้ด้วยรหัสห้องสมุด
  • โปรแกรมเมอร์ที่มีประสบการณ์ทุกคนที่ใช้ห้องสมุดก็รู้ว่ามันเป็นตัวชี้ไปยังโครงสร้างทึบแสง
    • พวกเขามีประสบการณ์ที่เจ็บปวดอย่างหนักพอที่จะรู้ว่าจะไม่ยุ่งกับไบต์ที่เก็บไว้ใน structs เหล่านั้น
  • โปรแกรมเมอร์ที่ไม่มีประสบการณ์รู้ว่าไม่รู้
    • พวกเขาจะพยายามmemcpyข้อมูลทึบแสงหรือเพิ่มไบต์หรือคำภายในโครงสร้าง ไปแฮ็ค

การตอบโต้แบบดั้งเดิมเป็นเวลานานคือ:

  • หลอกลวงความจริงที่ว่าด้ามจับทึบแสงเป็นตัวชี้ไปยังโครงสร้างทึบแสงที่มีอยู่ในกระบวนการหน่วยความจำเดียวกัน
    • ในการทำเช่นนั้นโดยอ้างว่ามันเป็นค่าจำนวนเต็มที่มีจำนวนบิตเท่ากับ a void*
    • เพื่อให้ดูพิถีพิถันเป็นพิเศษให้ทำการปลอมบิตของพอยน์เตอร์ด้วยเช่น
      struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);

นี่เป็นการบอกว่ามันมีประเพณีที่ยาวนาน ฉันไม่มีความเห็นส่วนตัวเกี่ยวกับว่าถูกหรือผิด


3
วิธีแก้ปัญหาของฉันก็ให้ความปลอดภัยเช่นกัน ลูกค้ายังไม่ทราบขนาดหรือเนื้อหาของโครงสร้างพร้อมประโยชน์เพิ่มเติมของความปลอดภัยประเภท ฉันไม่เห็นว่าการใช้ขนาดที่ไม่ถูกต้องในการถือตัวชี้นั้นดีกว่านี้อย่างไร
Jonathon Reinhart

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

คุณกำลังพูดเรื่องอะไร ทั้งหมดที่ฉันพูดคือคุณไม่สามารถรวบรวมรหัสใด ๆ ที่พยายามที่จะยกเลิกการอ้างอิงตัวชี้ไปยังโครงสร้างดังกล่าวหรือทำอะไรก็ได้ที่ต้องการความรู้เกี่ยวกับขนาดของมัน แน่นอนว่าคุณสามารถ memset (, 0,) ได้ตลอดทั้งกระบวนการ 'heap หากคุณต้องการ
Jonathon Reinhart

6
เรื่องนี้ฟังดูจำนวนมากเช่นการปกป้อง Machiavelli หากผู้ใช้ต้องการส่งผ่านขยะไปยัง API ของฉันไม่มีวิธีที่ฉันสามารถหยุดพวกเขาได้ ขอแนะนำอินเทอร์เฟซแบบไม่ปลอดภัยเช่นนี้ช่วยได้ยากเพราะมันทำให้การใช้ API ผิดวัตถุประสงค์โดยไม่ตั้งใจได้ง่ายขึ้น
ComicSansMS

@ComsSansMS ขอบคุณที่พูดถึง "อุบัติเหตุ" เพราะนั่นคือสิ่งที่ฉันพยายามป้องกันจริงๆที่นี่
Jonathon Reinhart
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.