株式会社クリアコード > ククログ

ククログ

タグ:

Apache Arrowの(非公式)パッケージを公開

Apache ArrowのC++実装およびC++実装のCバインディング(Cで書かれたからC++実装を使うためのラッパー)のパッケージを公開しました。これにより簡単にApache Arrowを使えるようになります。C/C++だけでなく、Rubyからも簡単に使えるようになります。

なお、パッケージはApache Arrowプロジェクトが公式に提供しているものではなく、Apache Arrowプロジェクトの開発に参加している人が提供している非公式なものです。非公式なものですが、Apache Arrowについてもパッケージについてもよく知っている人が作っているものなので安心して利用できます。

対応プラットフォーム

以下のプラットフォーム用のパッケージを公開しました。

  • Debian GNU/Linux jessie
  • Ubuntu 16.04 LTS
  • Ubuntu 16.10
  • CentOS 7

それぞれのプラットフォームごとに利用方法を説明します。それぞれのセクションだけ(自分が使っているプラットフォームのセクションだけ)を読めば分かるように書いています。そのためそれぞれのセクションで重複した説明があります。

Debian GNU/Linux jessieとUbuntu 16.04 LTSとUbuntu 16.10

Debian GNU/Linux jessieとUbuntu 16.04 LTSとUbuntu 16.10でApache Arrowのパッケージを使う方法を説明します。最初のAPTリポジトリーを追加するところだけがDebian GNU/LinuxとUbuntuで違うだけでそれ以降は同じです。

まずApache Arrowのパッケージを提供しているAPTリポジトリー用のapt-lineを追加します。GroongaのAPTリポジトリーに相乗りしているので以下のapt-lineになります。

Debian GNU/Linux jessieの場合は、まず以下の内容の/etc/apt/sources.list.d/groonga.listを作成してapt-lineを追加します。

deb http://packages.groonga.org/debian/ jessie main
deb-src http://packages.groonga.org/debian/ jessie main

次にこのapt-lineで追加したAPTリポジトリーを使えるようにします。

% sudo apt update
% sudo apt install -y --allow-unauthenticated groonga-keyring
% sudo apt update

Ubuntuの場合は次のようにします。

% sudo apt install -y software-properties-common
% sudo add-apt-repository -y ppa:groonga/ppa
% sudo apt update

これでパッケージをインストールする準備ができました。以下はDebian GNU/Linux jessie、Ubuntu 16.04 LTS、Ubuntu 16.10で共通です。

Apache ArrowのC++実装を使う場合はlibarrow-devパッケージをインストールします。

% sudo apt install -y libarrow-dev

Cバインディングを使う場合はlibarrow-glib-devパッケージをインストールします。Cバインディングを使うと、Ruby、Lua、Go、Rustなどからも使えるようになります。Ruby、Lua、Go、Rustなどから使いたい場合はlibarrow-glib-devパッケージをインストールしてください。

% sudo apt install -y libarrow-glib-dev

RubyでApache Arrowを使う場合は、まずはRubyをインストールします。拡張ライブラリーを使うのでruby-devパッケージもインストールします。

% sudo apt install -y ruby ruby-dev

red-arrow gemをインストールするとRubyからApache Arrowを使えるようになります。

% sudo gem install red-arrow

以下はRubyの配列とApache Arrowの配列を変換する例です。

require "arrow"

counts = Arrow::UInt32Array.new([1, 2, 4, 8])
p counts.to_a # => [1, 2, 4, 8]

以下は↑と同じ内容の配列を2回にわけて/tmp/logs-stream.arrowに書き出す例です。Apache Arrowは効率のよいプロセス間のデータ交換の実現も大事にしているので、データの書き出しはよくある使い方です。

#!/usr/bin/env ruby

require "arrow"

fields = [
  Arrow::Field.new("count", Arrow::UInt32DataType.new),
]
schema = Arrow::Schema.new(fields)

Arrow::IO::FileOutputStream.open("/tmp/logs-stream.arrow", false) do |output|
  Arrow::IPC::StreamWriter.open(output, schema) do |writer|
    counts1 = [1, 2]
    arrow_counts1 = Arrow::UInt32Array.new(counts1)
    record_batch = Arrow::RecordBatch.new(schema, counts1.size, [arrow_counts1])
    writer.write_record_batch(record_batch)

    counts2 = [4, 8]
    arrow_counts2 = Arrow::UInt32Array.new(counts2)
    record_batch = Arrow::RecordBatch.new(schema, counts2.size, [arrow_counts2])
    writer.write_record_batch(record_batch)
  end
end

それでは、このデータをLuaから読み出してみましょう。LuaからApache Arrowを使うにはLGIを使います。

% sudo apt install -y luarocks
% sudo luarocks install lgi

以下のコードでRubyで書き出したデータを読み込めます。MemoryMappedFileというオブジェクトを使って読み込んでいるのでゼロコピー(コピーなし)でデータを読み込んでいるので効率的です。

local lgi = require 'lgi'
local Arrow = lgi.Arrow
local ArrowIO = lgi.ArrowIO
local ArrowIPC = lgi.ArrowIPC

local input = ArrowIO.MemoryMappedFile.open("/tmp/logs-stream.arrow", ArrowIO.FileMode.READ)
local reader = ArrowIPC.StreamReader.open(input)

local i = 0
while true do
  local record_batch = reader:get_next_record_batch()
   if not record_batch then
    break
  end

  local column = record_batch:get_column(0)
  for j = 0, record_batch:get_n_rows() - 1 do
    print("record_batch["..i.."]["..j.."] = " .. column:get_value(j))
    -- record_batch[0][0] = 1
    -- record_batch[0][1] = 2
    -- record_batch[1][0] = 4
    -- record_batch[1][1] = 8
  end

  i = i + 1
end

input:close()

実行すると次のように表示されます。きちんと読み込めていますね。

record_batch[0][0] = 1
record_batch[0][1] = 2
record_batch[1][0] = 4
record_batch[1][1] = 8

次はPythonのPandasで作ったデータを書き出してRubyから読み込んでみましょう。

PythonからApache Arrowを使うにはpyarrowをインストールします。

% wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
% bash Miniconda3-latest-Linux-x86_64.sh
% source ~/.bashrc
% conda install -y --channel conda-forge pyarrow

Pandasで作ったデータをApache ArrowのRecordBatchに変換してファイルに書き出します。

#!/usr/bin/env python

import pandas as pd
import numpy as np
import pyarrow as arrow

df = pd.DataFrame({'count': [1, 2, 4, 8]})
batch = arrow.RecordBatch.from_pandas(df)
sink = arrow.io.OSFile("/tmp/logs.arrow", "w")
writer = arrow.ipc.FileWriter(sink, batch.schema)
writer.write_batch(batch)
writer.close()
sink.close()

