クロスプラットフォームアプリケーションを開発するためのSDKとしてChromiumプロジェクトのコードベースを使用する

わかりやすい公式ドキュメント( Chromium Wiki )に加えて、ソースコードを取得してChromiumプロジェクトをビルドする方法に関する記事があります( )。



このコードに基づいて、複数のオペレーティングシステムとアーキテクチャでコンパイルおよび実行できるC ++アプリケーションを作成する方法についてお話ししたかったのです。 もちろん、この目的のためにQtboostなどのライブラリが既に存在します。 しかし、この記事で「異常なプログラミング」というセクションを参照しているのは、クロスプラットフォームアプリケーションの基盤としてChromiumコードを真剣に考えている人がいないためです。



ただし、考えてみると、これは非常に可能であり、非常に難しいことでもないことが明らかになります。

結局のところ、Chromiumプロジェクトにはビルドシステムがあり、その助けによりプロジェクト自体とすべてが組み立てられます。

必要な依存関係。 boringsslffmpegfreetype2hunspellICUjsoncpplibjpeg 、libxml、 openh264protobufgtestsqliteなどのライブラリ 、そしてもちろんv8は 、使用のために配信、更新され、簡単に接続されます。



Chromiumチームは、ロギング、文字列、国際化、アプリケーションリソース(文字列、画像、バイナリデータ)の操作、ネットワークとファイルの操作、3Dを含むグラフィック、IPC、いくつかのプラットフォームのUIフレームワークなどのコンポーネントを作成しましたまだ。 これはすべて、パフォーマンステストを含む多数の(100パーセントではないが)テストによってカバーされています。



私は、それがあなたが知っているどのライブラリよりも優れていること、より速く、より便利であること、または単にあなたのカルマをきれいにすることを証明しようとしているのではないことに注意します。 いいえ、これは単なる実験であり、ある程度、現代のブラウザのような複雑で大規模なプロジェクトの構成に関するストーリーです。



そこで、Chromiumベースライブラリの一部のエンティティの操作を示す小さなアプリケーションを作成する方法を示すことにしました。

この資料が興味深いと思われる場合は、ネットワーク、グラフィックス、UIなどを使用した作業をより詳細に分析することができます。 これはChromiumの既存のAPIへの参照ではなく、ほとんどすべてのプログラムで必要な基本的なものを操作する方法のデモンストレーションです。

コードベースは絶えず変化しており、一部の部分はより変化しやすく、一部の変化は少ないことを考慮する必要があります。 これはまだ固定パブリックAPIではありません。



コードをダウンロードして環境を構成する方法については詳しく説明しません。これについては、提供されているリンクの記事で詳しく説明しています。 $ PATHにdepot_toolsがすでにあり(gnおよびninjaユーティリティが必要)、ソースコードが受信され、chromium /ディレクトリでのアセンブリの準備ができていると仮定します。 少なくとも最初の段階では、Chromiumプロジェクト全体を構築する必要はありません。



Chromium / srcにアプリケーションのディレクトリsample_appとsample_app / srcを作成します。

アプリケーションコードはsample_app / srcに配置され、現在のディレクトリchromium / src / sample_appに関連するすべてのコマンドを提供します。



記事からすべてのアプリケーションコードを一度に取得するには、リポジトリhttps://github.com/dreamer-dead/chromium-sample-app.gitを複製できます



$ pwd /Users/username/chromium/src $ git clone https://github.com/dreamer-dead/chromium-sample-app.git sample_app $ cd sample_app/
      
      





アプリケーションのエントリポイントとビルドシステムの基本構成から始めましょう。



src / sample_app.cc



 int main() { return 0; }
      
      





src / BUILD.gn



 # SampleApp executable("sample_app") { output_name = "sample_app" sources = [ "sample_app.cc", ] }
      
      





