前回は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(を使ったバインディング、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年2月9日(年に一度の肉の日!)にGroonga Meatup 2017が開催されました。ここでGroongaとMroongaとPGroongaの2016一年間での進化の様子を紹介しました。最近のGroonga・Mroonga・PGroongaの情報をざっと把握できるのでご活用ください。
関連リンク:
2016年はGroonga・PGroongaの改良が多く、Mroongaの改良は少なめでした。ただ、MroongaはGroongaを使っているので、Groongaの改良(の多く)はそのままMroongaでも利用できます。そのため、Groonga・PGroongaだけでなくMroongaも進化しています。
Groongaの大きな改良はこのあたりです。
Mroongaの大きな改良はこのあたりです。
*SS
プラグマを追加(MATCH AGAINST
ないでGroongaの検索条件を使える。Groongaの検索条件では複数インデックスを使えるのでMySQLで検索するよりも圧倒的に速くなる。)PGroongaの大きな改良はこのあたりです。
他にもいろいろあるのでぜひスライドも確認してください。
Groonga Meatup 2017で2016年のGroonga族の様子を紹介しました。
なお、今回のGroonga Meatup 2017は東京で開催しましたが、今月、名古屋と大阪でも開催されるので近辺の方はぜひお越しください!
他の発表については以下を参照してください。
おまけみたいな書き方になってしまいますが、同日、初心者向けのMroongaの電子書籍Groongaではじめる全文検索がリリースされました!初心者の人がくじけずに始められるようにできるだけ短い内容(約30ページ!)で動くものが作れる、という内容になっています。具体的にはPHPでMroongaを使ってPDF検索システムを作っています。全文検索をしたいけどどこから始めればいいかピンときていない…という方はまずはこれを読んでみてください。
2017年2月11日に名古屋Ruby会議03が大須演芸場で開催されました。ここで咳さんの並列処理啓蒙活動話の前座の1人として話してきました。内容は、Apache ArrowとGObject IntrospectionとRroongaを活用すれば自然言語のデータ分析の一部でRubyを活用できるよ!、です。
関連リンク:
リポジトリーには今回のスライド内で使ったスクリプトも入っています。
当初は以下にいろいろまとめていた通り、Apache Arrow・GObject Introspectionはどんな特徴でどういう仕組みでそれを実現しているかといったことも説明するつもりでした。
ただ、内容をまとめていく過程で、特徴や仕組みの説明が多いと大須演芸場で話すには勢いが足りないと判断しました。その結果、詳細をもろもろ省略したストーリーベースの内容にしました。そのため、↑の内容はほぼ使っていません。
ストーリーベースの内容にしたことで「あぁ、たしかにデータ分析の一部でRubyを使えるかも」という雰囲気は伝わりやすくなったはず(どうだったでしょうか?)ですが、詳細の説明を省略したので詳細が気になったまま終わった人もいるかもしれません。そんな人たち向けに詳細がわかる追加情報を紹介します。
まず、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 ArrowとApache Parquetは連携できます。具体的に言うと、Apache ParquetのC++実装にはApache Arrowのデータを読み書きできる機能があります。これを使うとApache ParquetのデータをApache Arrowのデータとして扱うことができます。
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でデータ分析できる環境を整備していきたい!という方はぜひお問い合わせください。
macOSでは印刷をプログラマブルに行うことができます。 また、macOSではAppKit, Core PrintingのAPIを用いて印刷を行うことができます。
通常はAppKitにあるNSPrintInfoやNSPrintOperationを使えばいいのですが、より細部の設定を変更するにはCore PrintingのAPIに触る必要があります。この記事ではAppKitの範囲でmacOSの印刷について見ます。
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のインスタンスであることを要求しています。
- (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は以下のように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となっていることがわかります。
Geckoエンジンはクロスプラットフォームを標榜して作成されています。また、できるだけそのプラットフォームの特長を生かすため、そのプラットフォーム特有のAPIを使用するように作られている箇所もあります。印刷に関連するGeckoのコードももちろんプラットフォームごとに異なるAPIを呼ぶようになっており、通常使用する限りにおいてはどのプラットフォームも一様に印刷の機能を使用することができます。
今回の記事は nsIWebBrowserPrint
に紐づいているprintという印刷のためのAPIにmacOSではPDFを印刷するのに必要な情報が渡っていなかった問題を解決した、という題材について書きます。
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ではnsIPrintSettingsServiceの関数のprintToFileへtrueを渡してもPDFヘ出力されないというBugが長年に渡って未解決です。詳細はMozillaのBugzillaのprintToFile is busted on Mac | Mozilla Bugzillaを参照してください。
macOSでPDFへの出力をさせるのに必要な情報が上記Bugのパッチがコミットされる前のコードで設定されているかどうかを見ます。
macOSでCocoaのAPIからPDF印刷を行うには | ククログ にあるように、NSPrintSaveJob
や NSPrintJobSavingURL
のような値が設定されているかを探します。
ここで、macOSでPDF印刷ができないのはnsIPrintSettingsServiceのCIDを持つ https://hg.mozilla.org/mozilla-central/file/0ca553b86af3/widget/cocoa/nsPrintSettingsX.mm にこれらの値がないかどうかを調べれば原因が分かります。
探すと見事にそのようなことをしている箇所はありませんでした。 前の記事を元にGeckoに向けたパッチを書く必要があります。
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する必要がある関数が残っています:
これらと、印刷用紙の単位変換を行なう処理を追加したものがこちらです。
このmozilla-centralのパッチではGeckoが印刷に用いている二つの単位系の対応も入っています。
の両方の単位系がGeckoでの印刷ではサポートされています。
そのため、
の両方を取り扱う必要があります。これは
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();
のコードにより取り扱うことができます。
ここではGetCocoaUnit
は kPaperSizeMillimeters
か kPaperSizeInches
を返すprotectedメソッドです。また、SetCocoaPaperSize
は印刷用紙の大きさを設定するメソッドです。
macOSでもXULアプリケーションなどから printToFile = true
を設定したときにPDFへ出力するパッチについて解説しました。
このBugのレポート日時は 2011-08-01 12:35 PDT
なので、このパッチで5年半越しの問題が解決されました。
mozilla-centralの該当コミットを見るとBug番号がまだ6桁であり、周辺のコミットに紐づいたBug番号は7桁なので感慨深いですね。