このデータを読み込むRubyのコードは次の通りです。

#!/usr/bin/env ruby

require "arrow"

Arrow::IO::MemoryMappedFile.open("/tmp/logs.arrow", :read) do |input|
  Arrow::IPC::FileReader.open(input) do |reader|
    reader.each do |record_batch|
      record_batch.each do |record|
        p record[:count]
        # 1
        # 2
        # 4
        # 8
      end
    end
  end
end

次のように出力されるので正しく読み込めています。

1
2
4
8
CentOS 7

CentOS 7でApache Arrowのパッケージを使う方法を説明します。

まずApache Arrowのパッケージを提供しているYumリポジトリーを追加します。GroongaのYumリポジトリーに相乗りしているので以下のように追加します。

% sudo yum install -y http://packages.groonga.org/centos/groonga-release-1.2.0-1.noarch.rpm

これでパッケージをインストールする準備ができました。

Apache ArrowのC++実装を使う場合はarrow-develパッケージをインストールします。

% sudo yum install -y arrow-devel

Cバインディングを使う場合はarrow-glib-develパッケージをインストールします。Cバインディングを使うと、Ruby、Lua、Go、Rustなどからも使えるようになります。Ruby、Lua、Go、Rustなどから使いたい場合はarrow-glib-develパッケージをインストールしてください。

% sudo yum install -y arrow-glib-devel

RubyでApache Arrowを使う場合は、まずはRubyをインストールします。

% sudo yum install -y git gcc openssl-devel readline-devel zlib-devel
% git clone https://github.com/rbenv/rbenv.git ~/.rbenv
% echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
% echo 'eval "$(~/.rbenv/bin/rbenv init)"' >> ~/.bash_profile
% source ~/.bash_profile
% git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
% rbenv install 2.4.1

red-arrow gemをインストールするとRubyからApache Arrowを使えるようになります。

% gem install red-arrow

以下はRubyの配列とApache Arrowの配列を変換する例です。

require "arrow"

counts = Arrow::UInt32Array.new([1, 2, 4, 8])
p counts.to_a # => [1, 2, 4, 8]

以下は↑と同じ内容の配列を2回にわけて/tmp/logs-stream.arrowに書き出す例です。Apache Arrowは効率のよいプロセス間のデータ交換の実現も大事にしているので、データの書き出しはよくある使い方です。

#!/usr/bin/env ruby

require "arrow"

fields = [
  Arrow::Field.new("count", Arrow::UInt32DataType.new),
]
schema = Arrow::Schema.new(fields)

Arrow::IO::FileOutputStream.open("/tmp/logs-stream.arrow", false) do |output|
  Arrow::IPC::StreamWriter.open(output, schema) do |writer|
    counts1 = [1, 2]
    arrow_counts1 = Arrow::UInt32Array.new(counts1)
    record_batch = Arrow::RecordBatch.new(schema, counts1.size, [arrow_counts1])
    writer.write_record_batch(record_batch)

    counts2 = [4, 8]
    arrow_counts2 = Arrow::UInt32Array.new(counts2)
    record_batch = Arrow::RecordBatch.new(schema, counts2.size, [arrow_counts2])
    writer.write_record_batch(record_batch)
  end
end

それでは、このデータをLuaから読み出してみましょう。LuaからApache Arrowを使うにはLGIを使います。

% sudo yum install -y lua-devel luarocks gobject-introspection-devel
% sudo luarocks install lgi

以下のコードでRubyで書き出したデータを読み込めます。MemoryMappedFileというオブジェクトを使って読み込んでいるのでゼロコピー(コピーなし)でデータを読み込んでいるので効率的です。

local lgi = require 'lgi'
local Arrow = lgi.Arrow
local ArrowIO = lgi.ArrowIO
local ArrowIPC = lgi.ArrowIPC

local input = ArrowIO.MemoryMappedFile.open("/tmp/logs-stream.arrow", ArrowIO.FileMode.READ)
local reader = ArrowIPC.StreamReader.open(input)

local i = 0
while true do
  local record_batch = reader:get_next_record_batch()
   if not record_batch then
    break
  end

  local column = record_batch:get_column(0)
  for j = 0, record_batch:get_n_rows() - 1 do
    print("record_batch["..i.."]["..j.."] = " .. column:get_value(j))
    -- record_batch[0][0] = 1
    -- record_batch[0][1] = 2
    -- record_batch[1][0] = 4
    -- record_batch[1][1] = 8
  end

  i = i + 1
end

input:close()

実行すると次のように表示されます。きちんと読み込めていますね。

record_batch[0][0] = 1
record_batch[0][1] = 2
record_batch[1][0] = 4
record_batch[1][1] = 8

次はPythonのPandasで作ったデータを書き出してRubyから読み込んでみましょう。

PythonからApache Arrowを使うにはpyarrowをインストールします。

% wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
% bash Miniconda3-latest-Linux-x86_64.sh
% source ~/.bashrc
% conda install -y --channel conda-forge pyarrow

Pandasで作ったデータをApache ArrowのRecordBatchに変換してファイルに書き出します。

#!/usr/bin/env python

import pandas as pd
import numpy as np
import pyarrow as arrow

df = pd.DataFrame({'count': [1, 2, 4, 8]})
batch = arrow.RecordBatch.from_pandas(df)
sink = arrow.io.OSFile("/tmp/logs.arrow", "w")
writer = arrow.ipc.FileWriter(sink, batch.schema)
writer.write_batch(batch)
writer.close()
sink.close()

このデータを読み込むRubyのコードは次の通りです。

#!/usr/bin/env ruby

require "arrow"

Arrow::IO::MemoryMappedFile.open("/tmp/logs.arrow", :read) do |input|
  Arrow::IPC::FileReader.open(input) do |reader|
    reader.each do |record_batch|
      record_batch.each do |record|
        p record[:count]
        # 1
        # 2
        # 4
        # 8
      end
    end
  end
end

次のように出力されるので正しく読み込めています。

1
2
4
8

まとめ

Apache ArrowをC/C++/Ruby/Lua/Go/Rustなどから使いやすくするためにApache ArrowのC++実装とそのCバインディングのパッケージを公開しました。

Apache Arrowを使っていろんな言語をデータ分析に活用してください。

2017-03-23

Apache Arrow C++の使い方:配列の作り方

Apache Arrowというデータ分析に特化したインメモリーのデータ操作ライブラリーがあります。

Apache Arrowは多くのシステムで使われることを目指しています。そうなることで各システム間でデータ交換しやすくなるからです。現時点では、Java、C++、JavaScriptは同じ仕様をそれぞれの言語で実装しています。Python、RubyにはC++のバインディングがあり、C++での実装をPython、Rubyで使えます。たとえば、Rubyでの使い方は名古屋Ruby会議03:Apache ArrowのRubyバインディングをGObject Introspectionでを参照してください。

