ICU を使ってみた

色々とありまして、間が空いてしまいました。
最近ついに C 言語に手を染めてしまい、Hello World -> Epeg/Imlib2 を弄ってみる -> 標準入出力を弄ってみる -> ICU を弄ってみる -> コマンドライン引数を解釈してみる -> メモリ管理やバッファオーバーフローについてちょっと勉強という出鱈目な順番でどうにか書けるようになりました。
もともと慣れている PHP の文法が C に似せてあり、言語そのものに慣れるのに苦労はしませんでしたが、メモリ管理が厄介ですね。スクリプト育ちの身には面倒に思えて仕方ないです。
最終的には PHPPython 用のバインディングが無いライブラリのバインディングを自前で作るのが目標です。あと Obj-C をやる前に必要と思ったというのもありますが。
習作として ICU を使った、ファイルまたは標準入力から読み込んだテキストを正規化して標準出力に書き出すプログラムを作ったので晒してみます。例によって全部コピペ。
ICU 3.4 で動作確認してますが、unicode/ucnv.h,unorm.h を見る限り ICU 2.0 以降ならコンパイルできそうです。

/*
 * A Simple Unicode Normalizer
 *
 * BUILD:
 *   CPPFLAGS=`icu-config --cppflags`
 *   LDFLAGS=`icu-config --ldflags`
 *   gcc -O2 -Wall $CPPFLAGS $LDFLAGS normalize.c -o unorm
 *
 * USAGE:
 *   unorm [-nfc|-nfd|-nfkc|-nfkd] [-ic charset] [-oc charset] [files ...]
 *
 * The default mode is NFC.
 * The default input charset is UTF-8.
 * The default output charset is same to the input.
 * If files not specified, read from STDIN.
 *
 * Write the results to STDOUT.
 *
 * Return 0 on success, 1 on invalid argument, 2 on I/O error,
 * 3 on Unicode error, -1 (255) if failed to allocate memory.
 */

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unicode/ucnv.h>
#include <unicode/unorm.h>

/* error code macros */
#define MYNORM_ERR_MEM -1
#define MYNORM_ERR_OK  0
#define MYNORM_ERR_ARG 1
#define MYNORM_ERR_IO  2
#define MYNORM_ERR_ICU 3

/* macro for check argument */
#define MYNORM_ARG_IS_OPT(arg) (arg[0] == '-')

/* global variables */
const char *myself;

/* function prototypes */
int main(int argc, char **argv);
static void normalize(UNormalizationMode mode, char *ic, char *oc, char *file);
static void parseargs(int argc, char **argv, UNormalizationMode *mode, char **ic, char **oc);
static void usage(int e);

/* main routine */
int main(int argc, char **argv)
{
  /* variables for command line arguments */
  UNormalizationMode mode = UNORM_DEFAULT;
  char *ic = "UTF-8";
  char *oc = NULL;

  /* other local variables */
  int i, fn = 0;

  /* get option */
  myself = argv[0];
  parseargs(argc, argv, &mode, &ic, &oc);
  if (oc == NULL) {
    oc = ic;
  }

  /* normalize given files */
  for (i = 1; i < argc; i++) {
    if (!strcmp(argv[i], "-ic") || !strcmp(argv[i], "-oc")) {
      i++;
    } else if (!MYNORM_ARG_IS_OPT(argv[i])) {
      normalize(mode, ic, oc, argv[i]);
      fn++;
    }
  }

  /* if files not given, normalize data which read from stdin */
  if (fn == 0) {
    normalize(mode, ic, oc, NULL);
  }

  /* success */
  return 0;
}

