Personal tools
You are here: Home ブログ iwanaga Categories rhino
« December 2010 »
Su Mo Tu We Th Fr Sa
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
Recent comments
Re:javascriptのthisについて inoue 2009-11-13
Re:Rhino Code Reading #0x04 iwanaga 2008-11-26
Re:Rhino Code Reading #0x04 inoue 2008-11-26
 
Document Actions

rhino

Up one level

Document Actions

Rhino Code Reading #0x01

皆さん始めまして、2008年11月からアリエルの育成プログラムに応募して入社した岩永といいます。

今後よろしくお願いします。

今回、育成プログラムの一環としてRhinoのコードリーディングの成果をここで綴って行くことになりました。

目標は週二回以上の更新らしいです。がんばります。


私がコードリーディングする環境は以下の通りです。

  • Windows XP Home Edition SP3
  • Eclipse3.2.0 (All-In-One-Eclipse 3.0.1)
  • JDK 5.0u16
  • Rhinoの最新ソースコード (執筆段階での最新リリースはRhino 1.7R1)

別途何か必要になった場合はその都度記述します。


さて、コードリーディングを行うからにはソースコードが無くては話になりませんね。

今回は、まずコードリーディングの為の初めの第一歩としてRhinoのソースコードを取ってきたいと思います。

Rhinoの最新のソースコードはmozillaが管理しているCVSリポジトリから取得することが可能です。

CVSの他にMercurialとよばれる分散型バージョンコントロールソフトウェアを使用して、

ソースコードを取得することも出来るようですが、機会があればそちらも紹介できればと思います。


eclipseなどのGUIクライアントからリポジトリサーバーに接続する場合は以下の設定のようです。

* ホスト    : cvs-mirror.mozilla.org
* リポジトリパス: /cvsroot
* ユーザ    : anonymous
* パスワード  : anonymous
* 接続方法   : pserver
* ポート    : デフォルト (2401)


CUIから接続する場合の「cvsroot」(リポジトリを識別する文字列)は以下の通りです。

:pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot


設定が終わったら mozilla/js/rhino の中身をまとめてチェックアウトします。

私の環境からはeclipseのCVSプラグインを利用して無事チェックアウトすることができました。

次回は取得したソースコードをコンパイルした後、実際にRhinoを動かしてみたいと思います。

Category(s)
rhino
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/iwanaga/rhino-code-reading01/tbping

Rhino Code Reading #0x02

Rhinoのコードリーディング 0x02回目です。

アリエル社内で上映された「死者の盆踊りエドワード・D・ウッド Jr.」を見て、

心、打ち砕かれるのにも負けず、頑張って行きたいと思います。


さて、前回のエントリではRhinoのソースコードのCVS上から取得しましたが、

このままではまだ依存するライブラリが足りないため、エラーが出ているのでこれを解決しましょう。

以下のコマンドRhinoのルートフォルダで実行してください。

ant compile-all

コマンドが正常に実行されると、ネットから取得したjarファイルがlibフォルダに追加されますので、

それらをクラスパスに追加してください。

プロキシを経由して取得したい場合は以下の一行編集して、bulid.xmlのpropertiesのtargetに追加します。

<setproxy proxyhost="proxy.host.name" proxyport="1234" />

また「ant dev-javadoc」を実行することでコードリーディングをする時に便利な開発者向けのjavadocが生成することができます。

その他の主なant tagetの説明は「ant help」を参照してください。


折角コンパイルまで行なったので、簡単なコード書いて実行させてみましょう。

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;

public class EvalString {
public static void main(String args[])
{
// (1) Contextの作成
Context cx = Context.enter();
try {
// (2) グローバルスコープオブジェクトの取得
Scriptable scope = cx.initStandardObjects();

// (3) JavaScriptのコード文字列をevaluate(評価)する
Object result = cx.evaluateString(scope, "1 + 1", "<cmd>", 1, null);

// (4) 実行結果を標準出力に書きこむ
System.out.println(Context.toString(result));
} finally {
// (5) Contextの後始末。
Context.exit();
}
}
}

上記のコードを実行すると標準出力に2と出力されます。


コードの解説をしていきます。

(1)はContextとよばれるスクリプトの実行環境などを保持するクラスを作成します。

Contextはthread単位で作成されます。


(2)はContextからグローバルスコープを表すオブジェクトを取得する関数です。

JavaScriptではスコープもオブジェクトの一つとして扱い、

ScriptableはJavaScriptにおけるオブジェクトを表わすクラスが実装しなければならないインターフェイスです。

以下はScriptableを実装したクラスの一覧です。