このようにApache Arrowにはいくつかの実装がありますが、ここでは何回かにわけてC++実装の使い方を紹介します。

配列

Apache Arrowはカラム指向のデータ構造になっています。カラム指向の方が高速にデータ分析処理できるからです。

カラム指向のデータ構造というのは、1つのレコード(レコードには複数のカラムがある)をレコード単位で持つのではなく、カラム単位で持つデータ構造です。Apache Arrowのサイトの「Performance Advantage of Columnar In-Memory」に図があるのでピンとこない人はその図を参照してください。

カラム指向のデータ構造にするために、Apache Arrowでは配列が主なデータ構造になります。32bit整数のカラムも配列で表現しますし、文字列のカラムも辞書のカラム(Pythonで言えばdict、Rubyで言えばHashで、Apache Arrowは辞書のような複合型もサポートしている)も配列で表現します。

32bit整数のように固定長データのカラムを配列で表現するのはイメージしやすいでしょう。単に固定長(32bit整数なら4バイト)の領域をレコード数分だけ連続して確保してそこに値を詰めていけばよいです。

一方、文字列のような可変長データや辞書のような複合型をどのように配列で表現するかはイメージしにくいかもしれません。詳細はおいおい紹介しますが、たとえば、文字列は2本の配列で表現します。1つ目の配列には文字列が連続して詰まっています。もう1つの配列にはどこからどこまでが1つの文字列かという位置情報が詰まっています。たとえば、["abc", "d", "ef"]という文字列カラムは次のように表現します。

data = ["a", "b", "c", "d", "e", "f"]
offsets = [
  0, # 0番目のカラムの値の開始オフセット→data[0] == "a"
  3, # 0番目のカラムの値の長さ→data[0] + data[1] + data[2] == "abc"
  3, # 1番目のカラムの値の開始オフセット→data[3] == "d"
  1, # 1番目のカラムの値の長さ→data[3] == "d"
  4, # 2番目のカラムの値の開始オフセット→data[4] == "e"
  5, # 2番目のカラムの値の長さ→data[4] + data[5] == "ef"
]

このような構造になっていると定数時間で目的のカラムの値にアクセスできますし、データが局所化されているので連続してアクセスするときも高速です。(データ分析時は集計処理やフィルター処理をしますが、そのようなときは連続してアクセスします。)

このようにApache Arrowはいろいろな型のカラムを配列で表現します。配列は変更できないという制約があります。この制約があるため、少しずつ配列を作っていくということはできません。作るときは一気に作らないといけません。(この制約があるので、配列間でデータを共有でき、サブ配列の作成コストが安くなるというメリットがあるのですが、そのあたりはおいおい紹介します。)

配列を作るのが大変そうと感じるかもしれませんが、配列を作ることを支援するために配列ビルダーが提供されているので、それを使えばそれほど大変ではありません。

配列ビルダー

配列ビルダーとは配列を作るオブジェクトです。配列を作るときは配列ビルダーを使うのが便利です。

配列ビルダーを使う基本的な流れは次の通りです。

  1. 配列ビルダーを作る。
  2. Append()で要素を追加する。(要素がNULLの場合はAppendNull()で要素を追加する。)
  3. Finish()でそれまで追加した要素で配列を作る。

たとえば、真偽値の配列を作るには次のようにarrow::BooleanBuilderを使います。

#include <iostream>
#include <arrow/api.h>

int
main(int argc, char **argv)
{
  std::shared_ptr<arrow::Array> array;
  {
    // 真偽値の配列データを確保する領域
    auto memory_pool = arrow::default_memory_pool();
    // 真偽値の配列を作成するビルダー
    arrow::BooleanBuilder builder(memory_pool);
    // 1つ目の要素はtrue
    builder.Append(true);
    // 2つ目の要素はfalse
    builder.Append(false);
    // 3つ目の要素はtrue
    builder.Append(true);
    // 3要素の配列を作成
    builder.Finish(&array);
  }

  // 内容を確認
  // 出力:
  // 0:true
  // 1:false
  // 2:true
  for (int64_t i = 0; i < array->length(); ++i) {
    auto boolean_array = static_cast<arrow::BooleanArray *>(array.get());
    std::cout << i << ":";
    // i番目の値を出力
    std::cout << (boolean_array->Value(i) ? "true" : "false");
    std::cout << std::endl;
  }

  return 0;
}

Apache Arrowはpkg-configに対応しているのでビルド方法は次の通りです。

% g++ -o boolean-array boolean-array.cpp $(pkg-config --cflags --libs arrow)

実行すると次のように[true, false, true]という配列を作れたことを確認できます。

% ./boolean-array
0:true
1:false
2:true

NULL配列や共用体配列のようにすぐに作れる配列には配列ビルダーはありませんが、基本的には配列ビルダーを使って配列を作ります。

まとめ

Apache Arrowでの配列の位置付けと配列ビルダーを使った配列の作り方を紹介しました。

2017-03-15

MozillaのBugzillaへのパッチのsubmitのやり方

はじめに

MozillaによるFirefoxやThuderbirdの開発は基本的にBugzillaのやり取りにより行われています。そのため、GitHubでのやり取りに比べとっつきにくい印象がどうしても拭えません。MozillaのBugzillaでの開発プロセスを一通り行ったのでまとめます。 前回の記事 は「macOS Printing」や「macOS printToFile PDF」などで検索して引っかかったBugにsubmitしたパッチを解説しました。 今回はそのパッチをsubmitしてmozilla-centralにコミットされるまでの流れを追います。

Bugzillaでのやり取り

Bugzillaでは以下の図のようなやり取りを通じて開発が進められます。*1

review process

Mozillaの開発では基本的にパッチは何らかのBugに紐づけられるものです。 Bugを立てないことにはパッチのレビューをしてもらうことはできません。

準備

まずは直面した問題がBugになっていないかを確認します。まずはキーワードにより検索します。該当のBugがあればこのBugの作業がしたいというコメントを残して権限のある人にAssignしてもらいます。

以降ではprintToFile is busted on Mac | Mozilla Bugzillaでのやり取りを元に解説していきます。

パッチの作成

Bugにアサインされるといよいよパッチをsubmitするための準備が整います。 StatusはNEWのままで問題ないようです。

レビューに出す

MozillaのBugzillaでのレビューはreviewして欲しい人に対してreviewフラグを立てることにより始まります。また、レビューフラグを立ててもreviewフラグが立っている人が忙しいなどの理由で別の人にreviewフラグが移されることもあります。printToFile is busted on Mac | Mozilla Bugzillaでのやり取りではComment 19のようにreviewフラグが別に人に移譲されています。

パッチのコミット