ChromiumはGYPGNなどのツールを使用して、プロジェクトの構築に必要な手順を説明する忍者ファイルを生成します。 GNは、忍者ファイルジェネレーターの開発における次の段階であり、Pythonの代わりにC ++で記述されたGYPよりもはるかに高速であり、その構文はより人間に優しいものです。 現時点ではChromiumはGYPを使用したアセンブリもサポートしていますが、それを使用します。



build config sample_app / src / BUILD.gnで、ターゲットの名前、実行可能ファイルの最終名を設定し、ソースコードとともにファイルをリストします。 かなり明確に見えますよね?

すべての小さな構成ファイルは明確に見えますが、少なくともCMake、少なくともMakefileです。



GNがプロジェクトの構成を確認するには、ルートファイルchrome / src / BUILD.gnで参照し、そのようなパッチを適用する必要があります



ルートパッチBUILD.gnの差分
src / root_BUILD_gn.patch



 diff --git a/BUILD.gn b/BUILD.gn index 0fa2013..729157d 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -906,3 +906,7 @@ template("assert_valid_out_dir") { assert_valid_out_dir("_unused") { actual_sources = [ "$root_build_dir/foo" ] } + +group("sample_app") { + deps = [ "//sample_app/src:sample_app" ] +}
      
      







リポジトリのチェックアウトが行われた場合、コマンドを実行できます



 $ (cd .. && git apply sample_app/src/root_BUILD_gn.patch)
      
      





ターゲットを一般的なビルド構成に追加した後、アプリケーションをビルドできます。



 $ gn gen --args=is_debug=true --root=../ ../out/gn $ ninja -C ../out/gn sample_app
      
      





したがって、デバッグアセンブリを構築し、Croom / src / out / gn /ディレクトリにninjaアセンブリファイルを生成し、root build configがchroma / src /にあることを示しました。



コンソール出力をアプリケーションに追加して、少なくともC ++標準ライブラリが利用できることを示しましょう。



src / sample_app.cc



 #include <iostream> #include <string> int main(int argc, const char* argv[]) { std::cout << "Hello from SampleApp!" << std::endl; return 0; }
      
      





ビルドコマンドを繰り返してアプリケーションを起動すると、挨拶が表示されます。



 $ ninja -C ../out/gn sample_app $ ../out/gn/sample_app Hello from SampleApp!
      
      





アプリケーションの基本クラスの1つは文字列です。 Chromiumは、C ++ライブラリの文字列クラスstd :: basic_string <>を使用し、UTF16文字列( base :: string16 、これはstd :: basic_stringのtypedefです)および軽量文字列ビュークラスbase :: StringPieceを使用します。

文字列と異なるエンコーディング間の変換を使用してみましょう。



src / sample_app.cc



 #include <iostream> #include <string> #include "base/strings/utf_string_conversions.h" namespace { void StringsSample() { std::cout << base::WideToUTF8(L"This is a wide string.") << std::endl; std::wcout << base::UTF8ToWide("This is an UTF8 string.") << std::endl; std::cout << base::UTF16ToUTF8(base::UTF8ToUTF16( "This is an UTF8 string converted to UTF16 and back.")) << std::endl; } } // namespace int main(int argc, const char* argv[]) { std::cout << "Hello from SampleApp!" << std::endl; StringsSample(); return 0; }
      
      





src / BUILD.gn



 executable("sample_app") { output_name = "sample_app" sources = [ "sample_app.cc", ] deps = [ "//base" ] }
      
      





BUILD.gnに必要な//base



ターゲットへの依存関係を追加し、必要な機能を使用することができました。

ご覧のとおり、複雑なことは何もありませんが、 ICUライブラリーは、追加のアクションなしで利用できる内部のすべての作業を担当します。



ビルドコマンドは変更されませんが、



 $ ninja -C ../out/gn sample_app
      
      





Ngnjaは、.gn構成を変更するとファイルを自動的に再構築します。



行から、アプリケーションとその分析のコマンドラインに移動できます。

src / baseのすべてのクラスコードは、Chromiumチームのニーズに合わせて作成されていることに注意してください。 機能がまったくない、またはその逆で、過剰なコードが記述されていることが奇妙に思える場合は、これを考慮してください。



src / sample_app.cc



 #include "base/command_line.h" #include "base/files/file_path.h" #include "base/logging.h" void CommandLineSample() { using base::CommandLine; DCHECK(CommandLine::ForCurrentProcess()) << "Command line for process wasn't initialized."; const CommandLine& command_line = *CommandLine::ForCurrentProcess(); std::cout << "Application program name is " << command_line.GetProgram().AsUTF8Unsafe() << std::endl; if (command_line.HasSwitch("bool-switch")) { std::cout << "Detected a boolean switch!" << std::endl; } std::string string_switch = command_line.GetSwitchValueASCII("string-switch"); if (!string_switch.empty()) { std::cout << "Got a string switch value: " << string_switch << std::endl; } } int main(int argc, const char* argv[]) { CHECK(base::CommandLine::Init(argc, argv)) << "Failed to parse a command line argument."; std::cout << "Hello from SampleApp!" << std::endl; StringsSample(); CommandLineSample(); return 0; }
      
      





キーを使用してアセンブルされたプログラムを実行し、出力を確認できます。



異なるコマンドラインスイッチで実行する
 $ ninja -C ../out/gn sample_app $ ../out/gn/sample_app --bool-switch --string-switch=SOME_VALUE Hello from SampleApp! This is a wide string. This is an UTF8 string. This is an UTF8 string converted to UTF16 and back. Application program name is ../out/gn/sample_app Detected a boolean switch! Got a string switch value: SOME_VALUE
      
      







ここでは、コマンドラインを操作するためのクラス、ファイルパスの抽象化、およびロギングライブラリを使用するためのクラスを同時に示します。 そのため、 CHECK()を呼び出すと、 CommandLine :: Initの呼び出し結果がチェックされ、 失敗した場合は、「コマンドライン引数の解析に失敗しました」という文字列が出力されます。 アプリケーションを完了します。 さらに、成功した場合、 operator <<



はログフローに対して呼び出されず、印刷のオーバーヘッドはありません。 このようなロギングが重要な機能の呼び出しを伴う場合、これは重要です。



チェックDCHECK(デバッグチェック)は、デバッグビルドでのみ実行され、リリース内のプログラムの実行には影響しません。



プログラムログについて引き続き説明し、ロギングを含めて使用する次のコードを検討します。



src / sample_app.cc



 void LoggingSample() { logging::LoggingSettings settings; // Set log to STDERR on POSIX or to OutputDebugString on Windows. settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG; CHECK(logging::InitLogging(settings)); // Log messages visible by default. LOG(INFO) << "This is INFO log message."; LOG(WARNING) << "This is WARNING log message."; // Verbose log messages, disabled by default. VLOG(1) << "This is a log message with verbosity == 1"; VLOG(2) << "This is a log message with verbosity == 2"; // Verbose messages, can be enabled only in debug build. DVLOG(1) << "This is a DEBUG log message with verbosity == 1"; DVLOG(2) << "This is a DEBUG log message with verbosity == 2"; // FATAL log message will terminate our app. if (base::CommandLine::ForCurrentProcess()->HasSwitch("log-fatal")) { LOG(FATAL) << "Program will terminate now!"; } }
      
      





ここでは、まずSTDERRに書き込むためにロギングサブシステムを初期化し、次にさまざまなレベルでメッセージをログに出力します。

FATALレベルの最後のメッセージは、プログラムの実行を完了し、可能であればスタックトレースを表示します。

LoggingSample()



LoggingSample()



関数への呼び出しを追加し、指定されたログレベルでプログラムの動作を確認します(Mac OS Xでの出力が表示されます)。



さまざまなレベルのログで実行する
 $ ninja -C ../out/gn sample_app $ ../out/gn/sample_app --v=2 --log-fatal Hello from SampleApp! This is a wide string. This is an UTF8 string. This is an UTF8 string converted to UTF16 and back. Application program name is ../out/gn/sample_app [0303/202541:INFO:sample_app.cc(51)] This is INFO log message. [0303/202541:WARNING:sample_app.cc(52)] This is WARNING log message. [0303/202541:VERBOSE1:sample_app.cc(55)] This is a log message with verbosity == 1 [0303/202541:VERBOSE2:sample_app.cc(56)] This is a log message with verbosity == 2 [0303/202541:VERBOSE1:sample_app.cc(59)] This is a DEBUG log message with verbosity == 1 [0303/202541:VERBOSE2:sample_app.cc(60)] This is a DEBUG log message with verbosity == 2 [0303/202541:FATAL:sample_app.cc(64)] Program will terminate now! 0 sample_app 0x000000010f276def _ZN4base5debug10StackTraceC2Ev + 47 1 sample_app 0x000000010f276f93 _ZN4base5debug10StackTraceC1Ev + 35 2 sample_app 0x000000010f2b53a0 _ZN7logging10LogMessageD2Ev + 80 3 sample_app 0x000000010f2b2c43 _ZN7logging10LogMessageD1Ev + 35 4 sample_app 0x000000010f235072 _ZN12_GLOBAL__N_113LoggingSampleEv + 1346 5 sample_app 0x000000010f2342e0 main + 288 6 sample_app 0x000000010f2341b4 start + 52 7 ??? 0x0000000000000003 0x0 + 3 Trace/BPT trap: 5 $ ../out/gn/sample_app Hello from SampleApp! This is a wide string. This is an UTF8 string. This is an UTF8 string converted to UTF16 and back. Application program name is ../out/gn/sample_app [0303/203145:INFO:sample_app.cc(51)] This is INFO log message. [0303/203145:WARNING:sample_app.cc(52)] This is WARNING log message.
      
      







また、かなり難しいが便利なルールがあることもわかります。各エンティティ/クラスには、コードを持つファイルに対応する名前のファイルが1つあります。 そのため、FilePathクラスはヘッダーファイルbase / files / file_path.hで検索する必要があり、その実装はbase / files / file_path.ccにあります。

これにより、コードをナビゲートし、目的のクラスと関数を簡単に検索できます。



たとえば、現在のディレクトリの内容をリストするために、より複雑なコードを見てみましょう。



src / sample_app.cc



 #include "base/files/file_enumerator.h" #include "base/files/file_util.h" void FilesSample() { base::FilePath current_dir; CHECK(base::GetCurrentDirectory(&current_dir)); std::cout << "Enumerating files and directories in path: " << current_dir.AsUTF8Unsafe() << std::endl; base::FileEnumerator file_enumerator( current_dir, false, base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES); for (base::FilePath name = file_enumerator.Next(); !name.empty(); name = file_enumerator.Next()) { std::cout << (file_enumerator.GetInfo().IsDirectory() ? "[dir ] " : "[file] ") << name.AsUTF8Unsafe() << std::endl; } }
      
      





そして、 main()



追加するだけです。

ご覧のとおり、 ベース:: FileEnumeratorクラスを使用することは難しくありません。その結果、現在のディレクトリにあるファイルのリストを取得できました。



アプリケーション出力
 $ ninja -C ../out/gn sample_app $ (cd src/ && ../../out/gn/sample_app) Hello from SampleApp! This is a wide string. This is an UTF8 string. This is an UTF8 string converted to UTF16 and back. Application program name is ../../out/gn/sample_app [0303/203629:INFO:sample_app.cc(51)] This is INFO log message. [0303/203629:WARNING:sample_app.cc(52)] This is WARNING log message. Enumerating files and directories in path: /Users/username/chromium/src/sample_app/src [file] /Users/username/chromium/src/sample_app/src/BUILD.gn [file] /Users/username/chromium/src/sample_app/src/sample_app.cc
      
      







通常、プログラムはmain.ccファイル以外のもので構成されているため、プロジェクトにAPIのスタンドアロンモジュールを追加しましょう。 新しいモジュールのコードの本質は今ではそれほど重要ではありません。これはデモです。たとえば、いつでもtrueを返すことができます。

ヘッダーファイルと、新しい関数の実装を含むファイルを作成します。



src / sample_api.h



 #ifndef SAMPLE_APP_SAMPLE_API_H_ #define SAMPLE_APP_SAMPLE_API_H_ namespace sample_api { // Do some black magic. bool CallApiFunction(); } // namespace sample_api #endif // SAMPLE_APP_SAMPLE_API_H_
      
      





src / sample_api.cc



 #include "sample_app/src/sample_api.h" namespace sample_api { bool CallApiFunction() { return true; } } // namespace sample_api
      
      





その後、関数の単体テストを作成できます。

これを行い、プロジェクトに新しいファイルを追加します。



テストのコード
src / sample_api_unittest.cc



 #include "sample_app/src/sample_api.h" #include "testing/gtest/include/gtest/gtest.h" namespace sample_api { namespace { TEST(SampleApi, ApiFunctionTest) { EXPECT_TRUE(CallApiFunction()); } } // namespace } // namespace sample_api
      
      





src / BUILD.gn



 import("//testing/test.gni") executable("sample_app") { output_name = "sample_app" sources = [ "sample_app.cc", "sample_api.cc", "sample_api.h", ] deps = [ "//base", ] } test("sample_app_unittests") { sources = [ # TODO: Extract these API files as a library. "sample_api.cc", "sample_api.h", "sample_api_unittest.cc", ] deps = [ "//base/test:run_all_unittests", "//testing/gtest", ] }
      
      







GTestライブラリの使用は非常に簡単ですが、プロジェクトに依存関係「// testing / gtest」を追加する必要があります。また、便宜上// // base / test:run_all_unittests。 これにより、プロジェクトテストを実行するためのコードを記述する必要がなくなります。src/ base / test / run_all_unittests.ccのコードがこれを担当します。



プロジェクトのninjaファイルを再生成し、テストを収集します。



 $ ninja -C ../out/gn sample_app_unittests
      
      





テストを実行します。
 $ ../out/gn/sample_app_unittests IMPORTANT DEBUGGING NOTE: batches of tests are run inside their own process. For debugging a test inside a debugger, use the --gtest_filter=<your_test_name> flag along with --single-process-tests. Using sharding settings from environment. This is shard 0/1 Using 8 parallel jobs. [1/1] SampleApi.ApiFunctionTest (0 ms) SUCCESS: all tests passed. Tests took 0 seconds.
      
      







すべてのテストに合格しました!

テストが作成され、APIのコードがプロジェクトに追加されたら、それを使用できます。



src / sample_app.cc



 #include "sample_app/src/sample_api.h" void UseSampleAPI() { if (sample_api::CallApiFunction()) { std::cout << "Magick!" << std::endl; } }
      
      





とても簡単です。

その結果、プログラムはさまざまなエンコーディングで動作し、ファイルシステムでコマンドラインを解析し、ログにエラーを報告し、テストカバレッジが100%に近づく新しいコードを使用します。

同時に、プロジェクトのビルド構成は非常に小さくて読みやすく、コンパイルして実行されます

コードはさまざまなプラットフォームで使用でき、コードには単一の#ifdefがありません。

それは素晴らしいことではありませんか?



もちろん、プロジェクトで利用可能なクラスの兵器庫全体を使用するには、多くを読む必要があります

コードとドキュメンテーション(時には同じものです)、またはニュースレターで尋ねます。

トレーニング資料、APIリファレンスブック、既製のサンプルはありません。

さらに、コードベースはより良いとはいえ、活発で常に変化しています。

繰り返しますが、あなたが読むすべてのものはあなた自身の危険とリスクで使用されるべきです=)



この記事は私が予想していたよりもずっと長くなったので、今のところはこれですべてです。

読んでくれてありがとう!



参照資料







All Articles