JPHP-仕組み。 創造の歴史

この記事では、 JPHPプロジェクトの歴史と、 JPHPプロジェクトが技術面からどのように開発されたかについて詳しく説明します。 このテキストは、単純なPHP開発者とコンパイラの愛好家の両方にとって興味深いものになります。 私はすべてを単純な言語で記述しようとしました。

画像

JPHPは、Java VM用のPHP言語コンパイラです。 2週間前、私はプロジェクトについての記事を書きまし 。 同様のプロジェクトは、Ruby用のJRuby、Python用のJythonです。 JPHPに関する最初の記事が公開された後、2日間でプロジェクトはgithubで500の星を獲得し、RuNetだけでなく外国のリソースでも点灯して、githubの評価で1位になりました。

そして、小さなニュースが始まる前。

最新のプロジェクトニュース




プロジェクトはすぐに別のサイトになり、JPHPを試すのは非常に簡単になり、エンジンのさまざまなコンパイルされたバージョンがレイアウトされます。 それでは、トピックに移りましょう...

プロジェクト開始


プロジェクトは自発的に始まりました。 その前に、私は同様のプロジェクトを探していました。 JVMのphp実装。 ResinのQuercusのようなプロジェクトがあります。これは、Javaで記述されたJavaコードへのトランスレータです。 特にプロジェクトの作者が、実装はZend PHP + APCと同じ速度で機能すると述べているため、この状況は私には向いていませんでした。 一定の時間まで、JVMには他のPHP実装(たとえばp8やprojectzero)がありましたが、それらはすべて死んで閉じました。

プロジェクトを開始する主な動機は、パフォーマンスとJITでした。 私はかなり長い間Javaと通信してきましたが、JVMの高いパフォーマンス、Cookie、巨大なコミュニティ、高品質のライブラリに惹かれています。 そして、少し考えて、私のアイデアが生まれました-Javaを最大限に活用し、その上にPHPエンジンを実装することです。 考えを集めて、テストバージョンの作成を開始しました。jphpが元のZend PHPよりも少なくとも2倍高速であれば、開発を続けると判断しました。

まず、よく知られているすべてのJVM言語(groovy、jruby、scala)のリポジトリを調べて、JVMバイトコードの生成に使用するライブラリのセットを見つけました。 判明したように、よく知られたサードパーティライブラリ-ASMがあります。 非常に活発に開発されており、PDFで十分なドキュメントがあり、Dalvik(Android)バイトコードもサポートしているようです(これについては以下で詳しく説明します)。

Java VMの紹介


Java Virtual Machine(JVM)は非常に強力なツールです。 ASMライブラリのドキュメントから、JVMバイトコードがどのように機能するかを確認できます。 次の段落で、VMのすべての機能を簡単に説明できます。

1.スタック仮想マシン
2.ローカル変数をインデックス(レジスタのようなもの)で保存することができます
3. GC(ガベージコレクター)はVMレベルで実装されます
4. VMレベルで実装されたオブジェクトとクラス
5.多数の標準操作-POP、PUSH、DUP、INVOKE、JMPなど。
6. Try Catch用のバイトコード命令があります。
7. VMの値には、int32、int64、float、double、object、スカラー配列、オブジェクト配列のいくつかのタイプがあります。int32はbool、short、byte、charに使用されます。

したがって、GC自体とオブジェクトクラスシステムをゼロから実装する必要がないことに気付きました。

目標と優先順位の選択


開発を始める前に、言語としてだけでなくプラットフォームとしてもPHPの主な利点を十分に理解しました。 私にとって最も明白なことは、次のことでした。



これらの利点は欠点でもあります。 各リクエストはすべてのクラスをリロードしますが、これはあまり良くありません。 問題は、部分的に解決されたバイトコードキャッシュですが、最後までではありません。 上記のメリットを維持しながら、この問題を解決できると思いました。 結果として私が得たもの:



動的型付け


Java VMレベルでは、動的型付けはありませんが、PHPでは必要です。 これは多くの人にとって大きな問題のように思えますが、そうではありません。

値を保存するために、抽象クラスMemoryを実装しました。 JPHPは値をMemoryオブジェクトとして保存します。 次に、StringMemory、NullMemory、DoubleMemory、LongMemory、TrueMemory、FalseMemory、ArrayMemory、およびObjectMemoryクラスを実装しました。 おそらくクラスの名前からわかるように、それぞれが特定のタイプ(数字、文字列など)を担当しています。 それらはすべてメモリから継承されました。

