Recently, I've been looking into how character encoding and locales work on Linux, and I thought it might be worthwhile to write down my findings; partly so that I can look them up again later, and partly so that people can correct all the things I've got wrong.
To begin with, let's define some terminology:
- Character set: a set of symbols which can be used together. This defines the symbols and their semantics, but not how they're encoded in memory. For example: Unicode. (Update: As noted in the comments, the character set doesn't define the appearance of symbols; this is left up to the fonts.)
- Character encoding: a mapping from a character set to an representation of the characters in memory. For example: UTF-8 is one encoding of the Unicode character set.
- Nul byte: a single byte which has a value of zero. Typically represented as the C escape sequence ‘
- NULL character: the Unicode NULL character (U+0000) in the relevant encoding. In UTF-8, this is just a single nul byte. In UTF-16, however, it's a sequence of two nul bytes.
Now, the problem: if I'm writing a (command line) C program, how do strings get from the command line to the program, and how do strings get from the program to the terminal? More concretely, what actually happens with
Let's consider the input direction first. When the
main() function of a C program is called, it's passed an array of pointers to
char arrays, i.e. strings. These strings can be arbitrary byte sequences (for example, file names), but are generally intended/assumed to be encoded in the user's environment's character encoding. This is set using the
LANG environment variables. These variables specify the user's locale which (among other things) specifies the character encoding they use.
So the program receives as input a series of strings which are in an arbitrary encoding. This means that all programs have to be able to handle all possible character encodings, right? Wrong. A standard solution to this already exists in the form of libiconv.
iconv() will convert between any two character encodings known to the system, so we can use it to convert from the user's environment encoding to, for example, UTF-8. How do we find out the user's environment encoding without parsing environment variables ourselves? We use
setlocale() parses the LC_ALL, LC_CTYPE and LANG environment variables (in that order of precedence) to determine the user's locale, and hence their character encoding. It then stores this locale, which will affect the behaviour of various C runtime functions. For example, it will change the formatting of numbers outputted by
printf() to use the locale's decimal separator. Just calling
setlocale() doesn't have any effect on character encodings, though. It won't, for example, cause
printf() to magically convert strings to the user's environment encoding. More on this later.
nl_langinfo() is one function affected by
setlocale(). When called with the
CODESET type, it will return a string identifying the character encoding set in the user's environment. This can then be passed to
iconv_open(), and we can use
iconv() to convert strings from
argv to our internal character encoding (which will typically be UTF-8).
At this point, it's worth noting that most people don't need to care about any of this. If using a library such as GLib – and more specifically, using its
GOption command line parsing functionality – all this character encoding conversion is done automatically, and the strings it returns to you are guaranteed to be UTF-8 unless otherwise specified.
So we now have our input converted to UTF-8, our program can go ahead and do whatever processing it likes on it, safe in the knowledge that the character encoding is well defined and, for example, there aren't any unexpected embedded nul bytes in the strings. (This could happen if, for example, the user's environment character encoding was UTF-16; although this is really unlikely and might not even be possible on Linux — but that's a musing for another blog post).
Having processed the input and produced some output (which we'll assume is in UTF-8, for simplicity), many programs would just
printf() the output and be done with it.
printf() knows about character encodings, right? Wrong.
printf() outputs exactly the bytes which are passed to its format parameter (ignoring all the fancy conversion specifier expansion), so this will only work if the program's internal character encoding is equal to the user's environment character encoding, for the characters being outputted. In many cases, the output of programs is just ASCII, so programs get away with just using
printf() because most character encodings are supersets of ASCII. In general, however, more work is required to do things properly.
We need to convert from UTF-8 to the user's environment encoding so that what appears in their terminal is correct. We could just use
iconv() again, but that would be boring. Instead, we should be able to use
gettext(). This means we get translation support as well, which is always good.
gettext() takes in a
msgid string and returns a translated version in the user's locale, if possible. Since these translations are done using message catalogues which may be in a completely different character encoding to the user's environment or the program's internal character encoding (UTF-8),
gettext() helpfully converts from the message catalogue encoding to the user's environment encoding (the one returned by
nl_langinfo(), discussed above). Great!
But what if no translation exists for a given string?
gettext() returns the
msgid string, unmodified and unconverted. This means that translatable string literals in our program need to magically be written in the user's environment encoding…and we're back to where we were before we introduced
I see three solutions to this:
gettext()solution: declare that all
msgidstrings should be in US-ASCII, and thus not use any Unicode characters. This works, provided we make the (reasonable) assumption that the user's environment encoding is a superset of ASCII. This requires that if a program wants to use Unicode characters in its translatable strings, it has to provide an en-US message catalogue to translate the American English
msgidstrings to American English (with Unicode). Not ideal.
gettext()++ solution: declare that all
msgidstrings should be in UTF-8, and assume that anybody who's running without message catalogues is using UTF-8 as their environment encoding (this is a big assumption). Also not ideal, but a lot less work.
gettext()to not return any strings in the user's environment encoding, but to return them all in UTF-8 instead (using
bind_textdomain_codeset()), and use UTF-8 for the
msgidstrings. The program can then pass these translated (and untranslated) strings through
iconv()as it did with the input, converting from UTF-8 to the user's environment encoding. More effort, but this should work properly.
An additional complication is that of combining translatable
printf() format strings with UTF-8 string output from the program. Since
printf() isn't encoding-aware, this requires that both the format string and the parameters are in the same encoding (or we get into a horrible mess with output strings which have substrings encoded in different ways). In this case, since our program output is in UTF-8, we definitely want to go with option 3 from above, and have
gettext() return all translated messages in UTF-8. This also means we get to use UTF-8 in
msgid strings. Unfortunately, it means that we now can't use
printf() directly, and instead have to
sprintf() to a string, use
iconv() to convert that string from UTF-8 to the user's environment encoding, and then
printf() it. Whew.
Here's a diagram which hopefully makes some of the journey clearer (click for a bigger version):
So what does this mean for you? As noted above, in most cases it will mean nothing. Libraries such as GLib should take care of all of this for you, and the world will be a lovely place with ponies (U+1F3A0) and cats (U+1F431) everywhere. Still, I wanted to get this clear in my head, and hopefully it's useful to people who can't make use of libraries like GLib (for whatever reason).
Exploring exactly what GLib does is a matter for another time. Similarly, exploring how Windows does things is also best left to a later post (hint: Windows does things completely differently to Linux and other Unices, and I'm not sure it's for the better).