BaseFunction, Delegator, FunctionObject, IdFunctionObject, IdScriptableObject, ImporterTopLevel, NativeArray, 
NativeCall, NativeContinuation, NativeFunction, NativeGenerator, NativeIterator, NativeJavaArray, NativeJavaClass,
NativeJavaConstructor, NativeJavaMethod, NativeJavaObject, NativeJavaPackage, NativeJavaTopPackage, NativeObject,
NativeRegExp, NativeWith, ScriptableObject, Synchronizer, XMLObject

(3)は文字列をevaluate(評価)してその結果を返しています。

evaluateStringメソッドのパラメータは以下の通りです。

  • 第1引数:評価を行なうスコープ
  • 第2引数:評価するJavaScriptのソースコードの文字列
  • 第3引数:ソースの名前
  • 第4引数:開始行番号
  • 第5引数:セキュリティドメイン
  • 戻り値:評価結果(Object型)


第3引数と第4引数は実際にJavaScriptとしての処理に使われるのではなく、デバッグやエラーメッセージの作成などの用途で使われます。

第5引数のセキユリティドメインはRhinoのセキュリティ関連の説明に上げられていましたが、まだよくわかっていないので詳しくは別のエントリで書くことにします。

すぐに知りたい方は Rhino Overview - MDC などを参考にして私に教えてあげると良いと思います。


また、メソッドの戻り値の型がObject型なのは、返す値が以下のいずれかによるためです。

  • java.lang.Boolean
  • java.lang.String
  • java.lang.Number
  • org.mozilla.javascript.Scriptable
  • null
  • Context.getUndefinedValue()
  • Scriptable.NOT_FOUND

(4)は実行結果を文字列に変換して標準出力に書きこみます。

(5)はContextの後始末です。

Context.enter()は複数回実行できますが、実行した回数だけContext.exit()を実行する必要があるようです。


というわけで簡単にでしたがソースコードの解説は終了です。

次回はその時の進行にもよりますが、今後のコードリーディングをするにあたっての戦略的な話や、

もしくはRhinoの主要クラスやパッケージ構成の話が出来たら楽しそうだなと考えています。


Category(s)
rhino
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/iwanaga/rhino-code-reading02/tbping

Rhino Code Reading #0x03

入社二週間目にして社内のネットワーク環境全域に障害を起した岩永ですごめんなさい。

Rhinoのコードリーディング 0x03回目です。

前回はRhinoを用いて、"1+1"というソースコードを読み込み、2を出力するプログラムを作成しました。

今回は文字列を評価してその結果を返す関数「evaluateString」が、内部で行なっていることを疑似コードを用いて解説したいと思います。

私も今回のアリエル育成プログラムで初めて学ぶ分野なため、情報として不正確な内容が含まれているかもしれません。

もし何かお気付きの際は情け容赦ある突っ込みをお待ちしております。


それでは初めます。


public class Context
{
public final Object evaluateString(Scriptable scope, String source,
String sourceName, int lineno,
Object securityDomain)
{
// (1) CompilerEnvironsクラスの初期化
CompilerEnvirons compilerEnv = new CompilerEnvirons();
compilerEnv.initFromContext(this);
// (2) ソースコードの解析
Parser p = new Parser(compilerEnv);
AstRoot ast = p.parse(sourceString, sourceName, lineno);
// (3) 中間コードへの変換
IRFactory irf = new IRFactory(compilerEnv);
ScriptNode tree = irf.transformTree(ast);
// (4) Javaバイトコードの生成
Evaluator compiler = new Codegen();
Object bytecode = compiler.compile(compilerEnv, tree);
// (5) スクリプトオブジェクトの生成・実行
Script = compiler.createScriptObject(bytecode, securityDomain);
return script.exec(this, scope);
}
}

上記がevaluateStringの疑似コードです。

ソースコードを(1)〜(5)までの5項目に分け、順番に解説していきたいと思います。


(1) CompilerEnvironsクラスの初期化

CompilerEnvironsクラスのインスタンスを生成・初期化を行ないます。

CompilerEnvironsクラスはコンパイルの環境情報を保持するためのクラスであり、

保持する項目としては、「JavaScriptのバージョン」・「コンパイル時の最適化レベル」・「警告をエラーとして扱うか」などが上げられます。


(2) ソースコードの解析

ソースコードの解析を行ない抽象構文木(Abstract Syntax Tree、AST)と呼ばれる、「プログラムの構造を表現するための木構造のデータ」を作成します。

Parser.parseが戻り値として返すAstRootクラスは、抽象構文木のルート要素となるクラスです。

Parser.parseの内部では、字句解析(Lexical Analysis)や構文解析(Syntactic Analysis)と呼ばれる処理が行なわれます。

字句解析・構文解析とはそれぞれ、以下のような処理のことを指します。

  • 字句解析 - 文字列(ソースコード)を「value(名前)」・「11(リテラル)」・「*(演算子)」などの基本要素(トークン)に分解する処理
  • 構文解析 - トークン列から構文木を作成する処理