/* normalize */
static void normalize(UNormalizationMode mode, char *ic, char *oc, char *file)
{
  /* local constants */
  static const size_t buf_unit = sizeof(char) * 8192;
  static const int32_t nrm_opts = 0;
  static const char *err_malloc = "failed to allocate memory";
  static const char *err_overflow = "integer overflow occurred";

  /* local variables */
  size_t buf_len, src_len, dst_len;
  int32_t max_len, uni_len, nrm_len;
  char *src, *tmp, *dst;
  UChar *uni, *nrm;
  UConverter *cnv;
  UErrorCode err;
  UNormalizationCheckResult res;
  int c;
  size_t i;
  FILE *fp;

#ifdef MYNORM_DEBUG
  /* for debug */
  const char *debug_filename = (file != NULL) ? file : "<stdin>";
  const char *check_result;
#endif

  /* open input file */
  if (file == NULL) {
    fp = stdin;
  } else {
    fp = fopen(file, "r");
    if (fp == NULL) {
      fprintf(stderr, "%s: %s: %s\n", myself, file, strerror(errno));
      exit(MYNORM_ERR_IO);
    }
  }

  /* read data */
  buf_len = buf_unit;
  src = (char *) malloc(buf_len);
  src_len = 0;
  if (src == NULL) {
    if (file != NULL) {
      fclose(fp);
    }
    fprintf(stderr, "ERROR: %s.\n", err_malloc);
    exit(MYNORM_ERR_MEM);
  }
  while (!feof(fp)) {
    c = fgetc(fp);
    if (c == EOF) {
      break;
    }
    src[src_len++] = c;
    if (src_len <= 0) {
      free(src);
      if (file != NULL) {
        fclose(fp);
      }
      fprintf(stderr, "ERROR: %s.\n", err_overflow);
      exit(MYNORM_ERR_MEM);
    } else if (src_len >= buf_len) {
      buf_len += buf_unit;
      if (buf_len <= 0 || (tmp = (char *) realloc(src, buf_len)) == NULL) {
        free(src);
        if (file != NULL) {
          fclose(fp);
        }
        fprintf(stderr, "ERROR: %s.\n", ((buf_len <= 0) ? err_overflow : err_malloc));
        exit(MYNORM_ERR_MEM);
      }
      src = tmp;
    }
  }

  /* close file handlef if input is not stdin */
  if (file != NULL) {
    fclose(fp);
  }

#ifdef MYNORM_DEBUG
  /* debug output - length of input data */
  fprintf(stderr, "%s:src_len:%lu (%p)\n", debug_filename, src_len, src);
#endif

  /* create the converter which converts between input charset and unicode */
  err = U_ZERO_ERROR;
  cnv = ucnv_open(ic, &err);
  if (U_FAILURE(err)) {
    free(src);
    fprintf(stderr, "ERROR: failed to create an Unicode converter for %s (%s).\n", ic, u_errorName(err));
    exit(MYNORM_ERR_ICU);
  }

  /* allocate memory for unicode data */
  max_len = (src_len / (int32_t) ucnv_getMinCharSize(cnv)) + 10;
  buf_len = sizeof(UChar) * max_len;
  if (buf_len <= 0 || (uni = (UChar *) malloc(buf_len)) == NULL) {
    ucnv_close(cnv);
    free(src);
    fprintf(stderr, "ERROR: %s.\n", ((buf_len <= 0) ? err_overflow : err_malloc));
    exit(MYNORM_ERR_MEM);
  }

  /* convert from input charset to unicode and delete the converter */
  err = U_ZERO_ERROR;
  uni_len = ucnv_toUChars(cnv, uni, max_len, src, src_len, &err);
  ucnv_close(cnv);
  free(src);
  if (U_FAILURE(err)) {
    free(uni);
    fprintf(stderr, "ERROR: failed to convert from %s to Unicode (%s).\n", ic, u_errorName(err));
    exit(MYNORM_ERR_ICU);
  }

#ifdef MYNORM_DEBUG
  /* debug output - length of unicode data */
  fprintf(stderr, "%s:uni_len:%d (%p)\n", debug_filename, uni_len, uni);
#endif

  /* check for normalize */
  err = U_ZERO_ERROR;
  res = unorm_quickCheck(uni, uni_len, mode, &err);
  if (U_FAILURE(err)) {
    free(uni);
    fprintf(stderr, "ERROR: failed to check for normalize (%s).\n", u_errorName(err));
    exit(MYNORM_ERR_ICU);
  }

#ifdef MYNORM_DEBUG
  /* debug output - whether need to normalize or not */
  switch (res) {
    case UNORM_YES:
      check_result = "already normalized";
    break;
    case UNORM_NO:
      check_result = "not normalized";
    break;
    case UNORM_MAYBE:
    default:
      check_result = "maybe normalized";
  }
  fprintf(stderr, "%s:%s\n", debug_filename, check_result);
#endif

  if (res == UNORM_YES) {
    nrm = uni;
    nrm_len = uni_len;
  } else {
    /* get needed buffer size */
    err = U_ZERO_ERROR;
    nrm_len = unorm_normalize(uni, uni_len, mode, nrm_opts, NULL, 0, &err);
    if (U_FAILURE(err) && err != U_BUFFER_OVERFLOW_ERROR) {
      free(uni);
      fprintf(stderr, "ERROR: failed to get needed buffer size (%s).\n", u_errorName(err));
      exit(MYNORM_ERR_ICU);
    }

    /* allocate memory for normalized data */
    buf_len = sizeof(UChar) * nrm_len + 1;
    if (buf_len <= 0 || (nrm = (UChar *) malloc(buf_len)) == NULL) {
      free(uni);
      fprintf(stderr, "ERROR: %s.\n", ((buf_len <= 0) ? err_overflow : err_malloc));
      exit(MYNORM_ERR_MEM);
    }

    /* normalize */
    err = U_ZERO_ERROR;
    unorm_normalize(uni, uni_len, mode, nrm_opts, nrm, nrm_len, &err);
    free(uni);
    if (U_FAILURE(err)) {
      free(nrm);
      fprintf(stderr, "ERROR: failed to normalize (%s).\n", u_errorName(err));
      exit(MYNORM_ERR_ICU);
    }
    nrm[nrm_len] = '\0';
  }

#ifdef MYNORM_DEBUG
  /* debug output - length of normalized data */
  fprintf(stderr, "%s:nrm_len:%d (%p)\n", debug_filename, nrm_len, nrm);
#endif

  /* create the converter which converts between output charset and unicode */
  err = U_ZERO_ERROR;
  cnv = ucnv_open(oc, &err);
  if (U_FAILURE(err)) {
    free(nrm);
    fprintf(stderr, "ERROR: failed to create an Unicode converter for %s (%s).\n", oc, u_errorName(err));
    exit(MYNORM_ERR_ICU);
  }

  /* allocate memory for output data */
  max_len = (nrm_len + 10) * (int32_t) ucnv_getMaxCharSize(cnv);
  buf_len = sizeof(char) * max_len;
  if (buf_len <= 0 || (dst = (char *) malloc(buf_len)) == NULL) {
    ucnv_close(cnv);
    free(nrm);
    fprintf(stderr, "ERROR: %s.\n", ((buf_len <= 0) ? err_overflow : err_malloc));
    exit(MYNORM_ERR_MEM);
  }

  /* convert from unicode to output charset and delete the converter */
  err = U_ZERO_ERROR;
  dst_len = ucnv_fromUChars(cnv, dst, max_len, nrm, nrm_len, &err);
  ucnv_close(cnv);
  free(nrm);
  if (U_FAILURE(err)) {
    free(dst);
    fprintf(stderr, "ERROR: failed to convert from Unicode to %s (%s).\n", oc, u_errorName(err));
    exit(MYNORM_ERR_ICU);
  }

#ifdef MYNORM_DEBUG
  /* debug output - length of output data */
  fprintf(stderr, "%s:dst_len:%lu (%p)\n", debug_filename, dst_len, dst);
#endif

  /* write to stdout */
  for (i = 0; i < dst_len; i++) {
    fputc(dst[i], stdout);
  }
  fflush(stdout);
  free(dst);
}

