Lập trình C: Làm thế nào để lập trình cho Unicode?


82

Điều kiện tiên quyết nào là cần thiết để lập trình Unicode nghiêm ngặt?

Điều này có ngụ ý rằng mã của tôi không nên sử dụng charcác loại ở bất kỳ đâu và cần sử dụng các hàm có thể xử lý wint_twchar_t?

Và vai trò của các chuỗi nhân vật nhiềubyte trong kịch bản này là gì?

Câu trả lời:


21

Lưu ý rằng đây không phải là về "lập trình unicode nghiêm ngặt" mà là một số kinh nghiệm thực tế.

Những gì chúng tôi đã làm ở công ty của tôi là tạo một thư viện trình bao bọc xung quanh thư viện ICU của IBM. Thư viện trình bao bọc có giao diện UTF-8 và chuyển đổi thành UTF-16 khi cần gọi ICU. Trong trường hợp của chúng tôi, chúng tôi không lo lắng quá nhiều về lượt truy cập hiệu suất. Khi hiệu suất có vấn đề, chúng tôi cũng cung cấp giao diện UTF-16 (sử dụng kiểu dữ liệu của riêng chúng tôi).

Các ứng dụng phần lớn có thể vẫn nguyên trạng (sử dụng ký tự), mặc dù trong một số trường hợp, chúng cần phải lưu ý một số vấn đề nhất định. Ví dụ, thay vì strncpy (), chúng tôi sử dụng trình bao bọc để tránh cắt bỏ các trình tự UTF-8. Trong trường hợp của chúng tôi, điều này là đủ, nhưng người ta cũng có thể xem xét kiểm tra việc kết hợp các ký tự. Chúng tôi cũng có các trình bao bọc để đếm số điểm mã, số lượng grapheme, v.v.

Khi giao tiếp với các hệ thống khác, đôi khi chúng tôi cần phải thực hiện thành phần ký tự tùy chỉnh, vì vậy bạn có thể cần một số linh hoạt ở đó (tùy thuộc vào ứng dụng của bạn).

Chúng tôi không sử dụng wchar_t. Sử dụng ICU tránh được các vấn đề không mong muốn về tính di động (tất nhiên là không phải các vấn đề không mong muốn khác :-).


2
Chuỗi byte UTF-8 hợp lệ sẽ không bao giờ bị ngắt (cắt ngắn) bởi strncpy. Các chuỗi UTF-8 hợp lệ không được chứa bất kỳ byte 0x00 nào (tất nhiên là ngoại trừ byte trống kết thúc).
Dan Molding

8
@Dan Molding: nếu bạn strncpy (), giả sử, một chuỗi chứa một ký tự tiếng Trung (có thể là 3 byte) thành một mảng char 2 byte, bạn tạo một chuỗi UTF-8 không hợp lệ.
Hans van Eck

@Hans van Eck: Nếu trình bao bọc của bạn sao chép ký tự Trung Quốc 3 byte duy nhất đó vào một mảng 2 byte, thì bạn sẽ cắt ngắn nó và tạo ra một chuỗi không hợp lệ hoặc bạn sẽ có hành vi không xác định. Rõ ràng, nếu bạn đang sao chép dữ liệu xung quanh, mục tiêu cần phải đủ lớn; mà đi mà không nói. Quan điểm của tôi là strncpysử dụng đúng cách là hoàn toàn an toàn khi sử dụng với UTF-8.
Dan Molding

5
@DanMoulding: Nếu bạn biết rằng bộ đệm mục tiêu của mình đủ lớn, bạn có thể sử dụng strcpy(điều này thực sự an toàn khi sử dụng với UTF-8). Những người sử dụng strncpycó thể làm như vậy vì họ không biết liệu bộ đệm đích có đủ lớn hay không, vì vậy họ muốn chuyển số byte tối đa để sao chép - điều này thực sự có thể tạo ra các chuỗi UTF-8 không hợp lệ.
Frerich Raabe

41

C99 trở xuống

Tiêu chuẩn C (C99) cung cấp các ký tự rộng và ký tự nhiều byte, nhưng vì không có gì đảm bảo về những ký tự rộng đó có thể chứa, giá trị của chúng có phần hạn chế. Đối với một triển khai nhất định, chúng cung cấp hỗ trợ hữu ích, nhưng nếu mã của bạn phải có thể di chuyển giữa các triển khai, thì không đủ đảm bảo rằng chúng sẽ hữu ích.