メモリは、値の操作を実装するために必要な抽象メソッドで構成されます。たとえば、プラス演算子とマイナス演算子には、各メモリクラスに実装する必要があるplus()およびminus()メソッドがあります。 これらは仮想メソッドです。 これらのメソッドは多数ありますが、これには理由があります-さまざまな最適化。 これがどのように機能するかを理解するために、擬似コードを提供します。

擬似コードの例
 $var + 20; //     //   $var    Memory $var->plus(20); //   plus      Memory $x + 20 - $y /*   */ $x->plus(20)->minus($y); 

当然、これは実際のphpコードではなく、擬似コードです。 これはすべて、内部でバイトコードで発生します。


Memoryオブジェクトは、メモリ消費の点でZend PHPのzvalオブジェクトを超えません。これが、JPHPとZend PHPのメモリ消費量がほぼ等しい理由の1つです。 多くの値(たとえば、false、true、null、小さい数値)の場合、オブジェクトはメモリにキャッシュされ、複製されません。 すべてのtrue新しいTrueMemoryオブジェクトtrue作成するのは理にかなっていますか? もちろん違います。

動的型付けとその修正の失敗


上で説明したように、すべての値はMemoryクラスのオブジェクトです。 最初に、単純な定数値のオートボクシングを実装しました。 たとえば、次の場合:

 //    $y = $x + 2; //     (  ) $y->assign( $x->plus( new LongMemory(2) ) ); 


ご覧のとおり、「2」がオブジェクトに変わりました。 プログラミングの観点からは便利でしたが、生産性の観点からは悪夢です。 このような実装は、Zend PHPよりも速く動作しませんでした。

最後に行くことにしました。 単純な値に対してこのような多数のオブジェクトが開始されるのを避けるために(そしてこのコードをループで想像しますか?)、私はplus()メソッドおよびその他の同様のメソッドを基本的なJava型(double、long、booleanなど)に実装することにしました。記憶 私は認める、それはルーチンであり、たわごとのようなにおいがしました。 私はコンパイラを再編集し、彼は型とそれらをどうするかを理解し始めました。 コンパイラは、スタック内の要素のタイプに応じて、異なるタイプと異なるメソッドを操作に置き換えることを学びました。 はい、スタックはコンパイル時に計算されます。 これらの定数値を定数のテーブルに保存することもできますが、それほど大きくはありませんが、オーバーヘッドになります。

結局のところ、私はそれを正当な理由ですべて開始し、合成テストのパフォーマンスは、ループ、変数などで、Zend PHPを2-3-4-10倍以上上回るようになりました。

変数の魔法


PHPは本当に魔法の言語です。確かにわかりませんが、実行時に文字列から名前で変数にアクセスできる言語は他にありますか? 簡単な例:

 $var = "foobar"; ${'var'} = 100500; $name = 'var'; echo $$name; 


このような魔法を実装するには、変数名テーブル-ハッシュテーブルを使用する必要があります。 JVMは、インデックスによって変数を保存する機能を提供します。もちろん、コンパイル時に変数名をインデックスに変換することはより論理的なステップであり、ハッシュテーブルよりも迅速に変数にアクセスできます。 より高速になるものと比較してください-ハッシュテーブルで検索するか、インデックスで配列にアクセスしますか?

最初はそれを忘れて、インデックスに変数を実装しました。 変数へのアクセスのパフォーマンスは最高でした。 そのような魔法を思い出したとき、私はハッシュのためにテーブルをやり直さなければなりませんでした...そして、すべては悪かったです。 変数による生産性は文字通り2〜3倍低下しました。

ためらうことなく、コンパイラーで変数のコンパイルの2つのモード(インデックスとハッシュテーブル)を実装することにしました。 アナライザーは、行の変数にアクセスする必要があるコードを識別するのに役立ちます。 これは、次の症状に従って発生します。

  1. コードに式が含まれている場合: $$var${...}
  2. 関数eval、include、require、get_defined_vars、extract、compactがあります
  3. グローバルスコープ


requireとincludeを含むコードのハッシュテーブルに変数名を保存する必要があるのはなぜですか? はい、すべてが単純です。PHPはプラグインスクリプト内の変数を渡す必要があります。これはevalと同じです。 そして、残りの関数は変数名で動作します。