(3) 中間コードへの変換

(2)で作成した抽象構文木を、中間コードもしくは中間表現(Intermediate Representation)と呼ばれるとソースコードとJavaバイトコードの中間にあたるデータに変換します。

中間コードは抽象構文木と同様の木構造のデータであり、JavadocではIR Tree(Intermediate Representation Tree)と表現がされています。

この抽象構文木から中間コードへの変換の過程には簡単な最適化も行なわれ、"1+1"のような式は数値リテラルの"2"に変換されます。


(4) Javaバイトコードの作成

JVM上で動作させる為のJavaバイトコードを作成します。

Codegen(コンパイラ)クラスのcompileメソッドは、中間コードからJavaバイトコードに翻訳(コンパイル)するメソッドです。

Evaluator型はコンパイラとインタプリンタを抽象化したインターフェイスで、Codegenクラスで実装しています。

疑似コードではCodegenクラスのインスタンスを直接作成していますが、

本来はcreateCompilerという、Evaluator型のオブジェクトを取得するFactory Methodを呼び出します。


(5) Scriptの作成・実行

Scriptオブジェクトを作成、実行します。

Scriptは、スクリプトをJava上から実行するためのexecメソッドを持つインターフェイスです。

Codegen.createScriptObjectはJavaバイトコードを入力にScript型のオブジェクトを返しますが、

実際の型は、CalssLoaderを使用して新たに定義された、Scriptインターフェイスを実装する独自クラスです。

作成されたScriptの実行結果は、evaluateStringの戻り値として呼び出し元へと返されます。


まとめ

evaluateStringを一通り追って、一連の実装流れと、バックグラウンドとなる概念の触りを学習しました。

evaluateStringには「ソースコードの解析・コンパイル・実行」とソースコードをプログラムとして動作させるの過程が詰め込まれています。

その為、evaluateStringを理解することはコンパイラやインタプリタが行なっていることの理解へと繋がり、プログラムに対する理解を深める良い教材になりそうです。



次回に何を書くかは今の所未定です。

Category(s)
rhino
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/iwanaga/rhino-code-reading03/tbping

Rhino Code Reading #0x04

日毎に寒さが加わり、心細くなってまいりましたがRhinoコードリーディング 0x04回目です。

今回はParserクラスを中心とした、ソースコード解析について学んだことの覚え書きのような話をします。


前回もお話ししましたが、Parserクラスはソースコードを解析して抽象構文木を作成するクラスです。

例えば"1+2"をparseした場合、以下のような抽象構文木が作成されます。

- AstRoot(root要素)
- ExpressionStatement(式文)
- InfixExpression(二項演算式) @type = ADD(+)
- NumberLiteral(数値リテラル) @number = 1.0 # Left
- NumberLiteral(数値リテラル) @number = 2.0 # Right

これはコンパイラやインタプリタでは一般的な処理ですが、

以前より、yaccやJavaCCなどの構文解析器作成ツールを使用した解析が主流です。

しかしRhinoでは、それらを一切使わなず手書きで構文解析器を作成しています。


構文解析には「再帰下降法」と呼ばれる解析手法を使います。

再帰下降法とはトークンに対応した表を作らず、

「トークンAの後にトークンBがきたらこんな処理」と直接関数を作っていく解析方法で、

自動生成が難しい反面、構文規則通りに書けばいいので手書のパーサーを作る際によく使われます。


このあたりは口で説明するより、実際に処理をみたほうが速いと思うので前回と同様、疑似コードをのせてみます。

public class Parser 
{
public AstRoot parse(String sourceString, String sourceURI)
{
this.ts = new TokenStream(this, sourceString);
AstRoot root = new AstRoot();
currentScope = currentScriptOrFn = root;
for (;;) {
int token = ts.getToken(); // 現在のトークンを取得
if (token <= Token.EOF) {
break;
}
AstNode n;
if (token == Token.FUNCTION) {
// 関数の解析
n = function();
} else {
// 文の解析
n = statement();
}
root.addChildToBack(n);
n.setParent(root);
}

return root;
}

private FunctionNode function()
{
// 現在のトークンを取得
int token = ts.getToken();

Name name = null;
if (token == Token.NAME) {
// 関数名の取得
name = createNameNode(true, Token.NAME);
} else {
// 無名関数 λ...
}
FunctionNode fnNode = new FunctionNode(name);
// 引数の解析
parseFunctionParams(fnNode);
// bodyの解析
fnNode.setBody(parseFunctionBody());

return fnNode;
}

private AstNode statement() {
int token = ts.getToken();
switch (token) {
case Token.IF: // if文
return ifStatement();

case Token.SWITCH: // switch文
return switchStatement();

case Token.WHILE: // while文
return whileLoop();

case Token.FOR: // for文
return forLoop();

case Token.FUNCTION: // 関数
return function();

// ...略

default: // それ意外の文(式文)
return new ExpressionStatement(expr(), !insideFunction());
}
}

// その他各構文の解析
}