レビューをめでたく通過した場合は、mozillaのリポジトリにコミットしてもらう必要があります。通常はコミット権はないのでcheckin-neededキーワードをkeywordに追加します: https://bugzilla.mozilla.org/show_bug.cgi?id=675709#c19

このcheckin-neededキーワードが追加されていないとレビューには通っても永久にmozillaのリポジトリにコミットされないことに注意してください。

Mozillaの古いWikiではこの段階で既にmozilla-centralへのコミットがなされると書かれていることがありますが、現在はmozilla-inboundというリポジトリにまずはコミットされます。

printToFile is busted on Mac | Mozilla Bugzillaでのやり取りでは一旦review+がもらえましたが、inboundでのビルド失敗により差し戻されています。

執筆現在のinboundサーバーのmacOSのワーカーが10.7のため、未定義となってしまった定数が出てしまったようです。

ステータスがFIXEDになればめでたく該当Bugでの作業は終了です。

まとめ

MozillaのBugzillaでの作業をどのように行うかをprintToFile is busted on Mac | Mozilla BugzillaのBugを元に解説を試みました。GitHubによる開発プロセスになれた開発者にはとっつきにくい印象がありますが、GitHubができるよりも前から開発が続いているソフトウェアの開発プロセスを体験してみるのはいかがでしょうか。

*1 以降は https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/How_to_Submit_a_Patch に基づいて解説します

2017-03-10

Fluentd v0.14.13で追加されたコマンドのご紹介

Fluentd v0.14.13が2017-02-27にリリースされていました。新しいコマンドが2つ追加されたのでご紹介します。

  • fluent-plugin-config-format
  • fluent-plugin-generate

fluent-plugin-config-format

プラグインの設定をいくつかのフォーマットで出力するコマンドです。 README.mdを書くのを楽にしたりコマンドラインからプラグインの設定値の意味を調べるためのものです。 Fluentd v0.12.16でプラグインの設定をコマンドラインから確認する方法で紹介したものの実装を改善して使いやすくしました。

$ fluent-plugin-config-format --help
Usage: fluent-plugin-config-format [options] <type> <name>

Output plugin config definitions

Arguments:
        type: input,output,filter,buffer,parser,formatter,storage
        name: registered plugin name

Options:
        --verbose                    Be verbose
    -c, --compact                    Compact output
    -f, --format=FORMAT              Specify format. (markdown,txt,json)
    -I PATH                          Add PATH to $LOAD_PATH
    -r NAME                          Load library
    -p, --plugin=DIR                 Add plugin directory

以下のコマンドは同じ意味です。

$ fluentd --show-plugin-config input:tail
$ fluent-plugin-config-format --format=txt input tail

plugin helperや Fluent::Plugin::Output の設定についてはdocs.fluentd.orgへのリンクにしたいところですが、2017年2月某日時点ではまだページがないので、リンクを生成することができていません。 ドキュメントが書かれたら、リンクを追加したいと考えています。

fluent-plugin-generate

新規プラグインを開発するときに使用するテンプレートを生成するためのコマンドです。 これまでは、既存のプラグインからコピーしたり、公式のドキュメントからコピーしたりしていましたが、このコマンドを使うことによって、これまでよりも簡単にプラグインの雛形を生成することができるようになりました。

$ fluent-plugin-generate --help
Usage: fluent-plugin-generate [options] <type> <name>

Generate a project skeleton for creating a Fluentd plugin

Arguments:
        type: input,output,filter,parser,formatter
        name: Your plugin name

Options:
        --[no-]license=NAME          Specify license name (default: Apache-2.0)

例えば、Zulipに出力するプラグインを作成するときは以下のように利用します。

$ fluent-plugin-generate output zulip
License: Apache-2.0
        create Gemfile
        create README.md
        create Rakefile
        create fluent-plugin-zulip.gemspec
        create lib/fluent/plugin/out_zulip.rb
        create test/helper.rb
        create test/plugin/test_out_zulip.rb
Initialized empty Git repository in /tmp/fluent-plugin-zulip/.git/

このように、必要なファイルが一通り生成されるのですぐに開発を始めることができます。 ユニットテストにはFluentd本体と同じ test-unit を使用しています。 ただし、このコマンドではFluentd v0.12以前のAPIを使用したプラグインを開発するためのファイルを生成することはできません。

fluent-plugins-nursery/fluent-plugin-zulip はこのコマンドを使用して開発を始めました。 README.mdのConfigurationの項は fluent-plugin-config-format を使用して生成しました。

まとめ

Fluentd v0.14.13で追加された2つの新しいコマンドについて紹介しました。 他の変更点についてはChangeLogを参照してください。

タグ: Fluentd
2017-03-09

OSS Gate大阪ワークショップ2017-02-25を開催 #oss_gate

2017年2月25日にファーストサーバさんOSS Gate大阪ミートアップを開催しました。初の大阪開催です!

集合写真

参加者は26名で東京開催並の規模になりました。大阪だけでなく、神戸・京都など近隣の人たちも集まっていました。

今回は進行の様子を動画に撮ることにチャレンジしました。撮影は@yasulabで、東京から日帰りで参加しました。ありがとうございます!

音声面での課題はありますが、録音機材の整備や字幕の追加などでカバーできそうです。ワークショップの最初の「OSS Gateの紹介」部分を字幕付きで公開しているので、雰囲気を知りたい方は確認してみてください!

今回のワークショップもビギナー・メンターともに楽しんで参加していたようです。次回も参加するという方が多かったので、次回も楽しみですね!

次回の関西でのワークショップ開催は4月になりそうです。興味のある方はチャットに参加してください。最新情報を入手できます。

現在は「OSS Gateワークショップ」は東京・札幌・大阪で開催しています。自分たちの地域でも開催したい!という方はチャットで相談しましょう!

2017-03-01

GeckoエンジンにmacOSでprintToFileの機能を実装してみた話

はじめに

Geckoエンジンはクロスプラットフォームを標榜して作成されています。また、できるだけそのプラットフォームの特長を生かすため、そのプラットフォーム特有のAPIを使用するように作られている箇所もあります。印刷に関連するGeckoのコードももちろんプラットフォームごとに異なるAPIを呼ぶようになっており、通常使用する限りにおいてはどのプラットフォームも一様に印刷の機能を使用することができます。

今回の記事は nsIWebBrowserPrint に紐づいているprintという印刷のためのAPIにmacOSではPDFを印刷するのに必要な情報が渡っていなかった問題を解決した、という題材について書きます。

Geckoでの印刷の流れ

Geckoでは印刷をするときにプラットフォーム固有のAPIを使用する箇所とプラットフォーム非依存のAPIの箇所があります。

