【C】printfに独自の表示形式を追加する

2023-08-29

C言語でプログラムを書いているとprintfで文字列を出力する中に自分で定義した構造体の内容を埋め込みたいことがたまにある。 ふつうにやるにはprintfを途中で分割して間で自前の出力ルーチンを呼び出してやることになるが、見通しが悪くなる。 しかしカスタマイズする方法があるということで使ってみた。

注意:gcc拡張で、標準ではない

register_printf_specifier (register_printf_function改め)

「customize printf」とかでググって出てくるページ Customizing Printf (The GNU C Library) を参照して、register_printf_function という関数でできるということを知る。

#include <printf.h>

register_printf_function(spec, render, arginfo);
  • spec: printf のフォーマット指定子に使う文字(%dとかのやつ)
  • render: 出力時に呼び出される関数を指定
  • arginfo: 引数情報を指定するために呼び出される関数を指定

例が載ったページがあるので参照するとよい。 登録するとprintfで標準のフォーマットに加えて独自の出力を追加できる。

ただ手元でやったところ register_printf_function はdeprecatedで register_printf_specifier を使えというエラーが出た。 違いは arginfo にサイズを指定するためのポインタが追加されている (PA_POINTERの場合は無視しても大丈夫っぽい)。

  • 最近のコンパイラでは printf のフォーマットや引数を見てエラーを出すが、 カスタマイズで追加したことは反映されないのでエラーを抑制してやる必要がある

Macの場合:register_printf_domain_function

上記はLinux/Ubuntu の場合だが、MacOSでは異なっていた。 printfの挙動を変えてしまうと影響が大きいとのことで?、「ドメイン」を用いるようになっている。 ドメインの作成/解放を new_printf_domain(), free_printf_domain() で行い、 登録には register_printf_domain_function 関数を使用する。 出力には明示的に追加のドメイン情報を渡す xprintf 関数で行う。

  • 登録関数に渡す関数の型は register_printf_specifier 用とは異なっている (register_printf_function と同じ)
  • fprintf に対応するのは fxprintf 、など

VisualC には?

ググったがカスタマイズする方法は用意されてないっぽい。

マクロで辻褄を合わせてLinuxとMacで動かす例:

#include <printf.h>
#include <stdio.h>

#pragma GCC diagnostic ignored "-Wformat"
#pragma GCC diagnostic ignored "-Wformat-extra-args"

#ifdef __APPLE__
// MacOS
printf_domain_t g_domain;
#define PRINTF(...) xprintf(g_domain, NULL, __VA_ARGS__)
#define REGISTER_PRINTF_SPECIFIER(spec, render, arginfo) \
register_printf_domain_function(g_domain, spec, render, arginfo ## _glue, NULL)
#define GLUE_PRINTF_ARGINFO_FUNC(specfn) \
static int specfn ## _glue(const struct printf_info *info, size_t n, int *argtypes) { \
int size[n]; return specfn(info, n, argtypes, size); /* size ignored */ }

#else
// Linux
#define PRINTF printf
#define REGISTER_PRINTF_SPECIFIER register_printf_specifier
#define GLUE_PRINTF_ARGINFO_FUNC(specfn) // Nothing
#endif

typedef struct {
char *name;
} Widget;

static void print_padding(FILE *stream, wchar_t pad, int n) {
for (; n > 0; --n)
fputc(pad, stream);
}

static int print_widget(FILE *stream, const struct printf_info *info, const void *const *args) {
const Widget *w = *((const Widget**)args[0]);
int padding = 0;
if (info->left) {
char buf[16];
int len = snprintf(buf, sizeof(buf), "Widget{%s}", w->name);
if (len < info->width) {
padding = info->width - len;
print_padding(stream, info->pad, padding);
}
if (len < sizeof(buf)) {
fputs(buf, stream);
return len + padding;
}
}
int len = fprintf(stream, "Widget{%s}", w->name);
if (!info->left && info->width > len) {
padding = info->width - len;
print_padding(stream, info->pad, padding);
}
return len + padding;
}

static int print_single_pointer_arginfo(const struct printf_info *info, size_t n, int *argtypes, int *psize) {
if (n > 0)
argtypes[0] = PA_POINTER;
return 1;
}
GLUE_PRINTF_ARGINFO_FUNC(print_single_pointer_arginfo)

int main(void) {
#ifdef __APPLE__
g_domain = new_printf_domain();
if (g_domain == NULL) {
fprintf(stderr, "create domain failed\n");
return 1;
}
#endif

/* Register the print function for widgets. */
REGISTER_PRINTF_SPECIFIER('W', print_widget, print_single_pointer_arginfo);

/* Make a widget to print. */
Widget mywidget = {.name = "mywidget"};
PRINTF("|%W|\n", &mywidget);
PRINTF("|%35W|\n", &mywidget);
PRINTF("|%-35W|\n", &mywidget);

#ifdef __APPLE__
free_printf_domain(g_domain);
#endif

return 0;
}
  • 環境の違いを吸収するためにマクロで置換する(REGISTER_PRINTF_SPECIFIER, PRINTF
  • 関数の引数の違いを吸収するためにマクロでグルーコードを生成する(GLUE_PRINTF_ARGINFO_FUNC
  • MacOSの場合
    • ドメインをグローバル変数 g_domain に保持して使用

メモ

  • 幅指定を使わない前提であればもっと楽できる
  • 標準の機能ではないので、依存するのは避けたほうがよさげ

リンク