Do đó, cách tiếp cận được đề xuất bởi Hans van Eck (là viết một trình bao bọc xung quanh thư viện ICU - International Components for Unicode -) là IMO.

Mã hóa UTF-8 có nhiều ưu điểm, một trong số đó là nếu bạn không làm rối dữ liệu (ví dụ: bằng cách cắt ngắn nó), thì nó có thể được sao chép bởi các hàm không nhận thức đầy đủ về sự phức tạp của UTF-8 mã hóa. Điều này rõ ràng không phải là trường hợp với wchar_t.

Unicode đầy đủ là định dạng 21 bit. Tức là, Unicode bảo lưu các điểm mã từ U + 0000 đến U + 10FFFF.

Một trong những điều hữu ích về các định dạng UTF-8, UTF-16 và UTF-32 (trong đó UTF là viết tắt của Unicode Transformation Format - xem Unicode ) là bạn có thể chuyển đổi giữa ba dạng biểu diễn mà không bị mất thông tin. Mỗi cái có thể đại diện cho bất cứ thứ gì mà những cái khác có thể đại diện. Cả UTF-8 và UTF-16 đều là định dạng nhiều byte.

UTF-8 được biết đến là một định dạng nhiều byte, với cấu trúc cẩn thận giúp bạn có thể tìm thấy phần đầu của các ký tự trong một chuỗi một cách đáng tin cậy, bắt đầu từ bất kỳ điểm nào trong chuỗi. Các ký tự byte đơn có bit cao được đặt thành 0. Các ký tự nhiều byte có ký tự đầu tiên bắt đầu bằng một trong các mẫu bit 110, 1110 hoặc 11110 (đối với ký tự 2 byte, 3 byte hoặc 4 byte), với các byte tiếp theo luôn bắt đầu bằng 10. Các ký tự tiếp tục luôn nằm trong phạm vi 0x80 .. 0xBF. Có các quy tắc rằng các ký tự UTF-8 phải được trình bày ở định dạng tối thiểu có thể. Một hệ quả của các quy tắc này là các byte 0xC0 và 0xC1 (cũng 0xF5..0xFF) không thể xuất hiện trong dữ liệu UTF-8 hợp lệ.

 U+0000 ..   U+007F  1 byte   0xxx xxxx
 U+0080 ..   U+07FF  2 bytes  110x xxxx   10xx xxxx
 U+0800 ..   U+FFFF  3 bytes  1110 xxxx   10xx xxxx   10xx xxxx
U+10000 .. U+10FFFF  4 bytes  1111 0xxx   10xx xxxx   10xx xxxx   10xx xxxx

Ban đầu, người ta hy vọng rằng Unicode sẽ là một bộ mã 16 bit và mọi thứ sẽ phù hợp với không gian mã 16 bit. Thật không may, thế giới thực phức tạp hơn và nó phải được mở rộng sang mã hóa 21-bit hiện tại.

UTF-16 do đó là một bộ mã đơn vị (từ 16 bit) cho 'Mặt phẳng đa ngôn ngữ cơ bản', có nghĩa là các ký tự có mã Unicode điểm U + 0000 .. U + FFFF, nhưng sử dụng hai đơn vị (32 bit) cho ký tự bên ngoài phạm vi này. Do đó, mã hoạt động với mã hóa UTF-16 phải có khả năng xử lý các mã hóa độ rộng thay đổi, giống như UTF-8 phải. Các mã cho các ký tự đơn vị kép được gọi là mã thay thế.

Đại diện là các điểm mã từ hai dải giá trị Unicode đặc biệt, được dành riêng để sử dụng làm giá trị đầu và giá trị theo sau của các đơn vị mã được ghép nối trong UTF-16. Các đại diện thay thế hàng đầu, còn được gọi là cao, là từ U + D800 đến U + DBFF và các đại diện thay thế ở cuối, hoặc thấp là từ U + DC00 đến U + DFFF. Chúng được gọi là đại diện, vì chúng không đại diện trực tiếp cho các ký tự mà chỉ là một cặp.

Tất nhiên, UTF-32 có thể mã hóa bất kỳ điểm mã Unicode nào trong một đơn vị lưu trữ. Nó hiệu quả cho tính toán nhưng không hiệu quả để lưu trữ.

Bạn có thể tìm thêm nhiều thông tin tại các trang web ICU và Unicode.

C11 và <uchar.h>

