C / C ++ พร้อม GCC: เพิ่มไฟล์ทรัพยากรลงในไฟล์ปฏิบัติการ / ไลบรารีแบบคงที่


94

ใครมีความคิดที่จะรวบรวมไฟล์ทรัพยากรใด ๆ แบบคงที่ในไฟล์ปฏิบัติการหรือไฟล์ไลบรารีที่แชร์โดยใช้ GCC

ตัวอย่างเช่นฉันต้องการเพิ่มไฟล์รูปภาพที่ไม่มีวันเปลี่ยนแปลง (และถ้าเป็นเช่นนั้นฉันก็ต้องแทนที่ไฟล์อยู่ดี) และไม่ต้องการให้พวกเขาอยู่ในระบบไฟล์

ถ้าเป็นไปได้ (และฉันคิดว่าเป็นเพราะ Visual C ++ สำหรับ Windows ก็ทำได้เช่นกัน) ฉันจะโหลดไฟล์ที่เก็บไว้ในไบนารีของตัวเองได้อย่างไร? ไฟล์ปฏิบัติการแยกวิเคราะห์ค้นหาไฟล์และดึงข้อมูลออกมาหรือไม่

อาจมีตัวเลือกสำหรับ GCC ที่ฉันยังไม่เคยเห็น การใช้เครื่องมือค้นหาไม่ได้คายสิ่งที่ถูกต้องออกไป

ฉันต้องการสิ่งนี้เพื่อใช้กับไลบรารีที่ใช้ร่วมกันและไฟล์ปฏิบัติการ ELF ปกติ

ขอความช่วยเหลือใด ๆ


3
อาจซ้ำกันของstackoverflow.com/questions/1997172/…
blueberryfields

ลิงก์ objcopy ในคำถาม blueberryfields ที่ชี้ไปก็เป็นวิธีแก้ปัญหาทั่วไปที่ดีเช่นกัน
Flexo

@blueberryfields: ขออภัยสำหรับการทำซ้ำ คุณถูก. โดยปกติฉันจะลงคะแนนให้ปิดว่าซ้ำกัน แต่เพราะพวกเขาทั้งหมดโพสต์คำตอบที่ดีฉันจึงจะยอมรับคำตอบ
Atmocreations

ฉันขอเพิ่มได้ไหมว่าวิธีการของ John Ripley น่าจะเป็นวิธีที่ดีที่สุดด้วยเหตุผลสำคัญประการหนึ่งนั่นคือการจัดตำแหน่ง ถ้าคุณทำ objcopy มาตรฐานหรือ "ld -r -b binary -o foo.o foo.txt" จากนั้นดูที่วัตถุที่เป็นผลลัพธ์ด้วย objdump -x ดูเหมือนว่าการจัดแนวของบล็อกจะถูกตั้งค่าเป็น 0 หากคุณต้องการ การจัดตำแหน่งให้ถูกต้องสำหรับข้อมูลไบนารีนอกเหนือจากถ่านฉันนึกไม่ออกว่านี่เป็นสิ่งที่ดี
carveone

คำตอบ:


51

ด้วยimagemagick :

convert file.png data.h

ให้สิ่งที่ชอบ:

