In addition to fully understandable official documentation (
Chromium Wiki ), there are articles on how to get the source code and build the Chromium project (
for example ).
I wanted to talk about how, based on this code, you can create C ++ applications that can be compiled and run on several operating systems and architectures. Of course, libraries already exist for this purpose, such as
Qt and
boost . But this is precisely why this article is related to the 'abnormal programming' section, because no one seriously considers Chromium code as the basis for a cross-platform application.
However, if you think about it, it becomes clear that this is quite possible, and not even very difficult.
After all, in the Chromium project there is an assembly system, with the help of which both the project itself and all
the necessary dependencies. Libraries like
boringssl ,
ffmpeg ,
freetype2 ,
hunspell ,
ICU ,
jsoncpp ,
libjpeg , libxml,
openh264 ,
protobuf ,
gtest ,
sqlite and, of course,
v8 , are delivered, updated and easily connected for use.
The Chromium team has written such components as logging, strings and internationalization, working with application resources (strings, images, binary data), working with networks and files, graphics, including 3D, IPC, UI framework for several platforms, and much more. still. All this is covered by a large number of tests (although not one hundred percent), including performance tests.
')
I note, I am not trying to prove that this is better than any library you know, faster, more convenient, or just clear your karma. No, this is just an experiment and to some extent a story about what makes up such a complex and large project as a modern browser.
So I decided to show how you can create a small application that demonstrates working with some entities from
the Chromium core library .
If this material seems interesting, it will be possible to take a closer look at working with the network, graphics, UI and others. This is not a reference to the existing API in Chromium, but rather a demonstration of how to work with the basic things that are needed in almost any program.
It is necessary to take into account the fact that the code base is constantly changing, some parts are more subject to change, some less. This is still not a fixed public API.
I will not dwell on how to download the code and set up the environment, all this is described in detail in the articles on the links provided. We assume that we already have depot_tools in our $ PATH (we need the gn and ninja utilities), the source code has been received and is ready for assembly in the chromium / directory. Build the entire project Chromium we do not need, at least in the first stage.
Create the chromium / src directory of our application, sample_app and sample_app / src.
The application code will be placed in sample_app / src, and I will list all the commands relative to the current chromium / src / sample_app directory.
To get all the application code from the article at once, you can clone the repository
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/
Let's start with the entry point of our application and the base config for the build system.
src / sample_app.cc int main() { return 0; }
src / BUILD.gn # SampleApp executable("sample_app") { output_name = "sample_app" sources = [ "sample_app.cc", ] }
Chromium uses tools such as
GYP and
GN to generate
ninja files describing the steps involved in building a project. GN is the next stage in the development of the ninja file generator, it is much faster than GYP, written in C ++ instead of Python, and its syntax is friendlier to humans. So we’ll use it, although at the moment Chromium supports building with GYP too.
In your build config sample_app / src / BUILD.gn we set the name of the target, the final name of the executable file and enumerate the files with the source code. Looks pretty clear, right?
Though all small files of configs look clear, even CMake, though Makefile.
In order for GN to see the configuration of our project, you need to refer to it in the root file chromium / src / BUILD.gn, putting such a patch
Diff for root patch BUILD.gnsrc / 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" ] +}
If a repository checkout has been made, then you can run the command
$ (cd .. && git apply sample_app/src/root_BUILD_gn.patch)
After adding our target to the general build config we can build our application.
$ gn gen --args=is_debug=true --root=../ ../out/gn $ ninja -C ../out/gn sample_app
Thus, we have indicated that we are assembling a debug build, we generate ninja build files in the chromium / src / out / gn / directory and the root build config is located in chromium / src /
Let's add console output to our application and show that at least the standard C ++ library is available to us.
src / sample_app.cc #include <iostream> #include <string> int main(int argc, const char* argv[]) { std::cout << "Hello from SampleApp!" << std::endl; return 0; }
Repeating the build command and running the application, we should see our greeting:
$ ninja -C ../out/gn sample_app $ ../out/gn/sample_app Hello from SampleApp!
One of the base classes in any application is a string. Chromium uses the class string from the C ++ library, std :: basic_string <>, UTF16 strings are used to a large extent (
base :: string16 , this is typedef for std :: basic_string) and lightweight string-view class
base :: StringPiece .
Let's try using strings and conversions between different encodings.
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; } }
src / BUILD.gn executable("sample_app") { output_name = "sample_app" sources = [ "sample_app.cc", ] deps = [ "//base" ] }
We have added the desired dependence on target
//base
in BUILD.gn and were able to use the necessary functions.
As you can see, nothing complicated, the
ICU library is responsible for all the work under the hood, which is available to us without any additional actions.
The build command doesn't change,
$ ninja -C ../out/gn sample_app
Ninja automatically rebuilds files when changing .gn config.
From the lines you can go to the command line of the application and parse it.
Please keep in mind that the entire class code in src / base was written exactly for the needs that the Chromium team had. If it seems strange to you that there is no functionality, or, on the contrary, that redundant code is written, consider this.
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; }
Now we can run the compiled program with keys and look at the output:
Run with different command line keys $ 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
It demonstrates simultaneously classes for working with the command line, an abstraction for file paths and a little with the logging library. So, the call
CHECK () will check the result of the call
CommandLine :: Init and in case of failure it will print the line "Failed to parse a command line argument." and complete the application. In this case, if successful, the
operator <<
for the log stream will not be called and there will be no overhead costs for printing. This is important if such logging is associated with calling non-trivial functions.
The
DCHECK (debug check) check will be performed only in the debug build and will not affect the program execution in the release.
Continuing to talk about the program log, consider the following code, including and using logging
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!"; } }
Here we first initialize the logging subsystem for recording in STDERR, and then output the messages to the log with different levels.
The last message with the FATAL level will terminate the execution of the program, and display the stack-trace if it can.
Add a call to the
LoggingSample()
function in
main()
and check the operation of the program with the specified logging level (output is on Mac OS X):
Run with different logging levels $ 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.
You can also see that there is a rather tough, but useful rule - for each entity / class there is one file whose name corresponds to files with code. So, the FilePath class should be searched in the header file
base / files / file_path.h , and its implementation is in
base / files / file_path.cc .
This makes it very easy to navigate through the code and find the necessary classes and functions.
Let's look at more complex code, for example, to list the contents of the current directory.
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(¤t_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; } }
And just add to
main()
.
As you can see, using the
base :: FileEnumerator class is not difficult, and as a result we were able to get a list of files in the current directory:
Application output $ 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
Usually the program consists not only of the main.cc file, so let's add a standalone module for a certain API to our project. The essence of the code in the new module is not so important now, it's a demonstration, you can always return true, for example.
Create a header file and a file with the implementation of our new function:
src / sample_api.h #ifndef SAMPLE_APP_SAMPLE_API_H_ #define SAMPLE_APP_SAMPLE_API_H_ namespace sample_api {
src / sample_api.cc #include "sample_app/src/sample_api.h" namespace sample_api { bool CallApiFunction() { return true; } }
After that you can write unit tests for our function.
Do this and add new files to our project.
Our test codesrc / 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()); } }
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", ] }
Using the GTest library is fairly straightforward, but you need to add the dependency "// testing / gtest" to the project, and for convenience also the "// base / test: run_all_unittests". This will save us from having to write code to run the project tests, the code in src / base / test / run_all_unittests.cc will be responsible for this.
Re-generate ninja files for the project and collect our tests:
$ ninja -C ../out/gn sample_app_unittests
Run the tests: $ ../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.
Great, all tests passed!
After the tests are written, and the code of our API is added to the project, you can use it.
src / sample_app.cc #include "sample_app/src/sample_api.h" void UseSampleAPI() { if (sample_api::CallApiFunction()) { std::cout << "Magick!" << std::endl; } }
Just like that.
As a result, our program can work with different encodings, with the file system, parses its command line, is able to report in the error log and uses the new code with test coverage approaching 100%.
At the same time, the build-config of the project is very small and readable, and compiled and executed
the code can on different platforms, and without a single #ifdef in our code.
Isn't it great?
Of course, to use the whole arsenal of classes available in the project, you will need to read a lot
code and documentation (which is sometimes the same thing), or ask in mailings, because no special
There are no training materials, API reference books and ready-made examples.
Moreover, the code base is alive and constantly changing, albeit for the better.
So again, everything you read should be used at your own risk =)
The article and so it turned out much longer than I expected, so that's all for now.
Thank you for reading!
Links