Tiêu chuẩn C11 đã thay đổi các quy tắc, nhưng không phải tất cả các triển khai đều bắt kịp với những thay đổi ngay cả bây giờ (giữa năm 2017). Tiêu chuẩn C11 tóm tắt những thay đổi để hỗ trợ Unicode như:

  • Các ký tự và chuỗi Unicode ( <uchar.h>) (ban đầu được chỉ định trong ISO / IEC TR 19769: 2004)

Những gì sau đây là một phác thảo tối thiểu về chức năng. Đặc điểm kỹ thuật bao gồm:

6.4.3 Tên ký tự chung

Cú pháp
phổ-ký tự-tên:
    \u hex-quad
    \U hex-quad hex-quad
hex-quad:
    thập lục phân-chữ số thập lục phân-chữ số thập lục phân-chữ số thập lục phân-chữ số

7.28 Tiện ích Unicode <uchar.h>

Tiêu đề <uchar.h>khai báo các kiểu và chức năng để thao tác các ký tự Unicode.

Các kiểu được khai báo là mbstate_t(mô tả trong 7.29.1) và size_t(mô tả trong 7.19);

char16_t

là kiểu số nguyên không dấu được sử dụng cho các ký tự 16 bit và cùng kiểu với uint_least16_t(được mô tả trong 7.20.1.2); và

char32_t

là kiểu số nguyên không dấu được sử dụng cho các ký tự 32 bit và cùng kiểu với uint_least32_t(cũng được mô tả trong 7.20.1.2).

(Dịch các tham chiếu chéo: <stddef.h>định nghĩa size_t, <wchar.h>xác định mbstate_t<stdint.h>định nghĩa uint_least16_tuint_least32_t.) <uchar.h>Tiêu đề cũng xác định một tập hợp tối thiểu các hàm chuyển đổi (có thể khởi động lại):

  • mbrtoc16()
  • c16rtomb()
  • mbrtoc32()
  • c32rtomb()

Có các quy tắc về các ký tự Unicode có thể được sử dụng trong các mã định danh bằng cách sử dụng ký hiệu \unnnnhoặc \U00nnnnnn. Bạn có thể phải tích cực kích hoạt hỗ trợ cho các ký tự như vậy trong mã định danh. Ví dụ: GCC yêu cầu -fextended-identifierscho phép những điều này trong số nhận dạng.

Lưu ý rằng macOS Sierra (10.12.5), với tên gọi nhưng một nền tảng, không hỗ trợ <uchar.h>.


3
Tôi nghĩ rằng bạn đang bán hàng wchar_tvà bạn bè một chút ở đây. Những kiểu này rất cần thiết để cho phép thư viện C xử lý văn bản trong bất kỳ bảng mã nào (bao gồm cả các bảng mã không phải Unicode). Nếu không có các loại ký tự và hàm rộng, thư viện C sẽ yêu cầu một tập hợp các hàm xử lý văn bản cho mọi mã hóa được hỗ trợ: hãy tưởng tượng có koi8len, koi8tok, koi8printf chỉ dành cho văn bản được mã hóa KOI-8 và utf8len, utf8tok, utf8printf cho UTF-8 bản văn. Thay vào đó, chúng tôi may mắn khi có chỉ một bộ các chức năng này (không kể những cái ASCII gốc): wcslen, wcstok, và wprintf.
Dan Molding

1
Tất cả những gì một lập trình viên cần làm là sử dụng các hàm chuyển đổi ký tự của thư viện C ( mbstowcsvà bạn bè) để chuyển đổi bất kỳ bảng mã nào được hỗ trợ sang wchar_t. Khi đã được wchar_tđịnh dạng, người lập trình có thể sử dụng một tập hợp các hàm xử lý văn bản rộng mà thư viện C cung cấp. Việc triển khai thư viện C tốt sẽ hỗ trợ hầu như bất kỳ mã hóa nào mà hầu hết các lập trình viên sẽ cần (trên một trong các hệ thống của tôi, tôi có quyền truy cập vào 221 bảng mã duy nhất).
Dan Molding

Về việc liệu chúng có đủ rộng để hữu ích hay không: tiêu chuẩn yêu cầu việc triển khai phải đảm bảo wchar_tđủ rộng để chứa bất kỳ ký tự nào được hỗ trợ bởi việc triển khai. Điều này có nghĩa là (có thể có một ngoại lệ đáng chú ý) hầu hết các triển khai sẽ đảm bảo rằng chúng đủ rộng để một chương trình sử dụng wchar_tsẽ xử lý bất kỳ mã hóa nào được hệ thống hỗ trợ (của Microsoft wchar_tchỉ rộng 16 bit có nghĩa là việc triển khai của chúng không hỗ trợ đầy đủ tất cả các mã hóa, đáng chú ý nhất là các mã hóa UTF khác nhau, nhưng của chúng là ngoại lệ không phải là quy tắc).
Dan Molding