プラットフォームに依存するモジュールはサービス化されており、実行時に適切なContract ID(CID)を持ったモジュールがロードされる仕組みとなっています。このCIDはJavaScript側からも見え、アドオンやXULアプリケーション上でもこのCIDを用いて適宜必要なモジュールをロードしていきます。

印刷の流れを追うと以下のようになります。

nsDocumentViewer::Print(...)
  -> nsPrintEngine::Print(...) 
    -> nsPrintEngine::DoCommonPrint(...) 
      -> printDeviceContext->InitForPrinting(aDevice, ...) // プラットフォームごとに別々のDeviceContextが作成されるのでそれを用いる
        -> aDevice->MakePrintTarget(..) 
    -> nsPrintEngine::DoCommonPrint(...) //戻ってきたら印刷プレビューか印刷ジョブを行う

macOSのCocoaのAPIでの実装はwidget/cocoa以下に配置されています。

macOSでprintToFileが動かない問題のBug

macOSではnsIPrintSettingsServiceの関数のprintToFileへtrueを渡してもPDFヘ出力されないというBugが長年に渡って未解決です。詳細はMozillaのBugzillaのprintToFile is busted on Mac | Mozilla Bugzillaを参照してください。

macOSでGeckoのレンダリング結果をPDFへ出力できなかったのはなぜか

macOSでPDFへの出力をさせるのに必要な情報が上記Bugのパッチがコミットされる前のコードで設定されているかどうかを見ます。

macOSでCocoaのAPIからPDF印刷を行うには | ククログ にあるように、NSPrintSaveJobNSPrintJobSavingURL のような値が設定されているかを探します。

ここで、macOSでPDF印刷ができないのはnsIPrintSettingsServiceのCIDを持つ https://hg.mozilla.org/mozilla-central/file/0ca553b86af3/widget/cocoa/nsPrintSettingsX.mm にこれらの値がないかどうかを調べれば原因が分かります。

探すと見事にそのようなことをしている箇所はありませんでした。 前の記事を元にGeckoに向けたパッチを書く必要があります。

GeckoのXPCOMで生成されたクラスのAPIの振る舞いをプラットフォーム固有にする

GeckoはXPCOMの技術を用いており、インターフェースはidlファイルで定義されています。 ここではnsIPrintSettingsのidlは https://dxr.mozilla.org/mozilla-central/source/widget/nsIPrintSettings.idl です。

GeckoはC++で書かれているため、nsPrintSettings クラスに定義されているメソッドで必要なものをoverrideしてやれば目的は達成できます。

そのため、SetToFileName(const char16_t *aToFileName) をoverrideします。

diff --git a/widget/cocoa/nsPrintSettingsX.h b/widget/cocoa/nsPrintSettingsX.h
--- a/widget/cocoa/nsPrintSettingsX.h
+++ b/widget/cocoa/nsPrintSettingsX.h
@@ -51,8 +51,10 @@ public:
   void SetInchesScale(float aWidthScale, float aHeightScale);
   void GetInchesScale(float *aWidthScale, float *aHeightScale);

+  NS_IMETHOD SetToFileName(const char16_t *aToFileName) override;
+
   void SetAdjustedPaperSize(double aWidth, double aHeight);
   void GetAdjustedPaperSize(double *aWidth, double *aHeight);

diff --git a/widget/cocoa/nsPrintSettingsX.mm b/widget/cocoa/nsPrintSettingsX.mm
--- a/widget/cocoa/nsPrintSettingsX.mm
+++ b/widget/cocoa/nsPrintSettingsX.mm
@@ -270,3 +275,30 @@ void nsPrintSettingsX::GetAdjustedPaperS
   *aWidth = mAdjustedPaperWidth;
   *aHeight = mAdjustedPaperHeight;
 }
+
+NS_IMETHODIMP
+nsPrintSettingsX::SetToFileName(const char16_t *aToFileName)
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  NSMutableDictionary* printInfoDict = [mPrintInfo dictionary];
+  nsString filename = nsDependentString(aToFileName);
+
+  NSURL* jobSavingURL =
+      [NSURL fileURLWithPath: nsCocoaUtils::ToNSString(filename)];
+  if (jobSavingURL) {
+    [printInfoDict setObject: NSPrintSaveJob forKey: NSPrintJobDisposition];
+    [printInfoDict setObject: jobSavingURL forKey: NSPrintJobSavingURL];
+  }
+  NSPrintInfo* newPrintInfo =
+      [[NSPrintInfo alloc] initWithDictionary: printInfoDict];
+  if (NS_WARN_IF(!newPrintInfo)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  SetCocoaPrintInfo(newPrintInfo);
+  [newPrintInfo release];
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}

ここまででPDFに出力するための関数の実装です。

まだmacOS向けにはoverrideする必要がある関数が残っています:

  • 用紙の単位
    • NS_IMETHOD SetPaperSizeUnit(int16_t aPaperSizeUnit) override;
  • 拡大縮小倍率
    • NS_IMETHOD SetScaling(double aScaling) override;
  • 縦横
    • NS_IMETHOD GetOrientation(int32_t *aOrientation) override;
    • NS_IMETHOD SetOrientation(int32_t aOrientation) override;
  • 余白(GeckoではMerginに加えてUnwritaebleMerginという設定値が存在する)
    • NS_IMETHOD SetUnwriteableMarginTop(double aUnwriteableMarginTop) override;
    • NS_IMETHOD SetUnwriteableMarginLeft(double aUnwriteableMarginLeft) override;
    • NS_IMETHOD SetUnwriteableMarginBottom(double aUnwriteableMarginBottom) override;
    • NS_IMETHOD SetUnwriteableMarginRight(double aUnwriteableMarginRight) override;

これらと、印刷用紙の単位変換を行なう処理を追加したものがこちらです。

このmozilla-centralのパッチではGeckoが印刷に用いている二つの単位系の対応も入っています。

  • インチ
    • kPaperSizeInches
  • ミリメーター
    • kPaperSizeMillimeters

の両方の単位系がGeckoでの印刷ではサポートされています。

そのため、

  • Inches -> Twips -> Pixels
  • Millimeters -> Twips -> Pixels

の両方を取り扱う必要があります。これは

