APIの利用

SubversionライブラリAPIを使ったアプリケーションの開発は比較的素直な 形で進みます。すべてのヘッダファイルはソースツリーの subversion/include にあります。これらのヘッダは ソースコードからSubversionを作りインストールすると、そのマシンの システムヘッダの置き場所にコピーされます。このようなヘッダには Subversionライブラリのユーザによってアクセスできるような機能と型の すべてがあります。

最初に気をつけなくてはならないのはSubversionのデータ型と関数は 固有の名前空間によって分離されていることです。すべてのパブリックな Subversionシンボル名はsvn_で始まり、そのシンボルが 定義されているライブラリの短いコードが続き、 (wcとか、 clientとか、fsなど)、 アンダースコアが一つきて、(_) 、最後にシンボル名の 残りの部分がきます。限定的にパブリックな関数(ライブラリ中のソースファイル 間では利用されるが、ライブラリの外では利用されず、ライブラリディレクトリ 自身の内部でだけ参照可能なもの)はこの命名規約とは違い、ライブラリコード のあとにアンダースコアが一つくるかわりに、二つきます (__)。あるソースファイルでプライベートな関数は 特殊な接頭辞を持たず、static宣言されます。もちろん コンパイラはこのような命名規約を解釈しませんが、ある関数のスコープや データ型を明らかにするときの助けになります。

Apache Portable Runtime ライブラリ

Subversion自身のデータ型とともに、apr_で 始まるデータ型への参照をたくさん見かけることがあります— これは Apache のPortable Runtime (APR) ライブラリです。APR は Apache の可搬なライブラリですが、もともとApacheのサーバコードの OS依存の部分をOS非依存の部分から分離するために作られました。 結果は、OSごとに、わずかに、あるいは大きく異なる操作を実行する ための抽象的なAPIを提供することになりました。Apache HTTPサーバは 明らかにAPRライブラリの最初の利用者でしたが、Subversion開発者は すぐにAPRを使うことの重要性に気づきました。これは実際にSubversion 自身の中にまったくOSに依存していないコードの部分があることを意味 します。さらに、Subversionクライアントはサーバがコンパイルし実行 する場所であればどこでも実行できることを意味します。現時点では このようなOSには、Unixライクなすべて、Win32, BeOS, OS/2 そして Mac OS X が含まれます。

オペレーティングシステム間で異なるシステムコールの一貫した 実装を提供することに加えて、 [43] APR は Subversionがたくさんの独自のデータ型に直接アクセスすることを 可能にしますが、それには、動的な配列やハッシュテーブルがあります。 Subversion はソースコード中でこれらの型を拡張して利用します。 しかし、多分最も広範囲に利用されているAPRデータ型は、ほとんど すべてのSubversion APIプロトタイプに現れますが、apr_pool_tです —APRのメモリプールです。Subversion はプールを内部的にすべてのメモリ確保 が必要な場合に利用します。(ただし、外部ライブラリがそのAPIを通じて 受け渡すデータのメモリ管理にこれと異なる形式を要求しない限りにおいて、 です。) [44] そして、Subversion API に対するコーディングは同じことを要求される わけではありませんが、必要な場所では API関数のために pool を用意する ことは要求されます。これは APR もリンクする必要のあるSubversion API の ユーザはapr_initialize()を呼んで APR サブシステム を初期化する必要があり、それから Subversion API 呼び出しを利用するために pool を用意しなくてはならない、ということになります。詳細は 「メモリプールを使ったプログラミング」を見てください。

URL と Path の要求

Subversion全体の問題としてのリモートバージョン管理操作では、国際化(i18n) のサポートについていくらか注意しておく必要があります。 結局、リモートが、オフィス以外の場所を意味するのなら、それは 全世界からという意味でもあります。このような状況に対応するために Subversionのパス引数をとる、すべてのパブリックインターフェースは パスが正規化され、UTF-8でエンコードされているものとします。 これはたとえば、libsvn_clientインターフェースを呼び出す、 新しいクライアントバイナリはすべて、Subversionライブラリにパスを渡す 前に、まずパスをローカルコーディングからUTF-8に変換しなくてはならず、 Subversion からの結果パスを、Subversion以外の目的に利用する前には ローカルコーディングに再変換しなくてはならないということです。 幸運なことに、Subversionはこのような変換が必要な任意のプログラムが 利用できるような関数を用意しています (subversion/include/svn_utf.hを参照してください)。

また、Subversion API はすべての URL引数が正しく URIエンコードされている ことを要求します。それで My File.txtという名前のファイルURLを、 file:///home/username/My File.txt と渡すかわりに、 file:///home/username/My%20File.txt と渡さなくてはなりません。 やはりSubversionはアプリケーションが利用できるヘルパー関数を用意して います— svn_path_uri_encode()svn_path_uri_decode()を使ってそれぞれ URI のエンコードとデコードができます。

C と C++以外の言語の利用