11

Đây FAQ là một sự giàu có của thông tin. Giữa trang đó và bài viết này của Joel Spolsky , bạn sẽ có một khởi đầu tốt.

Một kết luận mà tôi đã đi đến trong quá trình thực hiện:

  • wchar_tlà 16 bit trên Windows, nhưng không nhất thiết phải là 16 bit trên các nền tảng khác. Tôi nghĩ đó là một điều xấu cần thiết trên Windows, nhưng có lẽ có thể tránh được ở những nơi khác. Lý do điều quan trọng trên Windows là bạn cần nó sử dụng các tệp có ký tự không phải ASCII trong tên (cùng với phiên bản W của các chức năng).

  • Lưu ý rằng các API Windows nhận các wchar_tchuỗi sẽ được mã hóa UTF-16. Cũng lưu ý rằng điều này khác với UCS-2. Lưu ý các cặp thay thế. Đây trang thử nghiệm có kiểm tra làm sáng tỏ.

  • Nếu bạn lập trình đang trên Windows, bạn có thể không sử dụng fopen(), fread(), fwrite()vv kể từ khi họ chỉ mất char *và không hiểu mã UTF-8. Làm cho việc di chuyển trở nên đau đớn.


Lưu ý rằng stdio f*và bạn bè làm việc với char *trên mỗi nền tảng bởi vì tiêu chuẩn nói như vậy - sử dụng wcs*thay thế cho wchar_t.
cat

7

Để lập trình Unicode nghiêm ngặt:

  • Chỉ sử dụng các API chuỗi là Unicode biết ( KHÔNG strlen , strcpy... nhưng các đối tác widestring của họ wstrlen, wsstrcpy...)
  • Khi xử lý một khối văn bản, hãy sử dụng bảng mã cho phép lưu trữ các ký tự Unicode (utf-7, utf-8, utf-16, ucs-2, ...) mà không bị mất.
  • Kiểm tra xem bộ ký tự mặc định hệ điều hành của bạn có tương thích với Unicode không (ví dụ: utf-8)
  • Sử dụng phông chữ tương thích với Unicode (ví dụ: arial_unicode)

Chuỗi ký tự nhiều byte là kiểu mã hóa định ngày trước mã hóa UTF-16 (mã được sử dụng bình thường với wchar_t) và đối với tôi, có vẻ như nó chỉ dành cho Windows.

Tôi chưa bao giờ nghe nói về wint_t.


wint_t là một kiểu được định nghĩa trong <wchar.h>, giống như wchar_t. Nó có vai trò tương tự đối với các ký tự rộng mà int có đối với 'char'; nó có thể chứa bất kỳ giá trị ký tự rộng hoặc WEOF nào.
Jonathan Leffler

3

Điều quan trọng nhất là luôn phân biệt rõ ràng giữa dữ liệu văn bản và dữ liệu nhị phân . Cố gắng làm theo mô hình của Python 3.x strvsbytes hoặc SQL TEXTvs BLOB.

Thật không may, C gây nhầm lẫn vấn đề bằng cách sử dụng charcho cả "ký tự ASCII" và int_least8_t. Bạn sẽ muốn làm điều gì đó như:

typedef char UTF8; // for code units of UTF-8 strings
typedef unsigned char BYTE; // for binary data

Bạn cũng có thể muốn typedef cho các đơn vị mã UTF-16 và UTF-32, nhưng điều này phức tạp hơn vì mã hóa của wchar_tkhông được xác định. Bạn sẽ chỉ cần một bộ xử lý trước #if. Một số macro hữu ích trong C và C ++ 0x là:

  • __STDC_UTF_16__- Nếu được xác định, kiểu _Char16_ttồn tại và là UTF-16.
  • __STDC_UTF_32__- Nếu được xác định, kiểu _Char32_ttồn tại và là UTF-32.
  • __STDC_ISO_10646__- Nếu được định nghĩa, thì wchar_tUTF-32.
  • _WIN32- Trên Windows, wchar_tlà UTF-16, mặc dù điều này phá vỡ tiêu chuẩn.
  • WCHAR_MAX- Có thể được sử dụng để xác định kích thước của wchar_t, nhưng không phải là hệ điều hành sử dụng nó để đại diện cho Unicode.