diff --git a/widget/cocoa/nsPrintSettingsX.mm b/widget/cocoa/nsPrintSettingsX.mm
--- a/widget/cocoa/nsPrintSettingsX.mm
+++ b/widget/cocoa/nsPrintSettingsX.mm
@@ -254,8 +254,13 @@ NS_IMETHODIMP nsPrintSettingsX::SetPaper
 NS_IMETHODIMP
 nsPrintSettingsX::GetEffectivePageSize(double *aWidth, double *aHeight)
 {
-  *aWidth  = NS_INCHES_TO_TWIPS(mAdjustedPaperWidth / mWidthScale);
-  *aHeight = NS_INCHES_TO_TWIPS(mAdjustedPaperHeight / mHeightScale);
+  if (kPaperSizeInches == GetCocoaUnit(mPaperSizeUnit)) {
+    *aWidth  = NS_INCHES_TO_TWIPS(mAdjustedPaperWidth / mWidthScale);
+    *aHeight = NS_INCHES_TO_TWIPS(mAdjustedPaperHeight / mHeightScale);
+  } else {
+    *aWidth  = NS_MILLIMETERS_TO_TWIPS(mAdjustedPaperWidth / mWidthScale);
+    *aHeight = NS_MILLIMETERS_TO_TWIPS(mAdjustedPaperHeight / mHeightScale);
+  }
   return NS_OK;
 }

diff --git a/widget/cocoa/nsDeviceContextSpecX.mm b/widget/cocoa/nsDeviceContextSpecX.mm
--- a/widget/cocoa/nsDeviceContextSpecX.mm
+++ b/widget/cocoa/nsDeviceContextSpecX.mm
@@ -49,6 +49,19 @@ NS_IMETHODIMP nsDeviceContextSpecX::Init
   if (!settings)
     return NS_ERROR_NO_INTERFACE;

+  bool toFile;
+  settings->GetPrintToFile(&toFile);
+
+  bool toPrinter = !toFile && !aIsPrintPreview;
+  if (!toPrinter) {
+    double width, height;
+    settings->GetEffectivePageSize(&width, &height);
+    width /= TWIPS_PER_POINT_FLOAT;
+    height /= TWIPS_PER_POINT_FLOAT;
+
+    settings->SetCocoaPaperSize(width, height);
+  }
+
   mPrintSession = settings->GetPMPrintSession();
   ::PMRetain(mPrintSession);
   mPageFormat = settings->GetPMPageFormat();

のコードにより取り扱うことができます。

ここではGetCocoaUnitkPaperSizeMillimeterskPaperSizeInches を返すprotectedメソッドです。また、SetCocoaPaperSizeは印刷用紙の大きさを設定するメソッドです。

まとめ

macOSでもXULアプリケーションなどから printToFile = true を設定したときにPDFへ出力するパッチについて解説しました。 このBugのレポート日時は 2011-08-01 12:35 PDT なので、このパッチで5年半越しの問題が解決されました。 mozilla-centralの該当コミットを見るとBug番号がまだ6桁であり、周辺のコミットに紐づいたBug番号は7桁なので感慨深いですね。

つづき: 2017-03-10
タグ: Mozilla
2017-02-22

macOSでCocoaのAPIからPDF印刷を行うには

はじめに

macOSでは印刷をプログラマブルに行うことができます。 また、macOSではAppKit, Core PrintingのAPIを用いて印刷を行うことができます。

通常はAppKitにあるNSPrintInfoやNSPrintOperationを使えばいいのですが、より細部の設定を変更するにはCore PrintingのAPIに触る必要があります。この記事ではAppKitの範囲でmacOSの印刷について見ます。

AppKitを用いた印刷

単純な例

AppKitで印刷するにはまず、NSPrintOperationクラスを作成する必要があります。また、必要に応じてNSPrintInfoクラス を作成する必要があります。

単純にNSViewを持つCocoaアプリケーションからビューを印刷するには以下のように NSPrintOperation クラスを用いて

- (IBAction)print:(id)sender {
      NSPrintOperation *op;
      op = [NSPrintOperation printOperationWithView:self];
      if (op)
           [op setShowPanels:YES]; // If set 'NO', printing modal dialog will not be shown.
           [op runOperation];
      else
          // handle error here
}

のようにします。ここではselfがNSViewのインスタンスであることを要求しています。

NSPrintInfoを用いたより複雑な例
- (IBAction)print:(id)sender {
      NSPrintOperation* op;
      // Get NSPrintInfo
      NSPrintInfo* printInfo = [NSPrintInfo sharedPrintInfo];
      [printInfo setTopMargin:10.0];
      [printInfo setBottomMargin:10.0];

      op = [NSPrintOperation printOperationWithView:self printInfo:printInfo];
      if (op)
           [op setShowPanels:YES]; // If set 'NO', printing modal dialog will not be shown.
           [op runOperation];
      else
          // handle error here
}

のようにすることで、NSPrintOperationクラスにカスタマイズを施したNSPrintInfoクラスのオブジェクトを渡すことができます。

NSPrintInfoへdictionaryを渡しPDFを出力する場合

NSPrintInfoは以下のようにNSDictionary, NSMutableDictionaryのオブジェクトを渡すことにより初期化できます。

NSPrintInfo* printInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];

これを利用すると、例えば以下のようなコードを用いてアプリケーションのビューをPDFへ出力できるようになります。

NSPrintInfo* sharedInfo = [NSPrintInfo sharedPrintInfo];
NSMutableDictionary* printInfoDict = [sharedInfo dictionary];

NSURL* jobSavingURL = [NSURL fileURLWithPath:@"日本語ファイル名.pdf"];

[printInfoDict setObject:NSPrintSaveJob forKey:NSPrintJobDisposition];
[printInfoDict setObject:jobSavingURL forKey:NSPrintJobSavingURL];

NSPrintInfo* printInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];

// Then, set Cocoa application's NSView or its subclass View
NSPrintOperation* op = [NSPrintOperation printOperationWithView:self printInfo:printInfo];
[op setShowPanels:NO];
[op runOperation];

ここで注意する点は

NSURL* jobSavingURL = [NSURL fileURLWithPath:@"日本語ファイル名.pdf"];

は正しく日本語も扱えるコードとなりますが、

NSURL* jobSavingURL = [NSURL fileURLWithString:@"日本語ファイル名.pdf"];

は日本語が正しくエスケープされずにファイル出力に失敗します。

まとめ

macOSでのAppKitのAPIを用いた印刷の仕組み、特にPDFへ出力するようにするにはどうするかをざっと解説しました。 この記事では詳しく取り上げませんが、NSPrintJobDispositionへは通常のプリンターに送ったり、Preview.appに送ったり、PDFとしてファイルに出力したりという印刷ジョブの大まかな行き先を指定するために用いる定数を指定することができます。 macOSの印刷のAPIは扱いに慣れると一貫性のあるAPIとなっていることがわかります。

つづき: 2017-02-22
2017-02-21

名古屋Ruby会議03:Apache ArrowのRubyバインディングをGObject Introspectionで #nagoyark03

2017年2月11日に名古屋Ruby会議03大須演芸場で開催されました。ここで咳さんの並列処理啓蒙活動話の前座の1人として話してきました。内容は、Apache ArrowGObject IntrospectionRroongaを活用すれば自然言語のデータ分析の一部でRubyを活用できるよ!、です。

関連リンク:

リポジトリーには今回のスライド内で使ったスクリプトも入っています。

内容

当初は以下にいろいろまとめていた通り、Apache Arrow・GObject Introspectionはどんな特徴でどういう仕組みでそれを実現しているかといったことも説明するつもりでした。

ただ、内容をまとめていく過程で、特徴や仕組みの説明が多いと大須演芸場で話すには勢いが足りないと判断しました。その結果、詳細をもろもろ省略したストーリーベースの内容にしました。そのため、↑の内容はほぼ使っていません。

ストーリーベースの内容にしたことで「あぁ、たしかにデータ分析の一部でRubyを使えるかも」という雰囲気は伝わりやすくなったはず(どうだったでしょうか?)ですが、詳細の説明を省略したので詳細が気になったまま終わった人もいるかもしれません。そんな人たち向けに詳細がわかる追加情報を紹介します。

Apache ArrowとApache Parquet

まず、Apache ArrowとApache Parquetの連携についてです。両者はどちらもカラムストアのデータについて扱いますが、Apache Arrowはメモリー上での扱い、Aapache Parquetはストレージ上での扱いという違いがあります。この違いのためにトレードオフのバランスが変わっています。詳細はThe future of column-oriented data processing with Arrow and Parquetを見てください。2016年11月にニューヨークで開催されたDataEngConf NYCでのApache Parquetの作者の発表資料です。

ざっくりとまとめると次の通りです。

  • Apache Parquetはストレージ上でのカラムストアのデータの扱いなので、次の傾向がある。
    • CPUを活用するよりもI/Oを減らすほうが重要(たとえば、データを圧縮するとI/Oは減る一方CPU負荷は増えてしまうが、I/Oを減らしたいので圧縮をがんばる)
    • シーケンシャルアクセスが多い
  • Apache Arrowはメモリー上でのカラムストアのデータの扱いなので、次の傾向がある。
    • I/Oを減らすよりもCPUを活用するほうが重要(たとえば、CPUキャッシュミスが少なくなるようなデータ配置にする)
    • シーケンシャルアクセスもあるしランダムアクセスもある

データの配置や高速化の工夫なども前述の発表資料で説明しているので興味のある人は発表資料も確認してください。

Apache ArrowとApache Parquetは連携できます。具体的に言うと、Apache ParquetのC++実装にはApache Arrowのデータを読み書きできる機能があります。これを使うとApache ParquetのデータをApache Arrowのデータとして扱うことができます。

GObject Introspection

GObject Introspectionに関する情報は次の記事を参考にしてください。

GObject Introspectionに対応するとバインディングを書かなくてもCライブラリーのテストをRubyで書けるようになります。Cライブラリーの開発を捗らせるためにGObject Introspectionに対応させるというのもアリです。(これも当初は話したかったけど省略した話題です。)

GObject IntrospectionのRubyバインディングであるgobject-introspection gemについてはここで少し補足します。

話の中で、GI.loadした後、よりRubyっぽいAPIになるように一手間かけるとグッと使いやすくなると説明しました。当日は次のようにエイリアスを使う方法だけを紹介したのですが、別の方法もあってそれを紹介することをすっかり忘れていました。

require "gi"
Arrow = GI.load("Arrow")
class Arrow::Array
  def [](i)
    get_value(i)
  end
end

実はGObjectIntrospection::Loaderには定義するメソッド名を変える機能があります。上述のケースではエイリアスを作るのではなく、最初からget_value[]として定義するとよいです。

class Arrow::Loader < GObjectIntrospection::Loader
  private
  def rubyish_method_name(function_info, options={})
    # 引数が1つで、メソッド名がget_valueならメソッド名を[]にする
    if function_info.n_in_args == 1 and function_info.name == "get_value"
      "[]"
    else
      super
    end
  end
end