解説の為にかなりの部分の処理を省略しましたが、大体の流れは実際のコードでも同様です。

前回のエントリーで「構文解析の前にはソースコードからトークンを解析する「字句解析」の処理を行なう必要がある」と書きましたが、

その責務を受けもっているのがTokenStreamクラスです。

TokenStreamクラスのgetTokenメソッドを呼ぶと、ソースコードから字句の解析を行ない、

現在カーソルが指し示すトークンを一つ取得した後にカーソルを一つ進めます。

getTokenメソッドが呼ばれる度に、字句解析の処理を行なうため、

字句解析と構文解析は処理フェーズとして明確に分けられていません。


また、コンパイラによっては「意味解析」と呼ばれるフェーズが存在します。

これは「プログラムの構成要素どうしが、意味的に正しいつながりをもっているかどうかを検査する」フェーズですが、

Rhinoではこれをやっていないというのが、現在での見解です。

Rhinoでも、構文解析時にはシンボルテーブルの作成を、

同じく中間コードの作成時には関数テーブルの作成を行なっているのですが、

その際に意味的な妥当性の検査を行なっていない為です。(意味的におかしいコードは実行時例外になったりする)


さて、このあたりで構文解析の話は一旦終了です。

最後に学習のメモ代りに使用したマインドマップを載せて置きます。

何か他の方の学習の肥やしになれば幸いですが、

内容の正誤は保証できませんのであくまで参考程度に留めておいてください。


マインドマップメモ 構文解析
Category(s)
rhino
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/iwanaga/rhino-code-reading04/tbping

Re:Rhino Code Reading #0x04

Posted by inoue at 2008-11-26 02:17
> InfixExpression(代入式)

これはtypoですね。

岩永さんから話は聞いていましたが、確かにRhinoのソースコードが11月にかなり変わっています。cvsのログを見ると Landing Steve Yegge's new Abstract Syntax Tree (AST) API changes. とあるので、ejacsと連動した動きかもしれません。

Re:Rhino Code Reading #0x04

Posted by iwanaga at 2008-11-26 18:14
ありがとうございます。
代入式ではなくて、二項演算式でした。
気になったのでASTの変更について調べていたら、RhinoのGoogle GroupでI'm working on landing Steve Yegge's changes to the parser for a
rational AST.という記述がありました。
直接の切っ掛けとなったディスカッションではないのでこれで確信とはいえませんが。

■引用元
JavaScript 1.8 support in Rhino - mozilla.dev.tech.js-engine.rhino | Google グループ
http://groups.google.com/group/mozilla.dev.tech.js-engine.rhino/browse_thread/thread/baacba33aabb8d27/b5e656ef3fcf84ef?lnk=gst&q=AST#b5e656ef3fcf84ef

Rhino Code Reading #0x05

12月です。

気温の変化よりも月日の流れる速さに寒気を感じえない今日この頃ですがRhino Code Reading #0x05です。

前回はParserクラスを中心に、ソースコードから抽象構文木を作成する話をしましたが、今回は抽象構文木と中間コードについてす。


まずは抽象構文木について。

以前に書きましたが、抽象構文木とは「プログラムの構造を表現した木構造のデータ」です。

その木構造の一つ一つはAstRootを祖先としたノードクラス達であり、具体的には以下のようなクラス階層をしています。

- AstNode
- ArrayLiteral
- Block
- CatchClause
- Comment
- ConditionalExpression # 条件文 (test ? true : false)
- ElementGet
- EmptyExpression
- ErrorNode
- ExpressionStatement # 式文
- FunctionCall # 関数呼出
- NewExpression
- IfStatement
- InfixExpression # 二項演算式
- Assignment # 代入文
- ObjectProperty
- PropertyGet
- XmlDotQuery
- XmlMemberGet
- Jump
- BreakStatement
- ContinueStatement
- Label
- Scope # スコープ
- ArrayComprehension
- LetNode
- Loop
- DoLoop
- ForInLoop # foreach
- ArrayComprehensionLoop
- ForLoop # for
- WhileLoop
- ScriptNode
- AstRoot # ルートノード
- FunctionNode # 関数ノード
- SwitchStatement
- KeywordLiteral
- LabeledStatement
- Name # 変数名 or 関数名
- NumberLiteral
- ObjectLiteral
- ParenthesizedExpression # 丸括弧
- RegExpLiteral # 正規表現リテラル
- ReturnStatement
- StringLiteral
- SwitchCase
- ThrowStatement
- TryStatement
- UnaryExpression # 単項演算式 i++, i--
- VariableDeclaration # 変数宣言
- VariableInitializer # 変数初期化
- WithStatement
- XmlFragment
- XmlExpression
- XmlString
- XmlLiteral
- XmlRef
- XmlElemRef
- XmlPropRef
- Yield