C言語以外のものと組み合わせてSubversionライブラリを使うのに興味が あるなら—たとえばPython や Perl のスクリプトなどを使った— SubversionはSimplified Wrapper and Interface Generator (SWIG)という形 である程度サポートしています。Subversion用のSWIGは、 subversion/bindings/swigにあり、開発はまだ続いては いますが、利用可能な状態にあります。これを使えば、スクリプト言語固有の データ型を、SubversionのCライブラリで必要なデータ型に変換するラッパを 使ってSubversion API を間接的に呼び出すことができるようになります。

言語連携を通じてSubversionAPI にアクセスするのは明らかに利点があります —単純さ、です。一般的に、Python や Perl といった言語は C や C++ を使うよりもずっと柔軟でやさしいものです。このような言語が用意している 高レベルデータ型と文脈依存のデータ型のチェックのようなものは、もっと うまくユーザからの情報を処理します。ご存知のように、人間はプログラムの入力で ヘマをやらかすことにかけては達人であり、スクリプト言語はそのような間違っ た情報をより適切に扱える傾向になります。もちろんそのような柔軟性は しばしばパフォーマンスを犠牲にしますが。これが、非常に厳しく最適化された C 言語ベースのインターフェース+ライブラリ群と、強力で柔軟な連携言語 の組み合わせを利用するというやり方が強い説得力をもつ理由です。

Subversion の Python SWIG 連携を使って、最新のリポジトリリビジョンを 再帰的に訪問し、その途中で見つかったさまざまなパスを表示するような サンプルプログラムを見てみましょう。

例8.2 Pythonを使ったリポジトリ層

#!/usr/bin/python

"""Crawl a repository, printing versioned object path names."""

import sys
mport os.path
import svn.fs, svn.core, svn.repos

def crawl_filesystem_dir(root, directory, pool):
    """Recursively crawl DIRECTORY under ROOT in the filesystem, and return
    a list of all the paths at or below DIRECTORY.  Use POOL for all 
    allocations."""

    # Print the name of this path.
    print directory + "/"
    
    # Get the directory entries for DIRECTORY.
    entries = svn.fs.svn_fs_dir_entries(root, directory, pool)

    # Use an iteration subpool.
    subpool = svn.core.svn_pool_create(pool)

    # Loop over the entries.
    names = entries.keys()
    for name in names:
        # Clear the iteration subpool.
        svn.core.svn_pool_clear(subpool)

        # Calculate the entry's full path.
        full_path = directory + '/' + name

        # If the entry is a directory, recurse.  The recursion will return
        # a list with the entry and all its children, which we will add to
        # our running list of paths.
        if svn.fs.svn_fs_is_dir(root, full_path, subpool):
            crawl_filesystem_dir(root, full_path, subpool)
        else:
            # Else it's a file, so print its path here.
            print full_path

    # Destroy the iteration subpool.
    svn.core.svn_pool_destroy(subpool)

def crawl_youngest(pool, repos_path):
    """Open the repository at REPOS_PATH, and recursively crawl its
    youngest revision."""
    
    # Open the repository at REPOS_PATH, and get a reference to its
    # versioning filesystem.
    repos_obj = svn.repos.svn_repos_open(repos_path, pool)
    fs_obj = svn.repos.svn_repos_fs(repos_obj)

    # Query the current youngest revision.
    youngest_rev = svn.fs.svn_fs_youngest_rev(fs_obj, pool)
    
    # Open a root object representing the youngest (HEAD) revision.
    root_obj = svn.fs.svn_fs_revision_root(fs_obj, youngest_rev, pool)

    # Do the recursive crawl.
    crawl_filesystem_dir(root_obj, "", pool)
    
if __name__ == "__main__":
    # Check for sane usage.
    if len(sys.argv) != 2:
        sys.stderr.write("Usage: %s REPOS_PATH\n"
                         % (os.path.basename(sys.argv[0])))
        sys.exit(1)

    # Canonicalize (enough for Subversion, at least) the repository path.
    repos_path = os.path.normpath(sys.argv[1])
    if repos_path == '.': 
        repos_path = ''

    # Call the app-wrapper, which takes care of APR initialization/shutdown
    # and the creation and cleanup of our top-level memory pool.
    svn.core.run_app(crawl_youngest, repos_path)