非常に多くの場合、このような変数の魔法は必要ありません。つまり、JPHPでコードがより高速に動作します。 変数のグローバルスコープもあります。 ハッシュテーブルに変数を格納するメカニズムは、この領域で自動的に使用されます。 グローバル変数は、名前およびいつでも$GLOBALS配列を介してアクセスできると想定されています。

スーパー-グローバル変数
$GLOBALS, $_SERVER, $_ENV .. 、そのような変数の場合、 globalキーワードは必要ありません。 それらの実装は非常に簡単で、コンパイラはスーパーグローバル変数の名前を事前に知っており、それらが発生した場合、そのような変数にアクセスするために別のコード、擬似コードの例に置き換えます:

スーパーグローバル変数の例
 function test() { $a = $GLOBALS['a']; ... $GLOBALS['x'] = 33; } //        function test() { $GLOBALS =& getGlobalVar('GLOBALS'); //   $a->assign($GLOBALS['a']); ... $GLOBALS['x']->assign(33); } 



配列、参照、不変値、GC


PHPでは、配列はコピーされ、参照渡しではありません。 ただし、割り当て=の時点では配列のコピーは発生しません。 これにより多くのオーバーヘッドが発生します。 JPHPエンジンの内部、およびZend PHPでも、配列は参照によってコピーされますが、配列の変更時にコピーされます(配列への参照の数が1を超える場合)。

JPHPは参照カウントを使用せず、循環リンクを削除できる標準のJava GCを使用します。 これにより、このような配列を実装するときに問題が発生します。 そのため、Memory値を不変値に変換するための特別なメカニズムを実装しました。 最初に疑似コードを示します。

 $x = array(1, 2, 3); $y = $x; $y[0] = 100; //    $y ,     $x //       (-): $x->assign( array(1,2,3) ); $y->assign($x->toImmutable()); //     $x $y[0]->assign(100); //    : $y =& $x; //   ->toImmutable    $y->assign($x); 

JPHPには、別の種類のメモリオブジェクト、ReferenceMemoryがあります。これらのオブジェクトは、単に別のメモリオブジェクトへの参照を格納します。 ただし、変数は常に参照オブジェクトとして保存されるわけではありません。場合によっては、ローカル変数がそのようなリンクなしで実行でき、バイトコードを直接使用してセルに新しい値を書き込むことができます。これは通常の方法->assign().より自然に高速に動作します->assign().

参照オブジェクトはtoImmutableメソッドから実際の値を返し、配列は配列の特別な参照コピーを返します。これは、最初に変更されたときにコピーされます。 したがって、変数を別の変数へのリンクにコンパイラーに割り当てるには、 toImmutableメソッドを使用しないで十分です。

クラスと関数の実装


クラスはすでにJVMレベルで存在しています。 一般的に、Java、Scala、Groovy、JRubyは、JVMのフレームワーク内で異なるシグネチャを持つ同じクラスを生成します。 phpクラスをコンパイルするとき、JPHPは特別なシグネチャを持つJVMクラスを使用します。

 Memory methodName(Environment env, Memory[] args) 

Environmentオブジェクトは各メソッドと関数に転送されます。このオブジェクトを使用すると、メソッドが実行される環境について多くを学ぶことができます。 クラスと関数は、すべての環境で同じ方法でコンパイルされます。 メモリ[]は、メソッドに渡される引数の配列です。 PHPでは、関数が何も返さなくてもnullを返すため、メソッドは常にvoidではなく何かを返す必要があります。これはそのようなトートロジーです。

PHP関数もクラスにコンパイルされます JVMには関数のようなものはありません。 出力では、1つの静的メソッドを持つクラスを取得します。これは本質的に関数です。 関数が同じクラスのメソッドにコンパイルされないのはなぜですか? これは良い質問であり、おそらく余分なクラスを生成しないようにやり直す必要がありますが、今のところはより便利です。

JVMは、実行時にJavaクラスローダーを記述するだけで、メモリからクラスを簡単にロードできます。 したがって、JPHPは実行時にコンパイルされ、一度にすべてではなく実行時にクラスをロードできます。

ロングスタートと問題解決


このエンジンは、Java自体でクラスを作成する機能も実装しました。 ただし、すべてがそれほど単純ではありません。 そのようなクラスのすべてのメソッドには、必要な署名があり、そのようなクラスの一例として、いくつかの補助注釈(たとえば、タイプヒント用)でマークする必要があります。