これらのクラスは全て、org.mozilla.javascript.astパッケージに含まれています。

このパッケージは11月に新たに追加されたパッケージであり、現在の最新リリースであるRhino 1.7R1には存在しません。

パッケージが追加される以前では、どうやら抽象構文木を作らず、直接中間コードに変換を行なっていたようです。


また、他に気になった点としてはYieldノードです。

これは私がJavaScriptに不慣れなせいもありますが、初めはRubyのyieldと同じようなものかと思いましたが、実際は違っていました。

答えは下記のサイトを参照。

[javascript] JavaScript 1.7 の yield が凄すぎる件について - IT戦記


今回のコードリーディングによって、JavaScriptでコルーチンのサポートしていたことを知ることになりました。


各ノードには次のノードを指し示すフィールドが用意されています。

InfixExpressionならleftとright、FunctionNodeならtargetといった具合保持するフィールドは各ノードの種類によって異なります。

これらのノードは全て、プログラム的に意味のある構造だけを抽出した抽象的なデータである為に抽象構文木と呼びます。

逆にソースコードの構造をそのままの形で表現した木構造データを構文木(Syntax Tree)と呼ぶそうです。


次に中間コードについてです。

中間コードもまた抽象構文木と同様に木構造のデータであり、

その要素も抽象構文木と同様AstNodeを祖先とするノードの集合によって形成されています。

抽象構文木との違いは、中間コードのほうがよりバイトコードへと変換しやすい形式へと変換されている点です。

具体的には抽象構文木から中間コードの変換として、下記のような処理が行なわれています。

  • FunctionNodeは親スコープのAstRootもしくはFunctionNodeのfunctionsフィールドに格納する
  • 冗長な部分を最適化を行なう。例えば数値リテラル同士の演算は演算結果にに変換する
  • for文やwhile文などを、GOTO等を用いた等価のコードに変換する
  • Etc.


これらの処理によって、抽象構文木は中間コードへと変換されます。

またオプションとして、toStringが呼びだされた際に使用するソース情報の付加も行ないます。


抽象構文木より作成された中間コードはコンパイラへと渡され、Javaバイトコードを作成するのに使用されます。

Category(s)
rhino
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/iwanaga/rhino-code-reading05/tbping

Rhinoを組み込む為のイントロダクション

Rhino Code Readingの更新が滞ってしまってから久しいですが、
最近Rhinoの組込みを実践してみたのでそれを基にtipsに近いイントロダクションを書きます。

1 組込む

1.1 最も簡単な組み込み

public class EvalString {
    public static void main(String args[])
    {
        // 1. Contextの作成
        // Context cx = Context.enter(); は非推奨?
        ContextFactory contextFactory = new ContextFactory();
        Context cx = contextFactory.enterContext();
        try {
            // 2. グローバルスコープオブジェクトの取得
            Scriptable globalScope = cx.initStandardObjects();
            // 3. 文字列をeval(評価)する。
            Object result = cx.evaluateString(scope, "1 + 1", "<cmd>", 1, null);
            // 4. 実行結果を標準出力に書きこむ
            System.out.println(Context.toString(result));
        } finally {
            // 5. Contextの終了
            Context.exit();
        }
    }
}

Rhinoの組み込みで最も簡単な組み込み例です。

ContextをContextFactoryから作成している点を除けば、
Rhino Code Reading #0x02に載せたコードとだいたい同じですので説明はそちらを参照してください。

1.2 複数回コードを実行する

複数回コードを実行するだけならevaluateStringを複数回実行すれば良いですが、それでは定義された変数などの状態が保持され続てしまうのでShellのようなインタラクティブな用途以外では向きません。

cx.evaluateString(globalScope, "var i = 1;", "<cmd>", 1, null);
for (int i = 0; i < 10; i++) {
    Object result = cx.evaluateString(globalScope, "i += 1; i;", "<cmd>", 1, null);
    System.out.print(Context.toString(result) + " ");
}
// result: 2 3 4 5 6 7 8 9 10 11


JavaScriptの状態はglobaScopeが保持しています。
しかし、globalScopeを作成するのに使用したContext.initStandardObjectsメソッドはコストの高いメソッドです。
そこでContext.newObjectで新たなオブジェクトを作成し、それをローカルスコープとして使用します。
Context.newObjectで作成されたオブジェクトはプロパティを一切持たない空のオブジェクトですが、
プロトタイプを用いることでglobalScopeの環境を共有しつつローカルスコープとして利用することができます。
これはマルチスレッド間でスコープを共有したい時にも同じ方法が有効です。