このようにGObjectIntrospection::Loaderをカスタマイズするやり方には次のメリットがあります。

  • 余計なメソッドを増やさない(今回のケースではget_value
  • 新しく同じパターンのメソッドが増えてもエイリアスを追加する必要がない(たとえば、Arrow::Array以外にget_value(i)なメソッドが増えてもバインディングを変更する必要がない)

この実装はRed Arrow(RArrowから名前を変更、由来はRubyは赤いからというのと西武新宿線の特急列車)にあります。open {|io| ...}を実現する方法も面白いので、GObject Introspectionが気になってきた方はぜひ実装も見てみてください。

まとめ

名古屋Ruby会議03で「Apache ArrowとGObject IntrospectionとRroongaを使って自然言語のデータ分析の一部でRubyを活用する」という話をしました。(使い方を間違っていましたが)はじめて小拍子を使ったり、はじめてマクラの後に羽織(?)を脱いだりできて、楽しかったです。貴重な経験になりました。声をかけてもらってありがとうございます。名古屋のみなさん(名古屋外からの参加の方も多かったですが)に楽しんでもらえていたなら、とてもうれしいことです。

今回の話では詳細をもろもろ省略しましたが、そのあたりに興味のある方がいたらぜひお声がけください。

また、クリアコードと一緒にRubyでデータ分析できる環境を整備していきたい!という方はぜひお問い合わせください。

つづき: 2017-03-15
タグ: Ruby
2017-02-15

Groonga Meatup 2017:Groonga族2016 #groonga

2017年2月9日(年に一度の肉の日!)にGroonga Meatup 2017が開催されました。ここでGroongaMroongaPGroongaの2016一年間での進化の様子を紹介しました。最近のGroonga・Mroonga・PGroongaの情報をざっと把握できるのでご活用ください。

関連リンク:

内容

2016年はGroonga・PGroongaの改良が多く、Mroongaの改良は少なめでした。ただ、MroongaはGroongaを使っているので、Groongaの改良(の多く)はそのままMroongaでも利用できます。そのため、Groonga・PGroongaだけでなくMroongaも進化しています。

Groongaの大きな改良はこのあたりです。

Mroongaの大きな改良はこのあたりです。

  • FOREIGN KEY制約をサポート
  • マルチカラムインデックスの更新性能劣化を解消
  • *SSプラグマを追加(MATCH AGAINSTないでGroongaの検索条件を使える。Groongaの検索条件では複数インデックスを使えるのでMySQLで検索するよりも圧倒的に速くなる。)

PGroongaの大きな改良はこのあたりです。

  • ストリーミングレプリケーションをサポート
  • 検索の高速化(PostgreSQLがより適切な実行計画を選べるように、PostgreSQLにより精度の高い情報を提供するようになった)
  • Zstandardによるカラム圧縮をサポート
  • 各種便利演算子を追加(類似文書検索演算子・前方一致検索演算子・前方一致RK検索演算子)

他にもいろいろあるのでぜひスライドも確認してください。

まとめ

Groonga Meatup 2017で2016年のGroonga族の様子を紹介しました。

なお、今回のGroonga Meatup 2017は東京で開催しましたが、今月、名古屋と大阪でも開催されるので近辺の方はぜひお越しください!

他の発表については以下を参照してください。

おまけみたいな書き方になってしまいますが、同日、初心者向けのMroongaの電子書籍Groongaではじめる全文検索がリリースされました!初心者の人がくじけずに始められるようにできるだけ短い内容(約30ページ!)で動くものが作れる、という内容になっています。具体的にはPHPでMroongaを使ってPDF検索システムを作っています。全文検索をしたいけどどこから始めればいいかピンときていない…という方はまずはこれを読んでみてください。

タグ: Groonga
2017-02-09

名古屋Ruby会議03:Apache ArrowのRubyバインディング(6) #nagoyark03

前回はGObject Introspectionでのオブジェクトの寿命の扱いについて説明しました。今回は戻り値のオブジェクトの寿命について説明します。実際に動くバインディングはkou/arrow-glibにあります。

戻り値のオブジェクトの寿命

一般的に戻り値のオブジェクトの寿命には次のパターンがあります。

  • 所有権は関数実行側にあるパターン:戻り値のオブジェクトが必要なくなったら戻り値を受け取った側が責任を持って解放する。
  • 所有権は関数側にあるパターン:戻り値を受け取った側は解放してはいけない。

話を単純にするために、戻り値のオブジェクトとして文字列(char *)を考えてみましょう。

strdup(3)(引数の文字列をコピーして返す関数)は前者(所有権は関数実行側にあるパターン)です。strdup(3)の戻り値はstrdup(3)を呼び出した側が責任を持って解放しなければいけません。

char *copy;
copy = strdup("Hello");
free(copy); /* 呼び出し側がfree()する。 */

getenv(3)は後者(所有権は関数側にあるパターン)です。getenv(3)の戻り値はgetenv(3)を呼び出した側が解放してはいけません

char *path;
path = getenv("PATH");
/* free(path); */ /* 呼び出し側がfree()してはダメ! */

これが基本です。

ただし、前者の「所有権は関数実行側にあるパターン」にはもう少しパターンがあります。それは戻り値のオブジェクトがコンテナーの場合です。コンテナーというのは配列・リスト・ハッシュテーブルなどのように複数のオブジェクトを格納するオブジェクトのことです。

コンテナーの場合は、次のパターンがあります。

  • コンテナーの所有権のみ関数実行側にあるパターン
  • コンテナーの所有権もコンテナーの中のオブジェクトの所有権も関数実行側にあるパターン

コンテナーの所有権のみの場合はコンテナーのみを解放します。

GObject Introspectionでの戻り値のオブジェクトの寿命の指定

GObject Introspectionではこれらのそれぞれのパターンを指定できます。指定すると後はGObject Introspection(を使ったバインディング、Rubyならgobject-introspection gem)が適切に寿命を管理してくれます。

パターンはそれぞれの関数のドキュメントで指定します。

次の関数は新しくGArrowArrayオブジェクトを作って返す関数です。新しく作ったオブジェクトの所有権は関数呼び出し側にあります。そのため、「所有権は関数実行側にあるパターン」です。

/**
 * garrow_array_builder_finish:
 * @builder: A #GArrowArrayBuilder.
 *
 * Returns: (transfer full): The built #GArrowArray.
 */
GArrowArray *
garrow_array_builder_finish(GArrowArrayBuilder *builder)
{
  auto arrow_builder = garrow_array_builder_get_raw(builder);
  std::shared_ptr<arrow::Array> arrow_array;
  arrow_builder->Finish(&arrow_array);
  return garrow_array_new_raw(&arrow_array);
}

ポイントは次の箇所です。Returns:が戻り値に関する情報のドキュメントという意味で、(transfer full)が「所有権は呼び出し側にある」という意味です。

/**
 * Returns: (transfer full): The built #GArrowArray.
 */

「所有権は関数側にあるパターン」も見てみましょう。

次の関数はフィールド名を返す関数です。フィールド名はフィールドオブジェクトに所有権があるので関数呼び出し側は解放してはいけません。(gcharはGLibが提供しているchar型です。charと同じです。)

/**
 * garrow_field_get_name:
 * @field: A #GArrowField.
 *
 * Returns: The name of the field.
 */
const gchar *
garrow_field_get_name(GArrowField *field)
{
  const auto arrow_field = garrow_field_get_raw(field);
  return arrow_field->name.c_str();
}

ポイントは次の箇所です。戻り値の型にconstがついている場合はGObject Introspectionは所有権は関数側にあると推測します。そのため、明示的にReturns: (transfer none):と書く必要はありません。

const gchar *

「コンテナーの所有権のみ関数実行側にあるパターン」も見てみましょう。

arrow-glibでは使っていないのでGTK+にある関数を持ってきました。

/**
 * gtk_print_backend_load_modules:
 *
 * Returns: (element-type GtkPrintBackend) (transfer container):
 */
GList *
gtk_print_backend_load_modules (void)
{
  /* ... */
}

ポイントは(transfer container)です。これでコンテナーの所有権のみ関数呼び出し側にある、ということを指定します。

(element-type GtkPrintBackend)はコンテナー内の要素の型を指定しています。)

最後に「コンテナーの所有権もコンテナーの中のオブジェクトの所有権も関数実行側にあるパターン」を見てみましょう。

/**
 * garrow_record_batch_get_columns:
 * @record_batch: A #GArrowRecordBatch.
 *
 * Returns: (element-type GArrowArray) (transfer full):
 *   The columns in the record batch.
 */
GList *
garrow_record_batch_get_columns(GArrowRecordBatch *record_batch)
{
  const auto arrow_record_batch = garrow_record_batch_get_raw(record_batch);

  GList *columns = NULL;
  for (auto arrow_column : arrow_record_batch->columns()) {
    GArrowArray *column = garrow_array_new_raw(&arrow_column);
    columns = g_list_prepend(columns, column);
  }

  return g_list_reverse(columns);
}

ポイントは(transfer full)です。(transfer full)はコンテナーなオブジェクトにもそうでないオブジェクトにも使えるということです。

まとめ

GObject Introspectionでの戻り値のオブジェクトの寿命について説明しました。次回はインターフェイスについて説明します。

つづき: 2017-02-15
タグ: Ruby
2017-02-01

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|
タグ:
RubyKaigi 2015 sponsor RubyKaigi 2015 speaker RubyKaigi 2015 committer RubyKaigi 2014 official-sponsor RubyKaigi 2014 speaker RubyKaigi 2014 committer RubyKaigi 2013 OfficialSponsor RubyKaigi 2013 Speaker RubyKaigi 2013 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
RubyKaigi2008Speaker