php \ lang \システムクラス
 import php.runtime.Memory; import php.runtime.env.Environment; import php.runtime.lang.BaseObject; import php.runtime.reflection.ClassEntity; import static php.runtime.annotation.Reflection.*; @Name("php\\lang\\System") final public class WrapSystem extends BaseObject { public WrapSystem(Environment env, ClassEntity clazz) { super(env, clazz); } @Signature private Memory __construct(Environment env, Memory... args) { return Memory.NULL; } @Signature(@Arg("status")) public static Memory halt(Environment evn, Memory... args) { System.exit(args[0].toInteger()); return Memory.NULL; } } 


JPHPの新しいネイティブクラスの数が増えるにつれて、クラスの登録にかかる時間が長くなっていることに気付きました。 拡張機能とクラスが多ければ多いほど、エンジンが起動するまでの遅延が大きくなります。 気になりました。 そしてアイデアが浮かびました-それを修正する方法。

言語としてのPHPには遅延クラス読み込みメカニズムがあり、誰もが知っています。 ネイティブクラスを登録するために、遅延クラスロードメカニズムを使用しました。 ネイティブJavaクラスが登録されると、名前テーブルに登録されるだけで、このクラスの最初の使用時に事実上の登録が行われます。 このメカニズムを実装することで、良い結果が得られ、エンジンの初期化時間が2〜3倍に短縮され、テスト時間が24秒から13秒に短縮されました。 このため、ネイティブクラスの数は実質的にエンジンの初期化速度に影響しません。

エンジンの起動速度は、GUIアプリケーションでは特に重要です。

JVMで発生した問題


1. JVMクラスの命名。 JVMは、クラスの命名標準を実施します。 クラスへのファイルパスをバイトコードで記述すると、JVMはJavaの場合と同様にこのクラスの名前をチェックします。 これは、PSR-0標準をやや連想させます。 ただし、クラスがグローバルjvmパッケージに配置されている場合、このチェックは行われません。 PHPは1つのファイルに任意の数のクラスと関数を格納でき、任意の名前を付けることができます。 そのため、phpクラスの名前とバイトコード内のJVMクラスの名前のバインディングを解く必要がありました。 しかし、これがこの選択の唯一の理由ではありません...

2.一意のクラス名。 JPHPは、受信したバイトコードをファイルに保存し、任意の環境にロードできる必要があります。したがって、競合が発生しないように、jvmレベルのすべてのクラスに一意の名前を付ける必要があります。 jvmバイトコードのロード中にクラス名を変更することはできません。少なくとも私はまだ試していません。 現時点では、回避策として、UVIDベースのJVM +いくつかのランダムなクラス名を生成しています。 これは非常にエレガントなソリューションではないと思いますが、将来的にはより良いソリューションがあればいいのですが。 クラスが配置されているファイルの名前は使用できません。 コードがクラスに含まれていない可能性があり、バイトコードファイルがコンピューターからコンピューターに転送され、名前が変更される場合があります。

3.反射の制限。 Javaリフレクションにより、親クラスのコンテキストでメソッドを呼び出すことはできません。 super.call()ようなもの、およびphp parent:: 。 もちろん、Java 7ではinvokeDynamicが導入されました。これにより、これを行うことができますが、リフレクションよりも驚くほど遅く動作します。 invokeDynamicの拒否は、主にJava 7のパフォーマンスの低下が原因でしたが、Java 8ではこの問題は解決され、速度は同じになりました(おそらく正しく準備していないのでしょうか?)。 さらに、Java 6とAndroidへの簡単な適応をサポートしたかったので、invokeDynamicは存在しないと思われますが、リフレクションがあります。

私はこの問題をあまりエレガントに解決しませんでした.jvmクラスのメソッドを再定義するための標準メカニズムを放棄しなければならなかったため、jvmクラスのレベルで継承されたメソッドの名前は異なります-アルゴリズムmethod_name + $ + indexに従って このような解決策では問題は発生せず、発生しませんが、上記の問題は解決します。

特性の実装方法


Traitsは、バージョン5.4からPHPに導入された多重継承メカニズムです。 基本的に、コピーと貼り付けのように機能します。 JPHP実装も発生しますが、ASTツリーをコピーするのではなく、コンパイルされたバイトコードをコピーします。 もちろん、JVMには特性はありませんが、JPHPは特性を通常のJVMクラスにコンパイルし、特性の制限を制御します。たとえば、特性からオブジェクトを作成したり、特性から継承したりすることはできません。