/*
  data.h (PNM).
*/
static unsigned char
  MagickImage[] =
  {
    0x50, 0x36, 0x0A, 0x23, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 
    0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x0A, 0x32, 0x37, 
    0x37, 0x20, 0x31, 0x36, 0x32, 0x0A, 0x32, 0x35, 0x35, 0x0A, 0xFF, 0xFF, 
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 

....

สำหรับความเข้ากันได้กับรหัสอื่นคุณสามารถใช้fmemopenเพื่อรับFILE *วัตถุ"ปกติ" หรืออีกวิธีหนึ่งstd::stringstreamเพื่อสร้างiostreamไฟล์.std::stringstreamไม่เหมาะสำหรับสิ่งนี้และแน่นอนคุณสามารถใช้ตัวชี้ได้ทุกที่ที่คุณสามารถใช้ตัววนซ้ำได้

หากคุณใช้สิ่งนี้กับ automake อย่าลืมตั้งค่า BUILT_SOURCESเหมาะสม

สิ่งที่ดีในการทำเช่นนี้คือ:

  1. คุณได้รับข้อความดังนั้นจึงสามารถควบคุมเวอร์ชันและแก้ไขได้อย่างสมเหตุสมผล
  2. เป็นแบบพกพาและกำหนดไว้อย่างดีในทุกแพลตฟอร์ม

2
Bleahg! นั่นเป็นทางออกที่ฉันคิดเช่นกัน ทำไมใคร ๆ ก็อยากทำแบบนี้มากกว่าฉัน การจัดเก็บชิ้นส่วนของข้อมูลในเนมสเปซที่กำหนดไว้อย่างดีคือสิ่งที่ระบบไฟล์ใช้
Omnifarious

36
ในบางครั้งคุณมีไฟล์ปฏิบัติการซึ่งรันโดยที่ไม่มีระบบไฟล์หรือแม้แต่ไม่มีระบบปฏิบัติการ หรืออัลกอริทึมของคุณต้องการตารางที่คำนวณล่วงหน้าสำหรับการค้นหา และผมมั่นใจว่ามีมากขึ้นเมื่อกรณีการจัดเก็บข้อมูลในโปรแกรมที่ทำให้จำนวนมากของความรู้สึก
ndim

16
การใช้การแปลงนี้เหมือนกับxxd -i infile.bin outfile.h
greyfade

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

3
โปรดทราบว่าการกำหนดในส่วนหัวเช่นนี้หมายความว่าแต่ละไฟล์ที่รวมไว้จะได้รับสำเนาของตัวเอง จะดีกว่าถ้าประกาศในส่วนหัวเป็น extern แล้วกำหนดใน cpp ตัวอย่างที่นี่
Nicholas Smith

90

อัปเดตฉันชอบการควบคุมตามโซลูชันการประกอบของ John Ripley มากกว่า.incbinข้อเสนอและตอนนี้ใช้ตัวแปรกับสิ่งนั้น

ฉันใช้ objcopy (GNU binutils) เพื่อเชื่อมโยงข้อมูลไบนารีจากไฟล์ foo-data.bin ไปยังส่วนข้อมูลของไฟล์ปฏิบัติการ:

objcopy -B i386 -I binary -O elf32-i386 foo-data.bin foo-data.o

สิ่งนี้ให้foo-data.oไฟล์อ็อบเจ็กต์ที่คุณสามารถเชื่อมโยงไปยังไฟล์ปฏิบัติการของคุณได้ อินเทอร์เฟซ C มีลักษณะดังนี้

/** created from binary via objcopy */
extern uint8_t foo_data[]      asm("_binary_foo_data_bin_start");
extern uint8_t foo_data_size[] asm("_binary_foo_data_bin_size");
extern uint8_t foo_data_end[]  asm("_binary_foo_data_bin_end");

เพื่อให้คุณสามารถทำสิ่งต่างๆเช่น

for (uint8_t *byte=foo_data; byte<foo_data_end; ++byte) {
    transmit_single_byte(*byte);
}

หรือ

size_t foo_size = (size_t)((void *)foo_data_size);
void  *foo_copy = malloc(foo_size);
assert(foo_copy);
memcpy(foo_copy, foo_data, foo_size);

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


ความคิดที่ดี! ในกรณีของฉันมันไม่มีประโยชน์มากนัก แต่นี่คือสิ่งที่ฉันจะใส่ลงในคอลเลกชันตัวอย่างของฉันจริงๆ ขอบคุณสำหรับการแบ่งปัน!
Atmocreations

2
มันเป็นบิตง่ายต่อการใช้งานldเป็นรูปแบบผลลัพธ์เป็นนัยมีให้ดูstackoverflow.com/a/4158997/201725
ม.ค. Hudec

52

คุณสามารถฝังไฟล์ไบนารีในปฏิบัติการได้โดยใช้ldตัวเชื่อมโยง ตัวอย่างเช่นหากคุณมีไฟล์foo.barคุณสามารถฝังลงในไฟล์ปฏิบัติการได้โดยเพิ่มคำสั่งต่อไปนี้ld

--format=binary foo.bar --format=default

หากคุณกำลังเรียกใช้ldผ่านgccแล้วคุณจะต้องเพิ่ม-Wl

-Wl,--format=binary -Wl,foo.bar -Wl,--format=default

ที่นี่--format=binaryบอกผู้เชื่อมโยงว่าไฟล์ต่อไปนี้เป็นไบนารีและ--format=defaultเปลี่ยนกลับไปเป็นรูปแบบอินพุตเริ่มต้น (สิ่งนี้มีประโยชน์หากคุณจะระบุไฟล์อินพุตอื่นหลังจากfoo.bar )

จากนั้นคุณสามารถเข้าถึงเนื้อหาของไฟล์ของคุณจากรหัส:

extern uint8_t data[]     asm("_binary_foo_bar_start");
extern uint8_t data_end[] asm("_binary_foo_bar_end");

"_binary_foo_bar_size"นอกจากนี้ยังมีสัญลักษณ์ชื่อ ฉันคิดว่ามันเป็นประเภทuintptr_tแต่ฉันไม่ได้ตรวจสอบ


ความคิดเห็นที่น่าสนใจมาก ขอบคุณสำหรับการแบ่งปัน!
Atmocreations

1
ทำได้ดีนี่! คำถามเพียงข้อเดียว: ทำไมdata_endอาร์เรย์ถึงไม่ใช่ตัวชี้? (หรือนี่คือสำนวน C?)
xtofl

2
@xtofl ถ้าdata_endจะเป็นตัวชี้คอมไพเลอร์จะคิดว่ามีตัวชี้ที่เก็บไว้หลังเนื้อหาไฟล์ เหมือนกันถ้าคุณจะเปลี่ยนประเภทของdataตัวชี้คุณจะได้ตัวชี้ที่ประกอบด้วยไบต์แรกของไฟล์แทนที่จะเป็นตัวชี้ไปที่จุดเริ่มต้น ฉันคิดอย่างนั้น
Simon

1
+1: คำตอบของคุณช่วยให้ฉันสามารถฝังตัวโหลดคลาส java และ Jar ลงใน exe เพื่อสร้างตัวเรียกใช้งานจาวาแบบกำหนดเอง
Aubin

2
@xtofl - หากคุณจะกำหนดให้เป็นตัวชี้ให้กำหนดเป็นconst pointerไฟล์. คอมไพเลอร์ช่วยให้คุณสามารถเปลี่ยนค่าของพอยน์เตอร์ที่ไม่ใช่ค่าคงที่ได้ซึ่งจะไม่อนุญาตให้คุณเปลี่ยนค่าหากเป็นอาร์เรย์ ดังนั้นจึงอาจพิมพ์น้อยกว่าที่จะใช้ไวยากรณ์อาร์เรย์
Jesse Chisholm

41

คุณสามารถใส่ทรัพยากรทั้งหมดของคุณลงในไฟล์ ZIP และต่อท้ายไฟล์ปฏิบัติการได้ :

g++ foo.c -o foo0
zip -r resources.zip resources/
cat foo0 resources.zip >foo

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


7
ถ้าฉันต้องการเข้าร่วม foo0 และ resources.zip ใน foo ฉันต้อง> ถ้าฉันให้อินพุตทั้งสองในบรรทัดคำสั่งของ cat (เพราะฉันไม่ต้องการต่อท้ายสิ่งที่มีอยู่แล้วใน foo)
Nordic Mainframe

1
ใช่ความผิดพลาดของฉัน ฉันมองไม่เห็น 0 ที่นั่นในชื่ออย่างถูกต้องในการอ่านครั้งแรกของฉัน
Flexo

นี่คือความฉลาดมาก +1.
Linuxios

1
+1 ยอดเยี่ยมโดยเฉพาะอย่างยิ่งเมื่อจับคู่กับminiz
MVP

สิ่งนี้จะสร้างไบนารีที่ไม่ถูกต้อง (อย่างน้อยบน Mac และ Linux) ซึ่งไม่สามารถประมวลผลด้วยเครื่องมือเช่นinstall_name_tool. นอกจากนั้นไบนารียังคงทำงานเป็นปฏิบัติการได้
Andy Li

37

จากhttp://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967 :

เมื่อเร็ว ๆ นี้ฉันจำเป็นต้องฝังไฟล์ในไฟล์ปฏิบัติการ เนื่องจากฉันทำงานที่บรรทัดคำสั่งกับ gcc และอื่น ๆ และไม่ใช่ด้วยเครื่องมือ RAD สุดเก๋ที่ทำให้ทุกอย่างเกิดขึ้นอย่างน่าอัศจรรย์มันไม่ชัดเจนสำหรับฉันในทันทีว่าจะทำให้สิ่งนี้เกิดขึ้นได้อย่างไร การค้นหาบนเน็ตพบว่ามีการแฮ็กเพื่อจับมันไปยังส่วนท้ายของไฟล์ปฏิบัติการจากนั้นถอดรหัสว่ามันอยู่ที่ไหนโดยอาศัยข้อมูลจำนวนมากที่ฉันไม่ต้องการรู้ ดูเหมือนจะมีวิธีที่ดีกว่านี้ ...

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

สมมติว่าเรามีชื่อไฟล์ data.txt ที่เราต้องการฝังในไฟล์ปฏิบัติการของเรา:

# cat data.txt
Hello world

ในการแปลงไฟล์นี้เป็นไฟล์ออบเจ็กต์ที่เราสามารถเชื่อมโยงกับโปรแกรมของเราเราเพียงแค่ใช้ objcopy เพื่อสร้างไฟล์ ".o":

# objcopy --input binary \
--output elf32-i386 \
--binary-architecture i386 data.txt data.o

สิ่งนี้จะบอก objcopy ว่าไฟล์อินพุตของเราอยู่ในรูปแบบ "ไบนารี" ว่าไฟล์เอาต์พุตของเราควรอยู่ในรูปแบบ "elf32-i386" (ไฟล์ออบเจ็กต์บน x86) อ็อพชัน --binary-architecture จะบอก objcopy ว่าไฟล์เอาต์พุตมีไว้เพื่อ "รัน" บน x86 สิ่งนี้จำเป็นเพื่อให้ ld ยอมรับไฟล์สำหรับการเชื่อมโยงกับไฟล์อื่นสำหรับ x86 ใครจะคิดว่าการระบุรูปแบบผลลัพธ์เป็น "elf32-i386" จะบ่งบอกถึงสิ่งนี้ แต่ก็ไม่ใช่

ตอนนี้เรามีไฟล์ออบเจ็กต์แล้วเราจำเป็นต้องรวมไว้เมื่อเราเรียกใช้ตัวเชื่อมโยง:

# gcc main.c data.o

เมื่อเราเรียกใช้ผลลัพธ์เราจะได้รับการอธิษฐานเพื่อผลลัพธ์:

# ./a.out
Hello world

แน่นอนฉันยังไม่ได้เล่าเรื่องทั้งหมดและไม่ได้แสดงให้คุณเห็นหลัก c. เมื่อ objcopy ทำการแปลงข้างต้นจะเพิ่มสัญลักษณ์ "linker" ลงในไฟล์ออบเจ็กต์ที่แปลงแล้ว:

_binary_data_txt_start
_binary_data_txt_end

หลังจากเชื่อมโยงสัญลักษณ์เหล่านี้จะระบุจุดเริ่มต้นและจุดสิ้นสุดของไฟล์ฝังตัว ชื่อสัญลักษณ์ถูกสร้างขึ้นโดยการเติมไบนารีไว้ล่วงหน้าและต่อท้าย _start หรือ _end เข้ากับชื่อไฟล์ หากชื่อไฟล์มีอักขระใด ๆ ที่อาจไม่ถูกต้องในชื่อสัญลักษณ์จะถูกแปลงเป็นขีดล่าง (เช่น data.txt จะกลายเป็น data_txt) หากคุณได้รับชื่อที่ไม่ได้รับการแก้ไขเมื่อเชื่อมโยงโดยใช้สัญลักษณ์เหล่านี้ให้ทำ hexdump -C บนอ็อบเจ็กต์ไฟล์และดูที่ส่วนท้ายของดัมพ์สำหรับชื่อที่ objcopy เลือก

ตอนนี้รหัสที่จะใช้ไฟล์ฝังตัวควรมีความชัดเจนพอสมควร:

#include <stdio.h>

extern char _binary_data_txt_start;
extern char _binary_data_txt_end;

main()
{
    char*  p = &_binary_data_txt_start;

    while ( p != &_binary_data_txt_end ) putchar(*p++);
}

สิ่งหนึ่งที่สำคัญและละเอียดอ่อนที่ควรทราบก็คือสัญลักษณ์ที่เพิ่มในไฟล์ออบเจ็กต์ไม่ใช่ "ตัวแปร" ไม่มีข้อมูลใด ๆ แต่ที่อยู่คือคุณค่าของพวกเขา ฉันประกาศเป็นประเภท char เพราะสะดวกสำหรับตัวอย่างนี้: ข้อมูลที่ฝังไว้เป็นข้อมูลอักขระ อย่างไรก็ตามคุณสามารถประกาศเป็นอะไรก็ได้เช่น int ถ้าข้อมูลเป็นอาร์เรย์ของจำนวนเต็มหรือเป็น struct foo_bar_t ถ้าข้อมูลเป็นอาร์เรย์ของ foo bar หากข้อมูลที่ฝังไว้ไม่เหมือนกันแสดงว่า char น่าจะสะดวกที่สุด: ใช้ที่อยู่ของมันและส่งตัวชี้ไปยังประเภทที่ถูกต้องเมื่อคุณสำรวจข้อมูล


36

หากคุณต้องการควบคุมชื่อสัญลักษณ์ที่แน่นอนและตำแหน่งของรีซอร์สคุณสามารถใช้ (หรือสคริปต์) แอสเซมเบลอร์ GNU (ไม่ใช่ส่วนหนึ่งของ gcc) เพื่อนำเข้าไฟล์ไบนารีทั้งหมด ลองสิ่งนี้:

การประกอบ (x86 / แขน):

    .section .rodata

    .global thing
    .type   thing, @object
    .balign 4
thing:
    .incbin "meh.bin"
thing_end:

    .global thing_size
    .type   thing_size, @object
    .balign 4
thing_size:
    .int    thing_end - thing

ค:

#include <stdio.h>

extern const char thing[];
extern const unsigned thing_size;

int main() {
  printf("%p %u\n", thing, thing_size);
  return 0;
}

ไม่ว่าคุณจะใช้อะไรก็ควรสร้างสคริปต์เพื่อสร้างทรัพยากรทั้งหมดและมีชื่อสัญลักษณ์ที่ดี / สม่ำเสมอสำหรับทุกสิ่ง

ขึ้นอยู่กับข้อมูลของคุณและข้อมูลจำเพาะของระบบคุณอาจต้องใช้ค่าการจัดตำแหน่งที่แตกต่างกัน (ควรใช้.balignสำหรับการพกพา) หรือประเภทจำนวนเต็มที่มีขนาดแตกต่างกันสำหรับthing_sizeหรือประเภทองค์ประกอบอื่นสำหรับthing[]อาร์เรย์


ขอบคุณสำหรับการแบ่งปัน! ดูน่าสนใจแน่นอน แต่คราวนี้ไม่ใช่สิ่งที่ฉันกำลังมองหา =) นับถือ
Atmocreations