Điều này có ngụ ý rằng mã của tôi không nên sử dụng các loại char ở bất kỳ đâu và cần sử dụng các hàm có thể xử lý wint_t và wchar_t?

Xem thêm:

UTF-8 là bảng mã Unicode hoàn toàn hợp lệ sử dụng char*chuỗi. Nó có lợi thế là nếu chương trình của bạn trong suốt với các byte không phải ASCII (ví dụ: một bộ chuyển đổi kết thúc dòng hoạt động trên \r\nnhưng chuyển qua các ký tự khác không thay đổi), bạn sẽ không cần thực hiện thay đổi nào!

Nếu bạn sử dụng UTF-8, bạn sẽ cần thay đổi tất cả các giả định char= ký tự (ví dụ: không gọi touppertrong vòng lặp) hoặc char= cột màn hình (ví dụ: đối với gói văn bản).

Nếu bạn sử dụng UTF-32, bạn sẽ có sự đơn giản của các ký tự có chiều rộng cố định (nhưng không phải là grapheme có chiều rộng cố định , nhưng sẽ cần phải thay đổi loại của tất cả các chuỗi của bạn).

Nếu bạn sử dụng UTF-16, bạn sẽ phải loại bỏ cả giả định về các ký tự có độ rộng cố định giả định về các đơn vị mã 8 bit, điều này khiến đây trở thành đường dẫn nâng cấp khó khăn nhất từ ​​các mã hóa byte đơn.

Tôi khuyên bạn nên chủ động tránh wchar_t vì nó không đa nền tảng: Đôi khi là UTF-32, đôi khi là UTF-16 và đôi khi là bảng mã Đông Á trước Unicode. Tôi khuyên bạn nên sử dụngtypedefs

Quan trọng hơn nữa là tránhTCHAR .


Tôi không nghĩ rằng đó là điều đáng tiếc - char là một int. Đó là một lợi ích. Sử dụng hằng số ký tự theo nghĩa đen được coi là một công dụng. Và các hàm lấy a char *có thể gặp vấn đề nếu vượt qua một const char *lần cuối cùng mà tôi nhớ (nhưng tôi mơ hồ về điều này và chức năng nào nên hãy dùng nó với một chút muối). Chỉ vì nó phức tạp hơn với các ngôn ngữ khác không có nghĩa là nó là một thiết kế tồi.
Pryftan

2

Tôi sẽ không tin tưởng bất kỳ triển khai thư viện tiêu chuẩn nào. Chỉ cần cuộn các loại unicode của riêng bạn.

#include <windows.h>

typedef unsigned char utf8_t;
typedef unsigned short utf16_t;
typedef unsigned long utf32_t;

int main ( int argc, char *argv[] )
{
  int msgBoxId;
  utf16_t lpText[] = { 0x03B1, 0x0009, 0x03B2, 0x0009, 0x03B3, 0x0009, 0x03B4, 0x0000 };
  utf16_t lpCaption[] = L"Greek Characters";
  unsigned int uType = MB_OK;
  msgBoxId = MessageBoxW( NULL, lpText, lpCaption, uType );
  return 0;
}

2

Về cơ bản, bạn muốn xử lý các chuỗi trong bộ nhớ dưới dạng wchar_tmảng thay vì char. Khi bạn thực hiện bất kỳ loại I / O nào (như đọc / ghi tệp), bạn có thể mã hóa / giải mã bằng UTF-8 (đây có lẽ là kiểu mã hóa phổ biến nhất) đủ đơn giản để thực hiện. Chỉ cần google các RFC. Vì vậy, trong bộ nhớ không có gì phải là nhiều byte. Một wchar_tđại diện cho một ký tự. Tuy nhiên, khi bạn bắt đầu tuần tự hóa, đó là lúc bạn cần mã hóa thành một thứ gì đó như UTF-8 trong đó một số ký tự được biểu diễn bằng nhiều byte.

Bạn cũng sẽ phải viết các phiên bản mới của strcmpv.v. cho các chuỗi ký tự rộng, nhưng đây không phải là vấn đề lớn. Vấn đề lớn nhất sẽ là tương tác với các thư viện / mã hiện có chỉ chấp nhận mảng char.

Và khi nói đến sizeof(wchar_t)(bạn sẽ cần 4 byte nếu bạn muốn làm đúng), bạn luôn có thể xác định lại nó thành kích thước lớn hơn với typedef/ macrohacks nếu bạn cần.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.