cx.evaluateString(globalScope, "var i = 1;", "<cmd>", 1, null);
for (int i = 0; i < 10; i++) {
    // ローカルスコープの作成
    Scriptable localScope = cx.newObject(globalScope);
    localScope.setPrototype(globalScope);
    localScope.setParentScope(null);

    Object result = cx.evaluateString(localScope, "i += 1; i;", "<cmd>", 1, null);
    System.out.print(Context.toString(result) + " ");
}
// result: 2 2 2 2 2 2 2 2 2 2


同じコードを何回も実行するのであれば、事前にコンパイルを行ってから実行すると良いでしょう。

// スクリプトをコンパイル
Script script = cx.compileString("i += 1; i;", "<cmd>", 1, null);
for (int i = 0; i < 10; i++) {
    ...

    // コンパイルしたスクリプトの実行
    Object result = script.exec(cx, localScope);
    System.out.print(Context.toString(result) + " ");
}


2 拡張する

2.1 Javaオブジェクトの追加

Javaのオブジェクトを追加するにはContext.javaToJSでラッパーオブジェクトを作成し、
ScriptableObject.putPropertyでJavaScriptオブジェクトをプロパティに追加します。

Object wrappedOut = Context.javaToJS(System.out, globalScope);
ScriptableObject.putProperty(globalScope, "out", wrappedOut);
cx.evaluateString(globalScope, "out.println(42);", "<cmd>", 1, null);


2.2 メソッドの追加

組込みのRhino Shellのように独自のグローバルメソッドを定義したい時があります。
そういった時に利用されるのがScriptableObject.defineFunctionProperties()です。
追加できるメソッドは二種類の形式が許可されています。
一つは、引数を持たない、もしくは1個以上の引数(boolean, int, double, Object, String, Scriptableのいずれかの型)を持ち、引数と同じいずれかの型かvoid型の値を返すメンバメソッド。
もう一つは、(Context cx, Scriptable thisObj, Object[] args, Function funObj)形式で、可変長引数を受けとり、Object型かvoid型の値を返すstaticメソッドです。

public static void main(String args[]) {
    ContextFactory contextFactory = new ContextFactory();
    Context cx = contextFactory.enterContext();
    try {
        Scriptableobject globalScope = cx.initStandardObjects();

        // メソッドの追加
        String[] funcNames = { "sum", "print" };
        globalScope.defineFunctionProperties(funcNames, Global.class, ScriptableObject.DONTENUM);

        cx.evaluateString(globalScope, "print(sum(1,2), \"hello\");", "<cmd>", 1, null);
    } finally {
        Context.exit();
    }
    // result: 3 hello
}

// 一般的な形式
public int sum(int x, int y) {
    return x + y;
}

// 可変長引数形式
public static Object print(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
    for (int i = 0; i < args.length; i++) {
        if (i > 0) {
            System.out.print(" ");
        }
        System.out.print(Context.toString(args[i]));
    }
    System.out.println();
    return Context.getUndefinedValue();
}


2.3 ホストオブジェクト

Nativeオブジェクト以外の組込みオブジェクトのことを指します。
Rhinoでは特定の規約にそったJavaクラスを作成することで、独自のホストオブジェクトを組込むことが出来ます。

public class CustomHostObject extends ScriptableObject {
    private int count;
    public static void main(String args[]) throws Exception {
        ContextFactory contextFactory = new ContextFactory();
        Context cx = contextFactory.enterContext();
        try {
            Scriptable globalScope = cx.initStandardObjects();
            // ホストオブジェクトの登録
            ScriptableObject.defineClass(globalScope, CustomHostObject.class);

            Object result = cx.evaluateString(globalScope, "var c = new Custom(3); c.count; c.count;", "<cmd>", 1, null);
            System.out.println(Context.toString(result));
        } finally {
            Context.exit();
        }
        // result: 4
    }

    // 引数のないコンストラクタは: Rhinoランタイムによって、インスタンスの作成に使用される。
    public CustomHostObject() {}

    // JavaScriptコンストラクタ: jsConstructorメソッドによって定義。
    public void jsConstructor(int a) { count = a; }

    // クラス名: getClassNameメソッドによって定義。コンストラクター名を決定するのに使用。
    public String getClassName() { return "Custom"; }

    // 動的プロパティ: 「jsGet_ / jsSet_ プロパティ」ではじまるメソッド定義。
    public int jsGet_count() { return count++; }

    // JavaScriptメソッド: 「jsFunction_メソッド名」ではじまるメソッド定義。
    public void jsFunction_resetCount() { count = 0; }
}


3 更に組込む

3.1 デバッガ

Debuggerインターフェイスを実装したクラスを用意しても構いませんが、
Rhino組込みのデバッガを利用することで簡単且つ高機能なRhinoデバッガを利用することが出来ます。