したがって、元のソースがなくても、JVMでコンパイルされたバイトコードを簡単に繰り返し使用できます。 JVMレベルでのコピーに関して複雑なことは何もありません。ASMライブラリはこれに簡単に対処できます。 唯一のことは、場所によっては通常のクラスとは少し異なるバイトコードを生成する必要があることです。 たとえば、これは特性の__CLASS__定数で発生します。

 trait Foobar { function test(){ echo __CLASS__; } } class A { use Foobar; } $a = new A(); $a->test(); 


通常の状況での__CLASS__は、コンパイル時に__CLASS__定数を、コードが配置されているクラスの名前を持つ文字列に置き換えます。 トレイトでこれを行うことはできません。トレイトで発生する場合、実行時にこの定数の値を動的に計算する必要があります。 ただし、特性の__TRAIT__定数は、クラスの__CLASS__と同じように__CLASS__します。 また、式selfおよびself::classを処理する必要があります。 プロパティのコピーは非常に簡単なので、説明する意味がありません。

Zendランタイムライブラリのオプトアウト


ここで、ライブラリとは、標準のPHP関数を含むzend apiを使用してCで記述された拡張機能を意味します。 約1か月間、文字列、配列などの関数を実装しました。 PHPのように。最も厄介なのは、いくつかの関数の説明を読んだ後でも1-2-3回入力できなかったこと、引数にさまざまなオプションを渡すとどうなるか、どのような結果を待つかです。 phpでは、関数は普遍的であり、膨大な量の機能が1つの関数に詰め込まれているため、そのような関数の実装が非常に複雑になっています。

ある時点で、たとえばwordpressやsymfonyをJPHPで起動できるようなレベルでこれらの関数を実装していないことに気付きました。そして、外部からのプロジェクトに対する態度は次のようになります。

保守的な開発者「すべてのzendライブラリを実装するときに、wordpress、symfony、yii、またはその他の有名なプロジェクトを実行できないのに、なぜJPHPが必要なのか考えてみます。それまでの間は、HHVMをご覧ください。」

プログレッシブ開発者「JPHPを実装し、PHPカーブ全体とruntimeいランタイム、一貫性のないすべての関数とクラスを繰り返しました。なぜこれに時間を費やす必要があるのですか?」


Zend Runtimeを放棄することは非常に良い考えであることに気付きました。PHPは、カーブランタイム、カーブ、および一貫性のない機能のためにしばしばscられました。そして、ランタイムを作成することが決定されました。何か新しいことを試して実験したいアクティブな開発者は、プロジェクトに背を向けないでしょう。

Zend Runtimeを置き換える新しい機能とクラス


phpグローバルな名前空間を詰まらせないために、JPHPのボックスから必ず出てくるすべてのコアクラスと関数を別の名前空間に割り当てることにしましたその中には次のものがあります。

  1. php\lang\Module-includeおよびrequireなしでソースをロードするためのメカニズム。(インクルードなど)ファイルをダウンロードできますが、すぐに実行するのではなく、プログラマーの要求がある場合にのみ実行できます。さらに、クラスは、どのクラスと関数が内部にあるかに関する情報を取得する機能を提供します。彼は、任意のStreamオブジェクトからソースをダウンロードできます。
    クラスオートロードメカニズムの出現により、クラスローダーハンドラーを除き、includeとrequireを使用する理由はありません。
  2. php\io\Stream — fopen, fwrite, .. , typehinting , Stream , , ..
  3. php\lang\Thread, php\lang\ThreadGroup — . , 2 .
  4. php\io\File — , , File Java,
  5. Java , , , , java Java
  6. php\lang\Environmentコードサポートオプションを実行するための隔離された環境:HOT_RELOADホットスワップコードおよびCONCURRENT複数のスレッドで同じ環境を使用するため。


このリストはまだ完全ではありません。適切に考える時間がないため、クラスはほとんどありません。

JPHPテスト


多くの人は、このような複雑なプロジェクトをどのように単独で行うことができ、開発しても何も壊れないのではないかと考えています。単体テストは、この問題をほぼ100%解決します。プログラミング言語エンジンのテストは非常に簡単です。テストの対象と方法をすぐに確認できます。