1
สิ่งที่ฉันกำลังมองหา บางทีคุณอาจตรวจสอบได้ว่ามันใช้ได้เช่นกันสำหรับไฟล์ที่มีขนาดไม่มองไม่เห็นด้วย 4 ดูเหมือนว่า thing_size จะรวมไบต์ของช่องว่างเพิ่มเติม
Pavel P

จะเป็นอย่างไรหากฉันต้องการให้เป็นสัญลักษณ์ของท้องถิ่น ฉันสามารถ cat เอาท์พุทคอมไพเลอร์พร้อมกับแอสเซมบลีของฉันเองได้ แต่มีวิธีที่ดีกว่านี้หรือไม่
user877329

สำหรับบันทึก: การแก้ไขของฉันแก้ไขปัญหาของ padding bytes เพิ่มเติมที่ @Pavel ระบุไว้
ndim

4

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

1) ใช้งานง่ายในรหัส

2) อัตโนมัติ (รวมอยู่ใน cmake / make ได้ง่าย)

3) ข้ามแพลตฟอร์ม

ฉันตัดสินใจที่จะเขียนเครื่องมือด้วยตัวเอง รหัสมีอยู่ที่นี่ https://github.com/orex/cpp_rsc

การใช้งานร่วมกับ cmake นั้นง่ายมาก

คุณควรเพิ่มรหัสดังกล่าวในไฟล์ CMakeLists.txt

file(DOWNLOAD https://raw.github.com/orex/cpp_rsc/master/cmake/modules/cpp_resource.cmake ${CMAKE_BINARY_DIR}/cmake/modules/cpp_resource.cmake) 

set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}/cmake/modules)

include(cpp_resource)

find_resource_compiler()
add_resource(pt_rsc) #Add target pt_rsc
link_resource_file(pt_rsc FILE <file_name1> VARIABLE <variable_name1> [TEXT]) #Adds resource files
link_resource_file(pt_rsc FILE <file_name2> VARIABLE <variable_name2> [TEXT])

...

#Get file to link and "resource.h" folder
#Unfortunately it is not possible with CMake add custom target in add_executable files list.
get_property(RSC_CPP_FILE TARGET pt_rsc PROPERTY _AR_SRC_FILE)
get_property(RSC_H_DIR TARGET pt_rsc PROPERTY _AR_H_DIR)

add_executable(<your_executable> <your_source_files> ${RSC_CPP_FILE})

ตัวอย่างจริงโดยใช้แนวทางสามารถดาวน์โหลดได้ที่นี่ https://bitbucket.org/orex/periodic_table


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