/* parse commandline arguments */
static void parseargs(int argc, char **argv, UNormalizationMode *mode, char **ic, char **oc)
{
  static int mode_isset = 0;
  static int ic_isset = 0;
  static int oc_isset = 0;
  int i;

  for (i = 1; i < argc; i++) {
    if (MYNORM_ARG_IS_OPT(argv[i])) {
      /* parse option */
      if (!strcmp(argv[i], "-?") || !strcmp(argv[i], "-h") || !strcmp(argv[i], "-help")) {
        /* print usage without error*/
        usage((argc == 2) ? MYNORM_ERR_OK : MYNORM_ERR_ARG);
      } else if (!strcmp(argv[i], "-nfc")) {
        if (mode_isset) {
          usage(MYNORM_ERR_ARG);
        }
        mode_isset = 1;
        *mode = UNORM_NFC;
      } else if (!strcmp(argv[i], "-nfd")) {
        if (mode_isset) {
          usage(MYNORM_ERR_ARG);
        }
        mode_isset = 1;
        *mode = UNORM_NFD;
      } else if (!strcmp(argv[i], "-nfkc")) {
        if (mode_isset) {
          usage(MYNORM_ERR_ARG);
        }
        mode_isset = 1;
        *mode = UNORM_NFKC;
      } else if (!strcmp(argv[i], "-nfkd")) {
        if (mode_isset) {
          usage(MYNORM_ERR_ARG);
        }
        mode_isset = 1;
        *mode = UNORM_NFKD;
      } else if (!strcmp(argv[i], "-ic")) {
        /* get input charset */
        if (ic_isset || ++i >= argc || MYNORM_ARG_IS_OPT(argv[i])) {
          usage(MYNORM_ERR_ARG);
        }
        ic_isset = 1;
        *ic = argv[i];
      } else if (!strcmp(argv[i], "-oc")) {
        /* get output charset */
        if (oc_isset || ++i >= argc || MYNORM_ARG_IS_OPT(argv[i])) {
          usage(MYNORM_ERR_ARG);
        }
        oc_isset = 1;
        *oc = argv[i];
      } else {
        /* invalid option */
        usage(MYNORM_ERR_ARG);
      }
    }
  }
}

/* print usage */
static void usage(int e)
{
  fprintf(stderr, "usage: %s [-nfc|-nfd|-nfkc|-nfkd] [-ic charset] [-oc charset] [files ...]\n", myself);
  exit(e);
}

ところで前回ちょっと書いてた pecl_http のアスタリスクはポインタというやつだったのですね。ソース見ててなんとなく「ここ * が足りないんじゃね?」と勘でやってみたのですが、正解で、最新版 (1.0.0RC2) でも同様の修正が入ってました。