ContextFactory contextFactory = new ContextFactory();
Main debugger = new org.mozilla.javascript.tools.debugger.Main("debug window");
debugger.attachTo(contextFactory);
debugger.pack();
debugger.setSize(600, 460);
debugger.setVisible(true);

org.mozilla.javascript.tools.debugger.Mainが含まれていない場合は
ant compile-debuggerで作成したjarを使用すると良いようです。

3.2 セキュリティ

スクリプトから呼び出されたくないクラスはClassShutterを使用すればクラス名からクラスの呼び出しを遮断することが出来ます。
不特定多数のユーザーがスクリプトを実行できる様なアプリケーションの場合などで有効です。

// ClassShutterの登録
cx.setClassShutter(new ClassShutter() {
    // 可視判定メソッド
    public boolean visibleToScripts(String fullClassName) {
        if(fullClassName.indexOf("java.lang.System") != -1) {
            System.err.println("unvisible class : " + fullClassName);
            return false;
        }
        return true;
    }
});

ScriptableObject globalScope = cx.initStandardObjects();
cx.evaluateString(globalScope, "java.lang.System.out.println(3);", "<cmd>", 1, null);

// result :
// unvisible class : java.lang.System
// Exception in thread "main" org.mozilla.javascript.EcmaError:
// TypeError: Cannot call property println in object [JavaPackage java.lang.System.out].
// It is not a function, it is "object". (<cmd>#1)
// ...

3.3 書けなかったこと

SecurityController / PolicySecurityController - 電子署名ベースのセキュリティポリシー。難しい・・・。


3.4 最近知ったこと

JavaScript1.7でlet 文/式/宣言がサポートされていた事。


3.5 参考サイト

組込みはエントリを書くのに参考にさせて頂いたサイトです。

Rhino - MDC
https://developer.mozilla.org/ja/Rhino

ディスカッション - mozilla.dev.tech.js-engine.rhino | Google グループ
http://groups.google.com/group/mozilla.dev.tech.js-engine.rhino/topics

Rhinoに関する覚え書き
http://homepage.mac.com/takaho_e/computer/rhino/rhino.html

Helma Javascript Web Application Framework (Rhinoを利用したオープンソースのプロジェクト)
http://dev.helma.org/

a geek (Rhinoに関する複数のエントリー有り)
http://d.hatena.ne.jp/hiratara/


3.6 一番大事なこと

えんじょい。

Category(s)
rhino
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/iwanaga/rhino-intro/tbping

javascriptでthisと戯れる

最近のありえるはGoやったり、Closureやったりかっこ良いですね。

このあたりは社内の自称二十代の勉強会で教えて貰えることを期待しつつ、

新人さん向けに開催したjavascript勉強会の小ネタとして、

javascriptのthisについての話をしたのでその内容を載せてみます。

javascriptが本業の方などからしてみれば自明な内容かもしれませんが、

java等から来た人間からすると、わりと嵌りがちな部分です。


まずはグローバルスコープでのthisについて。

グローバルスコープでのthisはグローバルオブジェクトとよばれるオブジェクトを参照しています。

print(this); // [object global] thisはグローバルオブジェクトを参照

グローバルオブジェクトとはグローバル変数を格納するトップレベルのスコープオブジェクト。

IEなんかで言うところのwindowオブジェクトがそれにあたります。


次にメソッド内でのthisについて。

obj.func()の形式で呼び出されたメソッドの内部ではthisはobjを参照しています。

var obj = {
func: function() {
print(this); // [object Object]
print(this.func != undefined) // true
}
};
obj.func();


javaと違ってjavascriptでは自身のプロパティを参照するのにthisは省略できません。

省略すると意味が変ってしまい、現在のスコープオブジェクトへの参照になってしまいます。

var obj = {
func2: function() {
print(this); // [object Object]
print(this.func1 != undefined) // true
},

func1: function() {
this.func2(); // thisの省略は不可
}
};
obj.func2();


関数内で定義したローカルな関数を呼び出すとその内部ではthisの値が変ってしまいます。

これについては後述します。

ローカル関数内で宣言元のプロパティを参照する方法の一つとして、

ローカル関数外でthisを適当な変数に代入した後、ローカル関数内でその変数を参照する方法があります。

var obj = {
func2: function() {
print(this); // [object Object]
},

func1: function() {
var self = this; // selfにthisの値(obj)を代入
var local = function() {
self.func2(); // self(obj)のfunc2を呼び出し
};
local();
}
};
obj.func1();

このあたりからjavaやC++のthisと挙動が違ってきましたね。

javascriptの場合、thisの値はどのように決まるのでしょうか?


答えはFunction.applyの第一引数によって決まります。

var obj = {
func: function() {
print(this);
}
};

// 以下の二行は等価
obj.func(); // [object Object]
obj.func.apply(obj); // [object Object]

Function.applyは第一引数にthisの参照先を、

