- Debugging C++
- Quick start
- Details on debugging C++
First make sure your environment is setup by following the building instructions
Then you will need the following plugins for VSCode:
First lets take a look at .vscode/launch.json
. This contains various ways to launch commands from vscode. For this example we will look at the command LLDB Debug Unit Tests
.
{
"type": "lldb",
"request": "launch",
"name": "LLDB Debug Unit Tests",
"program": "node",
"args": [
"--expose_gc",
"${workspaceFolder}/tests/node_modules/jasmine/bin/jasmine.js",
"spec/unit_tests.js",
"--filter=${input:testFilter}"
],
"cwd": "${workspaceFolder}/tests",
"preLaunchTask": "Build Node Tests"
}
A quick read through this code shows that the launch type is lldb
provided by the CodeLLDB extension, and it is using the node
command to invoke the jasmine
test framework with our spec/unit_tests.js
. The ${input:testFilter}
will prompt us for a string to use as filter to avoid running all tests every time. The preLaunchTask
will compile Realm JS in debug mode before starting the debug session.
When prompted for a filter, try using "testListPush" as the input. As we are fairly certain this will perform a push
command on a realm list, we can place a breakpoint in the push function in src/js_list.hpp
by locating the push function and clicking next to the desired line number (see example below)
Now we can run the test and see what happens. Click on the debug tab in the left bar of vscode:
Select the LLDB Launch Unit Tests
from the run tab:
Now press play, type "testListPush" as filter and hit Enter
We should now arrive at our new breakpoint.
To run all tests, simply leave the filter at the default (.
).
Visual Studio Code with the CodeLLDB extension provides a good experience for debugging C++ code using the lldb
debugger.
The launch.json file contains various useful debugger launch configurations which attach lldb
to the node
process so that breakpoints can be set and exceptions can be caught. These profiles can be seected from the list in the top right of the "Run and Debug" pane in VS Code.
All the launch configurations are configured to compile Realm JS in debug mode before starting the debug session, so you do not need to remember to compile between sessions.
This configuration will run the unit tests with lldb
attached. By default, it will ask for the filter for the run each time you invoke it. If you are debugging a specific test, it can save time if you temporarily hardcode the filter: https://github.com/realm/realm-js/blob/master/.vscode/launch.json#L88.
This configuration will run the integration tests with lldb
attached. By default, it will ask for the grep pattern for the run each time you invoke it. If you are debugging a specific test, it can save time if you temporarily hardcode the filter: https://github.com/realm/realm-js/blob/master/.vscode/launch.json#L117.
A pre-requisite for running these tests is to start the https://github.com/realm/realm-js/blob/master/packages/realm-app-importer script in a terminal, by running: npx lerna bootstrap --scope @realm/integration-tests --include-dependencies && cd integration-tests/tests && npm run app-importer
– this is usually started automatically when you run the tests, but as we need to connect lldb
directly to the node
instance that is running the tests, you need to start it manually.
This configuration starts a node
REPL with the debugger attached. This allows you to easily evaluate statements and jump into the C++ debugger. If you are running the same commands over and over, you may want to save these to a temporary .js
file and add this file's path to the args
in https://github.com/realm/realm-js/blob/master/.vscode/launch.json#L98, so that node
runs this script instead of a REPL.
This configuration will attach lldb
to a running process. This can be useful for debugging Electron applications, for example, in which case you might want to connect the debugger to the main.js
process.
When this configuration is run, it will open a process picker. You should be able to identify the correct process by searching for a known string in the application name, though it might require some trial and error!
It can be helpful to tick the C++: on throw
and/or C++: on catch
default breakpoints, in order to catch exceptions.
It is sometimes possible to get useful information on variables via the "Variables" panel in the debug pane, but sometimes it is not that helpful (e.g. with NAPI objects which just show as a memory address), or you might want to call methods on an instance rather than just inspect it.
The lldb
p
command can be used in the "Debug Console" pane (to the right of the terminal) to do this when you are paused at a breakpoint – you can inspect/interact with anything in the scope of the current breakpoint, for example p my_array.size()
or p napi_object.Type()
.
Note that this might not always work, e.g. sometimes it can crash when trying to inspect certain properties, in which case a useful technique is to add a line of code storing the value you are interested in temporarily so that you can inspect it in the variables window, e.g. auto temp_type = napi_object.Type();
.
There are many useful advanced lldb
features, for example:
- you can add a breakpoint whenever a named method is called, even if you can't locate it in the source, with
br s -M method_name
, e.g.br s -M ~realm::js::MyClass
to break wheneverrealm::js::MyClass
's destructor is called - you can print the memory address of a variable with
expr --raw -- &variable_name
, which can be useful when trying to work out if you are accidentally working with a copy of an object - you can make it break whenever a certain variable is read or modified, by right clicking on it in the Variables pane and clicking "Break on Value Read/Change/Access".
The lldb
documentation and the CodeLLDB
documentation are useful resources.
It can sometimes be useful to use a debug version of Node. This allows you to view the source code of Node and v8 when inspecting stack traces, rather than the assembly code, and can also yield more useful stack traces when debugging deep C++ integration issues.
- Download the source code from https://nodejs.org/en/download/
- Unzip the source code to wherever you want to keep the debug version of Node (note that the debug symbol paths get hardcoded, so you need to recompile if you move it around after compilation)
- From the root of the Node source directory, run
./configure --debug -C
– this configures the build in debug mode, and-C
outputscompile_commands.json
so that you can get better debug information in VS Code. - Run
make -j32
to compile Node.-j32
specifies the number of jobs to run in parallel – it seems that 2x the number of threads (which is 2x the number of cores) is recommended. You can play around with different values – 32 seems to saturate the CPU of a Macbook Pro 16" (which has 8 cores = 16 threads), while still leaving it usable, and compiles in under half an hour.
To use a debug version of Node, change the path to node
for the lldb
launch configuration you are using to point to the debug version you compiled above, e.g. change https://github.com/realm/realm-js/blob/master/.vscode/launch.json#L103 to "program": "/Users/my_name/dev/node-v16.13.2/out/Debug/node"
. You should now get full source code in stack traces.
You can also open the Node source directory in VS Code and use the launch config from https://joyeecheung.github.io/blog/2018/12/31/tips-and-tricks-node-core/ (which has some other useful tips) if you wish to go deeper into the Node source code.
To debug Realm C++ in an iOS app using Xcode:
- Ensure you are using a debug version of the Realm
xcframework
(./scripts/build-ios.sh -c Debug simulator
) - In your Xcode project, go to
File
>Add files to <project name>
and select yourrealm-js/src
directory (it must be the same directory you used to build thexcframework
as the paths are absolute). Ensure "Copy items" is not ticked, and "Create folder references" is selected, then pressAdd
. - Repeat step 2, for your
realm-js/vendor/realm-core/src
directory. - Build and run the app in debug mode.
You should now be able to navigate to Realm C++ source files and add breakpoints by navigating to the source files in the Project navigator.
To debug Realm C++ in an Android app using Android Studio (the integration test is already set up to do this so you shouldn't need to do it for that):
- Ensure you are using a debug version of the Realm
.so
(node scripts/build-android.js --arch=x86 --build-type=Debug
) - Prevent Gradle from stripping debug symbols from Realm by adding to your
app/build.gradle
in theandroid.buildTypes.debug
section:// Do not strip debug symbols from the Realm library packagingOptions { jniLibs.keepDebugSymbols += "**/librealm.so" }
- Add the source paths for Realm to the project by adding the paths (which can be relative to the
build.gradle
file) to yourapp/build.gradle
in theandroid
section:// Add the Realm source files to the Android Studio project so that we can add breakpoints // in debug mode. These will not be compiled, it will still use the .so library. sourceSets { main.java.srcDirs += '<path to realm-js/src>' main.java.srcDirs += '<path to realm-js/vendor/realm-core/src>' }
- In Android Studio, go to
Run
>Edit Configurations...
and in theDebugger
tab, select aDebug type
ofNative Only
- In the same window, add a
LLDB Startup Command
entry ofprocess handle --pass true --stop false --notify true SIGUSR1
. This prevents it from breaking on signals used internally in React Native. - Build and run the app in debug mode.
You should now be able to navigate to Realm C++ source files and add breakpoints by navigating to the "Project Files" view (using the "Project" dropdown in the top left file browser).
You can also start lldb-server
on the Android emulator and connect to it directly from another client (e.g. lldb
or the VS Code LLDB plugin). You need to be using a rooted emulator (i.e. one of the images without Google Play).
- Follow steps 1, 2 and 4 of the Android Studio debugging instructions (you do not have to leave it in "Native Only" debugging, but need to do this at least once so
lldb-server
is installed on the device) - Build and run the app in debug mode
- Run
adb shell
to open a shell on the emulator - Run
su
to become root - Run
ps -A | grep com.yourapp
, replacingcom.yourapp
with your app's bundle ID, to get the PID (you can also see this in the Android Studio Debug tab) - Run
/data/data/com.yourapp/lldb/bin/lldb-server platform --server --listen "*:9123"
, replacingcom.yourapp
with your app's bundle ID (e.g.com.realmreactnativetests
) This will startlldb-server
running over TCP so you can connect to it. - In a separate local shell, run
adb forward tcp:9123 tcp:9123
to forward thelldb-server
port to your local machine
If you would like to make this happen on every startup, you can modify the script which Android Studio installs on to the device to always start lldb-server
in TCP mode:
- Open
/Applications/Android\ Studio.app/Contents/plugins/android-ndk/resources/lldb/android/start_lldb_server.sh
in a text editor - Modify the last line to e.g.
$BIN_DIR/lldb-server platform --server --listen "*:9123" & $BIN_DIR/lldb-server platform --server --listen $LISTENER_SCHEME://$DOMAINSOCKET_DIR/$PLATFORM_SOCKET --log-file "$PLATFORM_LOG_FILE" --log-channels "$LOG_CHANNELS" </dev/null >$LOG_DIR/platform-stdout.log 2>&1
To connect to the running server with lldb
:
- Run
lldb
platform select remote-android
platform connect connect://localhost:9123
attach PID
, replacing PID with your app's PID
- Open VS Code with the relevant source files (make sure you use the same directory which Realm JS was compiled in)
- Run the [LLDB Attach to Android Emulator] debug launch config
- Enter the app's PID at the prompt
Sometimes it can be non-obvious what type an auto
variable has. The debugger can potentially help you here, by adding a breakpoint then inspecting the type in the Variables pane, but an alternative approach is to use the following template trick (from https://stackoverflow.com/a/38820784/17700221):
-
Add
template<typename T> struct TD;
somewhere at the top level of a header file before the code you are looking at (probably in the.hpp
file you are working with, just above the method in question, for Realm JS) -
Add
TD<decltype(variable_name)> td;
after the variable (variable_name
) who's type you want to inspect. -
Now when you compile, you will get an error like
error: implicit instantiation of undefined template 'realm::js::TD<const realm::ObjectSchema &>'
– the type parameter ofTD
is the type of the variable in question, in this caseconst realm::ObjectSchema &
.
To run a Node script and capture a performance trace to analyse in Instruments, you can run it like: xcrun xctrace record --template 'Time Profiler' --target-stdout - --launch -- ~/.nvm/versions/node/v16.13.2/bin/node index.js
.
Using a debug version of Realm (e.g. compile Realm in debug mode, then npm i ~/dev/realm-js
to install your local version into your Node project) may yield more useful information.