This same program in C would need to deal with custom datatypes (such as those provided by the APR library) for representing the hash of entries and the list of paths, but Python has hashes (called dictionaries) and lists as built-in datatypes, and provides a rich collection of functions for operating on those types. So SWIG (with the help of some customizations in Subversion's language bindings layer) takes care of mapping those custom datatypes into the native datatypes of the target language. This provides a more intuitive interface for users of that language.

The Subversion Python bindings can be used for working copy operations, too. In the previous section of this chapter, we mentioned the libsvn_client interface, and how it exists for the sole purpose of simplifying the process of writing a Subversion client. The following is a brief example of how that library can be accessed via the SWIG bindings to recreate a scaled-down version of the svn status command.

例8.3 A Python Status Crawler

#!/usr/bin/env python
"""Crawl a working copy directory, printing status information."""
 
import sys
import os.path
import getopt
import svn.core, svn.client, svn.wc

def generate_status_code(status):
    """Translate a status value into a single-character status code,
    using the same logic as the Subversion command-line client."""

    if status == svn.wc.svn_wc_status_none:
        return ' '
    if status == svn.wc.svn_wc_status_normal:
        return ' '
    if status == svn.wc.svn_wc_status_added:
        return 'A'
    if status == svn.wc.svn_wc_status_missing:
        return '!'
    if status == svn.wc.svn_wc_status_incomplete:
        return '!'
    if status == svn.wc.svn_wc_status_deleted:
        return 'D'
    if status == svn.wc.svn_wc_status_replaced:
        return 'R'
    if status == svn.wc.svn_wc_status_modified:
        return 'M'
    if status == svn.wc.svn_wc_status_merged:
        return 'G'
    if status == svn.wc.svn_wc_status_conflicted:
        return 'C'
    if status == svn.wc.svn_wc_status_obstructed:
        return '~'
    if status == svn.wc.svn_wc_status_ignored:
        return 'I'
    if status == svn.wc.svn_wc_status_external:
        return 'X'
    if status == svn.wc.svn_wc_status_unversioned:
        return '?'
    return '?'

def do_status(pool, wc_path, verbose):
    # Calculate the length of the input working copy path.
    wc_path_len = len(wc_path)

    # Build a client context baton.
    ctx = svn.client.svn_client_ctx_t()

    def _status_callback(path, status, root_path_len=wc_path_len):
        """A callback function for svn_client_status."""

        # Print the path, minus the bit that overlaps with the root of
        # the status crawl
        text_status = generate_status_code(status.text_status)
        prop_status = generate_status_code(status.prop_status)
        print '%s%s  %s' % (text_status, prop_status, path[wc_path_len + 1:])
        
    # Do the status crawl, using _status_callback() as our callback function.
    svn.client.svn_client_status(wc_path, None, _status_callback,
                                 1, verbose, 0, 0, ctx, pool)

def usage_and_exit(errorcode):
    """Print usage message, and exit with ERRORCODE."""
    stream = errorcode and sys.stderr or sys.stdout
    stream.write("""Usage: %s OPTIONS WC-PATH
Options:
  --help, -h    : Show this usage message
  --verbose, -v : Show all statuses, even uninteresting ones
""" % (os.path.basename(sys.argv[0])))
    sys.exit(errorcode)
    
if __name__ == '__main__':
    # Parse command-line options.
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hv", ["help", "verbose"])
    except getopt.GetoptError:
        usage_and_exit(1)
    verbose = 0
    for opt, arg in opts:
        if opt in ("-h", "--help"):
            usage_and_exit(0)
        if opt in ("-v", "--verbose"):
            verbose = 1
    if len(args) != 1:
        usage_and_exit(2)
            
    # Canonicalize (enough for Subversion, at least) the working copy path.
    wc_path = os.path.normpath(args[0])
    if wc_path == '.': 
        wc_path = ''

    # Call the app-wrapper, which takes care of APR initialization/shutdown
    # and the creation and cleanup of our top-level memory pool.
    svn.core.run_app(do_status, wc_path, verbose)


現時点で、これが SubversionのPython連携であり、それはほとんど完成 されたものです。Java連携についても少し触れておきます。SWIGインターフェース ファイルが正しく設定されれば、すべてのSWIG対応言語(現在のところ、 C#, Guile, Java, MzScheme, OCaml, Perl, PHP, Python, Ruby, そして Tclですが)の 特定のラッパを生成するのは理論的には簡単なことです。しかし、SWIGが インターフェースするのに必要になる複雑なAPI に対しては、もう少し追加のコーディング が必要となります。SWIG自身のより詳しい情報は http://www.swig.orgにあるプロジェクト のウェブサイトをみてください。

Subversion の言語連携は不幸にも Subversion のコアモジュールほど注目 されていません。しかし Python, Perl, そして Ruby 用の関数連携を作る ためにかなり努力されてきました。ある程度の範囲で、このような拳固に 対して容易された SWIG インターフェースファイルは SWIG でサポートされて いるへ科の言語の連携を生成するために再利用することができます。 (このような言語には C#、Guilde, Java, Mzscheme, OCaml, PHP, Tcl その他があります)。 それでも SWIG を汎用的に利用するのに必要な複雑な API のために ある程度特殊なプログラミングが必要にはなります。SWIG 自身の詳細に ついてはhttp://www.swig.org/にある プロジェクトウェブサイトを参照してください。




[43] Subversion はANSI システムコールとデータ型をできる限り 利用しています。

[44] Neon と Berkeley DB はそのようなライブラリの例です。