最初は自分で簡単なテストを作成しましたが、当時は言語の複雑なzendテストを使用できませんでした。しかし、JPHPが進化するにつれて、PHP自体のソースコードにあるzendテストを徐々に導入し始めました。また、テストではサードパーティの機能が使用されていたため、完全ではなく、修正する必要がある場合がありました。あなたは理解するでしょう、ここに例があります:テストのためのテストset_error_handler、関数はテスト内で使用されますfopen私の意見では、これは非常に間違っています。なぜ拡張機能は言語の基本部分のテストに参加すべきなのでしょうか?典型的なzendユニットテストは、次のようないくつかのセクションで構成されています。

特性の単体テスト
 --TEST-- Use instead to solve a conflict. --FILE-- <?php error_reporting(E_ALL); trait Hello { public function saySomething() { echo 'Hello'; } } trait World { public function saySomething() { echo 'World'; } } class MyHelloWorld { use Hello, World { Hello::saySomething insteadof World; } } $o = new MyHelloWorld(); $o->saySomething(); ?> --EXPECTF-- Hello 


あなたが見ることができるように、いくつかのセクションがあります:--TEST----FILE----EXPECTF---テストの記述は、コードが実行すると、その結果はと期待されています。


Zendテストの助けを借りて、特にOOPでの膨大な数のバグとPHP言語との矛盾を修正することができました(そして、信じられないほどの多くの動作)。特性もzendテストの実装を使用して実装されました。このような機能が実装されたことを書いたとき、jphpおよびzendユニットテストに合格することを意味します。

AndroidとDalvik


DalvikはJVMとはまったく異なるタイプの仮想マシンであり、異なるバイトコード形式で実行され、それ自体がスタックではなくレジスタです。JPHPはJVMマシン用のコンパイラであり、当然、Dalvikと互換性のないバイトコードをコンパイルします。ただし、GoogleはJVMバイトコードをDalvikバイトコードに変換する興味深いユーティリティを開発者に親切に提供してくれました。JRubyのRubotoプロジェクトがあります。これはガイドラインにも役立ちます-どこへ行くか。

Androidは確かに有望な分野ですが、JPHPをこのプラットフォームに移植する時が来るまでは。プロジェクトがバージョン1.0に達して初めて安定するようになったときのみ、私はそれが理にかなっていると思います。

次はどこへ行きますか?


WEB
はい、可能です。 JPHPを使用すると、Node.js、Ruby、その他の言語で記述されている方法と同様に、Webサーバーを完全にphpで記述できます。同時に、すぐに使用できるコードをホットスワップするための、柔軟でカスタマイズ可能なホットリロードモードを提供します。

JPHPを使用すると、非常に生産的なサーバーを作成して、要求間で共有メモリにアクセスするメカニズムを提供できます。これにより、まったく異なる計画のphpフレームワークを作成できます。 Phalconのことを聞いたことがあるなら、これは似たようなもので、Cでのみ書かれています。 JPHPは、CまたはC ++の複雑な拡張としてではなく、複雑なロジック、PHPでの高いパフォーマンスを備えた、このようなフレームワークを作成する機会を提供します。最後の手段として、ボトルネックのJava拡張機能を作成できます。これは、CまたはC ++で拡張機能を作成するよりもはるかに簡単です。

GUI
はい、多くの人がデスクトップをオフに書きます。なぜなら、すべてがウェブに行きます。しかし、この分野は依然として関連しており、需要があります。JPHPを使用すると、GUIアプリケーションを作成できます。既に許可されています。Swingには拡張機能があり、将来的にはHTML5およびCSS3をサポートするJavaFXにも表示される可能性があります。

Android
もちろん、これはまだ遠い見通しですが、そうです。さらに、JPHPは、Raspberry Piなど、Oracle VMが存在するARMデバイスで既に実行できます。

おわりに


PHPはやや気まぐれな患者であることが判明しましたが、最終的に操作は成功しました=)。多くのサードパーティ開発者はこの言語を好まないこと、しばしば批判されることを理解していますが、反対側からこの言語を見る価値があります。何かのフロントエンドであるプロトタイプをすばやく作成し、機能テストを作成するために頻繁に使用する必要がある場合、私自身はPHPを使用します。

ご清聴ありがとうございました。

Source: https://habr.com/ru/post/J218021/


All Articles