pack のテンプレート文字列から、それを使ってパックした結果のサイズを求めたいということはありませんか。つまりやりたいことは sizeof(struct x) です。

pack テンプレート文字列は割と複雑で、任意長や文字列やポインタなどがあり、全てにおいてうまく動作するものを作るのは無理ですが、だいたいうまくいくのは実装できそうです。

sizeof(template)

def sizeof(template)
	x = Class.new(Numeric) do
		def to_str; ""; end
		def to_int; 0; end
		# implicit to_f is called only with Numeric subclass
		def to_f; 0.0; end
	end.new
	size = template.scan(/([a-zA-Z][_!]?[<>]?)([0-9]*)/).reduce(0) {|r,(_,n)|
		r + (n.empty?? 1 : n.to_i)
	}
	# p [template, size]
	Array.new(size) { x }.pack(template).size
end

やってることは「とりあえず pack してみる」ですが、pack するためには、まずテンプレート文字列に応じて、適当な型と適当な長さの配列が必要になります。

適当な長さという点では多い分には問題ないので、適当に数えています。

適当な型というのは、厳密にやると結局テンプレート文字列をパースするのと同じぐらい面倒なので、pack が要求するデータ型すべてに暗黙的に変換可能なオブジェクトを作っています。

pack が要求する型は整数・文字列・浮動小数点数があります。整数は to_int、文字列は to_str を実装すると、Ruby はそのオブジェクトについて整数・文字列と同等に扱う(必要なら暗黙的に変換される) ことになっています。

浮動小数点数にはそういったどんなオブジェクトも浮動小数点数として扱えるようにする、というメソッド名がないのですが、Numeric のサブクラスであって to_f が実装されている場合には暗黙的に変換されるというルールがあるので、ベースクラスを Numeric にしています。

[
	"C",
	"S",
	"L",
	"Q",

	"c",
	"s",
	"l",
	"q",

	"S_", "S!",
	"I", "I_", "I!",
	"L_", "L!",
	"Q_", "Q!",

	"s_", "s!",
	"i", "i_", "i!",
	"l_", "l!",
	"q_", "q!",

	"S>", "L>", "Q>",
	"s>", "l>", "q>",
	"S!>", "I!>",
	"L!>", "Q!>",
	"s!>", "i!>",
	"l!>", "q!>",

	"S<", "L<", "Q<",
	"s<", "l<", "q<",
	"S!<", "I!<",
	"L!<", "Q!<",
	"s!<", "i!<",
	"l!<", "q!<",

	"n",
	"N",
	"v",
	"V",

	"U",
	"w",

	"D", "d",
	"F", "f",
	"E",
	"e",
	"g",
	"G",

	"A",
	"a",
	"Z",
	"B",
	"b",
	"H",
	"h",
	"u",
	"M",
	"m",

#	"P",
#	"p",
#	"@",
#	"X",
#	"x",

	"C255",
	"i!2s!2",
	"i!i!s!s!",
].each do |tmpl|
	next if tmpl =~ /q/i
	puts "sizeof(%p) = %d" % [tmpl, sizeof(tmpl)]
end
sizeof("C") = 1
sizeof("S") = 2
sizeof("L") = 4
sizeof("c") = 1
sizeof("s") = 2
sizeof("l") = 4
sizeof("S_") = 2
sizeof("S!") = 2
sizeof("I") = 4
sizeof("I_") = 4
sizeof("I!") = 4
sizeof("L_") = 8
sizeof("L!") = 8
sizeof("s_") = 2
sizeof("s!") = 2
sizeof("i") = 4
sizeof("i_") = 4
sizeof("i!") = 4
sizeof("l_") = 8
sizeof("l!") = 8
sizeof("S>") = 2
sizeof("L>") = 4
sizeof("s>") = 2
sizeof("l>") = 4
sizeof("S!>") = 2
sizeof("I!>") = 4
sizeof("L!>") = 8
sizeof("s!>") = 2
sizeof("i!>") = 4
sizeof("l!>") = 8
sizeof("S<") = 2
sizeof("L<") = 4
sizeof("s<") = 2
sizeof("l<") = 4
sizeof("S!<") = 2
sizeof("I!<") = 4
sizeof("L!<") = 8
sizeof("s!<") = 2
sizeof("i!<") = 4
sizeof("l!<") = 8
sizeof("n") = 2
sizeof("N") = 4
sizeof("v") = 2
sizeof("V") = 4
sizeof("U") = 1
sizeof("w") = 1
sizeof("D") = 8
sizeof("d") = 8
sizeof("F") = 4
sizeof("f") = 4
sizeof("E") = 8
sizeof("e") = 4
sizeof("g") = 4
sizeof("G") = 8
sizeof("A") = 1
sizeof("a") = 1
sizeof("Z") = 1
sizeof("B") = 1
sizeof("b") = 1
sizeof("H") = 1
sizeof("h") = 1
sizeof("u") = 0
sizeof("M") = 47
sizeof("m") = 0
sizeof("C255") = 255
sizeof("i!2s!2") = 12
sizeof("i!i!s!s!") = 12
  1. トップ
  2. tech
  3. Ruby の pack テンプレート文字列からそのデータサイズを求める
  1. トップ
  2. ruby
  3. Ruby の pack テンプレート文字列からそのデータサイズを求める