第二引数に関数の引数の配列(今回はいらないので省略)を受けとる関数です。

そしてobj.func()は言わばobj.func.apply(obj)のシンタックスシュガーと言えます。


応用するとこんなこともできます。

var obj = {
func: function() {
print(this); // [object global]
}
};
obj.func.apply(this); // applyの第一引数にグローバルオブジェクトを指定

obj.func.applyの第一引数にグローバルオブジェクトを指定しており、

その結果objのfunc内のthisはグローバルオブジェクトになります。


ではメソッドとしてではなく、関数として呼び出された場合はどうなるのでしょうか?

var func = function() {
print(this); // [object global]
};
func();

グローバルスコープに定義されたfuncを呼び出した場合のthisはグローバルスコープ。

ここはなんとなくOKですね。


var obj = {
func: function() {
var local = function() {
print(this); // [object Object]
};
local.apply(this); // this == obj
}
};
obj.func();

func内で定義されたlocalのapplyにthis(obj)を指定すると、local内のthisはobj。

これもまぁOKです。


var obj = {
func: function() {
var local = function() {
print(this); // [object global]
};
local();
}
};
obj.func();

え、グローバルオブジェクト?

thisはてっきりobj.funcのCallオブジェクトかと思いましたがグローバルオブジェクトを指していました。

この動作は腑に落ちなかったのでオライリーのサイ本を読んでみると、以下の様な記述を発見。


■ O'REILLY JAPAN JavaScript 第五版 p134

関数がメソッドではなく関数として呼び出された場合、
thisキーワードはグローバルオブジェクトを参照します。
ややこしいことに、メソッドとして呼び出された関数内に入れ子にされた別の関数が
関数として呼び出された場合も、グローバルオブジェクトを参照します。

どうも仕様みたいです。


実験。

// globalFuncの宣言。
// グローバルスコープで宣言されたプロパティはグローバルオブジェクトが保持する。
var globalFunc = function() {
print(this);
};

// グローバルスコープでのthisはグローバルオブジェクトを差す。
// その為以下の二行は等価
globalFunc(); // [object global]
this.globalFunc(); // [object global]

var obj = {
func: function() {
var localFunc = function() {
print(this);
};

(function() {
// ここから先のthisはグローバルオブジェクト

globalFunc(); // [object global] スコープチェインを辿るので参照可能
localFunc(); // [object global] 同様
this.globalFunc(); // [object global] グローバルオブジェクトにglobalFuncは存在する
this.localFunc(); // エラー グローバルオブジェクトにlocalFuncは存在しない
})();
}
};
obj.func();


そしてこの動作はapplyの第一引数を省略した場合も同様です。

var obj = {
func: function() {
var local = function() {
print(this); // [object global]
};
local.apply(); // applyの第一引数を省略
}
};
obj.func();

local()がlocal.apply()のシンタックスシュガーってことか。



まとめるとこんな感じ。

  • グローバルスコープでのthisはグローバルオブジェクト
  • 関数内でのthisの参照先はFunction.applyの第一引数で、省略時はグローバルオブジェクト
  • obj.func() と obj.func.apply(obj) は等価
  • func() と func.apply() はそれぞれ等価

試した処理系はrhino 1.7 release 1。

一部の動作は処理系に依存してるかもしれません。今度試してみます。

今回はスコープやプロトタイプについての話はしてませんし、

コンストラクタでのthisの話も忘れてました。気が狂ったらその内追記します。かも。


あわせて読みたい

■JavaScriptのthisキーワードをちゃんと理解する - builder by ZDNet Japan

http://builder.japan.zdnet.com/sp/javascript-kickstart-2007/story/0,3800083428,20371112,00.htm

■JavaScriptのオブジェクト、コンストラクタ、プロトタイプ、スコープ等 - qnzm.log(クニジマログ)
http://d.hatena.ne.jp/qnzm/20081014/1223934778

■JavaScript の this について - IT戦記
http://d.hatena.ne.jp/amachang/20070917/1190015123


おしまい。

Category(s)
rhino
js
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/iwanaga/js-this/tbping

Re:javascriptのthisについて

Posted by inoue at 2009-11-13 00:41
次のようにダイナミックスコープっぽく解釈しています。個人的にはJavaScriptのthisはダイナミックスコープと同程度に邪悪だと思っています。
(以下、globalオブジェクトをわかりやすいようにwindowで代表させています)。

- x.foo()の呼び出しで、常に暗黙に this = x の代入が起きる

- apply(y)の呼び出しで、常に暗黙に this = y の代入が起きる

- x.が省略されると、常に window. があると見なされる(with文の内部では異なるので、統一的に見るなら、Webブラウザでは暗黙に with (window) {} が外側にあるイメージ)

- new(演算子だっけ?)の時、暗黙にthisに生成オブジェクトの代入が起きる

Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.