Cの構造体とかだと、構造体の中に他の構造体ということは普通にあります。

こういった構造体の文字列を unpack すると、全部フラットな配列になってしまうので、Ruby レベルのオブジェクト構造として再構成しようと思うと面倒なことになります。

ということで、入れ子に対応した unpack というのを実装してみました。

概要

以下のような感じで {} (ブレース) で入れ子を表現するようにテンプレート文字列を拡張します。

struct_foo_t = %{
	I!
	I!
	Z16
}

struct_bar_t = %{
	I!
	I!
	{
		#{struct_foo_t}
	}
	I!
} #=> "I!I!{I!I!Z16}I!"

original = [
	0xffff,
	0xfeff,
	[
		0x11,
		0x22,
		"foobar"
	],
	0x10,
]

packed = original.pack_deeply(struct_bar_t)
p packed
#=> "\xFF\xFF\x00\x00\xFF\xFE\x00\x00\x11\x00\x00\x00\"\x00\x00\x00foobar\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00"

unpacked = packed.unpack_deeply(struct_bar_t)
p unpacked
#=> [65535, 65279, [17, 34, "foobar"], 16]

p original == unpacked
#=> true

実装

先頭から unpack して入れ子を処理していくというイメージです。処理済みのバイト長が欲しいので、クソみたいですが一旦 unpack したものをもう一度 pack しています。

pack_deeply は単にフラットにして pack するだけなので簡単です。

class String
	def unpack_deeply(template)
		ret = []
		tree = [ ret ]
		n = 0
		tmpl = ""

		# unpack partial and add to result
		unpack = lambda {
			unpacked = self.slice(n..-1).unpack(tmpl)
			# re-pack to get byte length
			size = unpacked.pack(tmpl).size
			n += size
			tmpl = ""
			tree.last.concat(unpacked)
		}

		template.each_char do |chr|
			case chr
			when "{"
				unpack.call
				ary = []
				tree.last << ary
				tree << ary
			when "}"
				unpack.call
				tree.pop
			else
				tmpl << chr
			end
		end
		unpack.call
		ret
	end
end

class Array
	def pack_deeply(template)
		flatten.pack(template.gsub(/[{}]/, ''))
	end
end
  1. トップ
  2. tech
  3. Ruby でネストした構造体文字列をネストした配列として unpack する
  1. トップ
  2. ruby
  3. Ruby でネストした構造体文字列をネストした配列として unpack する

markdown で HTML にして wkhtmltopdf で PDF 化する汎用 Makefile です。

Markdown で書いてプレビューするのはいいんですが、画像がちらばってしまって面倒です。PDF にしておけば1ファイルにまとまるのと、だいたいどこでも見ることができるので便利です。

# brew install markdown
# http://wkhtmltopdf.org/downloads.html

.SUFFIXES: .md .html
.SUFFIXES: .html .pdf

SRCS = ${wildcard *.md}
HTMLS = $(SRCS:.md=.html)
PDFS = $(SRCS:.md=.pdf)

define header
	<!DOCTYPE html>
	<meta charset="utf-8">
	<title>MD</title>
	<style>
	body { font-family: sans-serif; line-height: 1.66 }
	img { max-width: 800px }
	</style>
endef
export header

.PHONY: all
all: $(PDFS)

$(HTMLS): Makefile

.html.pdf:
	wkhtmltopdf $< $@

.md.html:
	(echo "$$header"; markdown $<) > $@
  1. トップ
  2. tech
  3. Markdown → PDF にする Makefile