Arcane  4.2.3.0
Developer documentation
Loading...
Searching...
No Matches
LibUnwindStackTraceService.cc
1// -*- tab-width: 2; indent-tabs-mode: nil; coding: utf-8-with-signature -*-
2//-----------------------------------------------------------------------------
3// Copyright 2000-2026 CEA (www.cea.fr) IFPEN (www.ifpenergiesnouvelles.com)
4// See the top-level COPYRIGHT file for details.
5// SPDX-License-Identifier: Apache-2.0
6//-----------------------------------------------------------------------------
7/*---------------------------------------------------------------------------*/
8/* LibUnwindStackTraceService.cc (C) 2000-2025 */
9/* */
10/* Function call trace service using 'libunwind'. */
11/*---------------------------------------------------------------------------*/
12/*---------------------------------------------------------------------------*/
13
14#include "arcane/utils/PlatformUtils.h"
15#include "arcane/utils/IStackTraceService.h"
16#include "arcane/utils/StackTrace.h"
17#include "arcane/utils/StringBuilder.h"
18#include "arcane/utils/Array.h"
19#include "arcane/utils/ISymbolizerService.h"
20
21#include "arccore/base/internal/DependencyInjection.h"
22#include "arccore/common/internal/Process.h"
23
24#include "arcane/core/ServiceBuilder.h"
25#include "arcane/core/Directory.h"
26
28#include "arcane/core/AbstractService.h"
29
30#include "arcane_packages.h"
31
32#define UNW_LOCAL_ONLY
33#include <libunwind.h>
34#include <cxxabi.h>
35
36#include <map>
37#include <mutex>
38
39#include <execinfo.h>
40#include <cstdio>
41#include <sys/types.h>
42#include <unistd.h>
43//#include <cstdlib>
44#include <dlfcn.h>
45
46#if defined(ARCANE_HAS_PACKAGE_DW)
47#include <elfutils/libdwfl.h>
48#endif
49
50/*---------------------------------------------------------------------------*/
51/*---------------------------------------------------------------------------*/
52
53namespace Arcane
54{
55
57{
58 int m_line = 0;
59 int m_column = 0;
60 const char* m_source_file = nullptr;
61};
62
63#if defined(ARCANE_HAS_PACKAGE_DW)
76class DWHandler
77{
78 public:
79
80 DWHandler()
81 {
82 m_callback.find_elf = &dwfl_linux_proc_find_elf;
83 m_callback.find_debuginfo = &dwfl_standard_find_debuginfo;
84 m_callback.section_address = nullptr;
85 m_callback.debuginfo_path = nullptr;
86 // Make sure we do not try to connect to internet to search for
87 // debug information. This may cause deadlocks on some environments.
88 setenv("DEBUGINFOD_URLS", "", 1);
89 }
90
91 ~DWHandler()
92 {
93 if (m_session)
94 dwfl_end(m_session);
95 }
96
102 DebugSourceInfo getInfo(unw_word_t func_address)
103 {
104 DebugSourceInfo source_info;
105
106 // This function need a lock but it can be recursively called so this
107 // may produce a deadlock. To prevent that, we try to get the lock
108 // and if does not work we return immediately.
109 ScopedTryLock tried_lock(m_mutex);
110 if (!tried_lock.isLock())
111 return source_info;
112
113 _init();
114
115 auto dw_address = reinterpret_cast<Dwarf_Addr>(func_address);
116
117 Dwfl_Module* module = dwfl_addrmodule(m_session, dw_address);
118 if (!module)
119 return source_info;
120 Dwarf_Addr bias = 0;
121 Dwarf_Die* dw_die = dwfl_module_addrdie(module, dw_address, &bias);
122 if (dw_die)
123 return source_info;
124
125 Dwarf_Line* dw_source_info = dwarf_getsrc_die(dw_die, dw_address - bias);
126
127 if (dw_source_info) {
128 int line = 0;
129 int column = 0;
130 // WARNING: the returned pointer is no longer valid if m_session is destroyed.
131 const char* source_file = dwarf_linesrc(dw_source_info, nullptr, nullptr);
132 dwarf_lineno(dw_source_info, &line);
133 dwarf_linecol(dw_source_info, &column);
134 source_info = { line, column, source_file };
135 }
136
137 return source_info;
138 }
139
140 public:
141
142 bool m_is_init = false;
143 Dwfl_Callbacks m_callback;
144 Dwfl* m_session = nullptr;
145 std::mutex m_mutex;
146
147 private:
148
150 class ScopedTryLock
151 {
152 public:
153
154 ScopedTryLock(std::mutex& mutex)
155 : m_mutex(mutex)
156 {
157 m_is_lock = m_mutex.try_lock();
158 }
159 ~ScopedTryLock()
160 {
161 if (m_is_lock)
162 m_mutex.unlock();
163 }
164
165 constexpr bool isLock() const { return m_is_lock; }
166
167 private:
168
169 std::mutex& m_mutex;
170 bool m_is_lock = false;
171 };
172
173 void _init()
174 {
175 if (m_is_init)
176 return;
177 m_is_init = true;
178
179 m_session = dwfl_begin(&m_callback);
180 // TODO: call dwfl_end()
181 if (!m_session)
182 return;
183
184 dwfl_report_begin(m_session);
185 dwfl_linux_proc_report(m_session, getpid());
186 dwfl_report_end(m_session, nullptr, nullptr);
187 }
188};
189
190#else
191
192// Empty implementation if libdw is not found.
194{
195 public:
196
197 DebugSourceInfo getInfo(unw_word_t func_address)
198 {
199 return {};
200 }
201};
202
203#endif
204
205/*---------------------------------------------------------------------------*/
206/*---------------------------------------------------------------------------*/
207
211class LibUnwindStackTraceService
212: public TraceAccessor
213, public IStackTraceService
214{
215 private:
216
218 struct ProcInfo
219 {
220 public:
221
222 ProcInfo() = default;
223 explicit ProcInfo(const String& aname)
224 : m_name(aname)
225 {}
226
227 public:
228
230 const String& name() const { return m_name; }
232 const char* libraryFileName() const { return m_library_file_name; }
233
234 public:
235
239 const char* m_library_file_name = nullptr;
241 //TODO: do not store this for every function.
242 unw_word_t m_file_loaded_address = 0;
243 unw_word_t m_base_ip = 0;
244 DebugSourceInfo m_source_info;
245 };
246
247 public:
248
249 explicit LibUnwindStackTraceService(const ServiceBuildInfo& sbi)
250 : TraceAccessor(sbi.application()->traceMng())
251 {
252 }
254 : TraceAccessor(tm)
255 {
256 }
257
258 public:
259
260 void build() override
261 {
262 if (!platform::getEnvironmentVariable("ARCANE_GDB_STACK").null())
263 m_want_gdb_info = true;
264 if (!platform::getEnvironmentVariable("ARCANE_USE_BACKTRACE").null())
265 m_use_backtrace = true;
266 }
267
268 public:
269
271 StackTrace stackTrace(int first_function) override;
272 StackTrace stackTraceFunction(int function_index) override;
273
274 private:
275
276 using ProcInfoMap = std::map<unw_word_t, ProcInfo>;
277 ProcInfoMap m_proc_name_map;
278 std::mutex m_proc_name_map_mutex;
279
280 bool m_want_gdb_info = false;
281 bool m_use_backtrace = false;
282 DWHandler m_dw_handler;
283 ProcInfo _getFuncInfo(unw_word_t ip, unw_cursor_t* cursor);
284 ProcInfo _getFuncInfo(const void* ip);
285 String _getGDBStack();
286 StackTrace _backtraceStackTrace(const FixedStackFrameArray& stack_frames);
287 String _generateFileAndOffset(const FixedStackFrameArray& stack_frames);
288 FixedStackFrameArray _backtraceStackFrame(int first_function);
289};
290
291/*---------------------------------------------------------------------------*/
292/*---------------------------------------------------------------------------*/
293
294LibUnwindStackTraceService::ProcInfo LibUnwindStackTraceService::
295_getFuncInfo(unw_word_t ip, unw_cursor_t* cursor)
296{
297 {
298 std::lock_guard<std::mutex> lk(m_proc_name_map_mutex);
299 auto v = m_proc_name_map.find(ip);
300 if (v != m_proc_name_map.end())
301 return v->second;
302 }
303
304 unw_word_t offset;
305 char func_name_buf[10000];
306 char demangled_func_name_buf[10000];
307 unw_get_proc_name(cursor, func_name_buf, 10000, &offset);
308 int dstatus = 0;
309 size_t len = 10000;
310 char* buf = abi::__cxa_demangle(func_name_buf, demangled_func_name_buf, &len, &dstatus);
311 ProcInfo pi;
312 pi.m_base_ip = offset;
313 {
314 Dl_info dl_info;
315 void* addr = (void*)ip;
316 int r2 = dladdr(addr, &dl_info);
317 if (r2 != 0) {
318 const char* dli_fname = dl_info.dli_fname;
319 // Base address of the file being loaded.
320 void* dli_fbase = dl_info.dli_fbase;
321 pi.m_library_file_name = dli_fname;
322 pi.m_file_loaded_address = (unw_word_t)dli_fbase;
323 }
324 }
325
326 if (buf)
327 pi.m_name = std::string_view(buf);
328 else
329 pi.m_name = std::string_view(func_name_buf);
330
331 // We must use 'ip-1' because the address returned by libunwind is that
332 // of the return instruction.
333 pi.m_source_info = m_dw_handler.getInfo(ip - 1);
334
335 {
336 std::lock_guard<std::mutex> lk(m_proc_name_map_mutex);
337 m_proc_name_map.insert(ProcInfoMap::value_type(ip, pi));
338 }
339
340 return pi;
341}
342
343/*---------------------------------------------------------------------------*/
344/*---------------------------------------------------------------------------*/
345
350LibUnwindStackTraceService::ProcInfo LibUnwindStackTraceService::
351_getFuncInfo(const void* addr)
352{
353 {
354 std::lock_guard<std::mutex> lk(m_proc_name_map_mutex);
355 auto v = m_proc_name_map.find((unw_word_t)addr);
356 if (v != m_proc_name_map.end()) {
357 return v->second;
358 }
359 }
360 const size_t buf_size = 10000;
361 char demangled_func_name_buf[buf_size];
362 Dl_info dl_info;
363 int r = dladdr(addr, &dl_info);
364 if (r == 0) {
365 // Error in dladdr.
366 std::cout << "ERROR in dladdr\n";
367 return ProcInfo("Unknown");
368 }
369 const char* dli_sname = dl_info.dli_sname;
370 if (!dli_sname)
371 dli_sname = "";
372 int dstatus = 0;
373 size_t len = buf_size;
374 char* buf = abi::__cxa_demangle(dli_sname, demangled_func_name_buf, &len, &dstatus);
375 //char* buf = (char*)dli_sname;
376 ProcInfo pi;
377 if (buf)
378 pi.m_name = std::string_view(buf);
379 else
380 pi.m_name = std::string_view(dli_sname);
381 {
382 std::lock_guard<std::mutex> lk(m_proc_name_map_mutex);
383 m_proc_name_map.insert(ProcInfoMap::value_type((unw_word_t)addr, pi));
384 }
385 return pi;
386}
387
388/*---------------------------------------------------------------------------*/
389/*---------------------------------------------------------------------------*/
390
391String LibUnwindStackTraceService::
392_getGDBStack()
393{
394 void* array[256];
395 char** names;
396 int i, size;
397
398 fprintf(stderr, "\nNative stacktrace:\n\n");
399
400 size = backtrace(array, 256);
401 names = backtrace_symbols(array, size);
402 for (i = 0; i < size; ++i) {
403 fprintf(stderr, "\t%s\n", names[i]);
404 }
405
406 fflush(stderr);
407 return platform::getGDBStack();
408}
409
410/*---------------------------------------------------------------------------*/
411/*---------------------------------------------------------------------------*/
412
415stackTrace(int first_function)
416{
417 unw_cursor_t cursor;
418 unw_context_t uc;
419
420 String last_str;
421 if (m_want_gdb_info)
422 last_str = _getGDBStack();
423 FixedStackFrameArray backtrace_stack_frames = _backtraceStackFrame(first_function);
424 if (m_use_backtrace)
425 return _backtraceStackTrace(backtrace_stack_frames);
426
427 const size_t hexa_buf_size = 100;
428 char hexa[hexa_buf_size + 1];
429
430 unw_getcontext(&uc);
431 unw_init_local(&cursor, &uc);
432 int current_func = 0;
433 StringBuilder message;
434
435 FixedStackFrameArray stack_frames;
436 while (unw_step(&cursor) > 0) {
437 unw_word_t ip;
438 unw_get_reg(&cursor, UNW_REG_IP, &ip);
439 if (current_func >= first_function) {
440 ProcInfo pi = _getFuncInfo(ip, &cursor);
441 String func_name = pi.m_name;
442 message += " ";
443 snprintf(hexa, hexa_buf_size, "%14llx", (long long)ip);
444 message += hexa;
445 message += " ";
446 message += func_name;
447 if (pi.m_source_info.m_source_file) {
448 message += " \"";
449 message += pi.m_source_info.m_source_file;
450 if (pi.m_source_info.m_line > 0) {
451 message += ":";
452 message += pi.m_source_info.m_line;
453 }
454 message += "\"";
455 }
456 message += "\n";
457
458 stack_frames.addFrame(StackFrame((intptr_t)ip));
459 }
460 ++current_func;
461 }
463
464 if (ss) {
465 // It is better to read the call stack from _backtrace() because
466 // libunwind returns the return address for each function
467 // which causes a shift in the source code line info
468 // (it points to the line after the one being executed).
469 last_str = ss->stackTrace(backtrace_stack_frames.view());
470 }
471 else {
472 message += "\nFileAndOffsetStack:{{\n";
473 message += _generateFileAndOffset(backtrace_stack_frames);
474 message += "}}\n";
475 }
476 message += last_str;
477 return StackTrace(stack_frames, message);
478}
479
480/*---------------------------------------------------------------------------*/
481/*---------------------------------------------------------------------------*/
482
489stackTraceFunction(int function_index)
490{
491 unw_cursor_t cursor;
492 unw_context_t uc;
493 unw_word_t ip;
494 //unw_word_t offset;
495
496 String last_str;
497 if (m_want_gdb_info)
498 last_str = _getGDBStack();
499
500 unw_getcontext(&uc);
501 unw_init_local(&cursor, &uc);
502 int current_func = 0;
503 StringBuilder message;
504
505 while (unw_step(&cursor) > 0) {
506 unw_get_reg(&cursor, UNW_REG_IP, &ip);
507 if (current_func == function_index) {
508 ProcInfo pi = _getFuncInfo(ip, &cursor);
509 String func_name = pi.m_name;
510 message += func_name;
511 break;
512 }
513 ++current_func;
514 }
515 return StackTrace(message.toString() + last_str);
516}
517
518/*---------------------------------------------------------------------------*/
519/*---------------------------------------------------------------------------*/
520
523_backtraceStackFrame(int first_function)
524{
525 void* ips[256];
526 Integer size = backtrace(ips, 256);
527
528 FixedStackFrameArray stack_frames;
529 for (Integer i = first_function; i < size; ++i) {
530 stack_frames.addFrame(StackFrame((intptr_t)ips[i]));
531 }
532 return stack_frames;
533}
534
535/*---------------------------------------------------------------------------*/
536/*---------------------------------------------------------------------------*/
537
541{
542 const size_t buf_size = 100;
543 char hexa[buf_size + 1];
544
545 StringBuilder message;
546 ConstArrayView<StackFrame> frames_view = stack_frames.view();
547 for (StackFrame f : frames_view) {
548 intptr_t ip = f.address();
549 ProcInfo pinfo = _getFuncInfo((void*)ip);
550 String func_name = pinfo.name();
551 message += " ";
552 snprintf(hexa, buf_size, "%10llx", (long long)ip);
553 message += hexa;
554 message += " ";
555 message += func_name;
556 message += "\n";
557 }
558 return StackTrace(stack_frames, message);
559}
560
561/*---------------------------------------------------------------------------*/
562/*---------------------------------------------------------------------------*/
563
576{
577 const size_t buf_size = 100;
578 char hexa[buf_size + 1];
579
580 StringBuilder message;
581 ConstArrayView<StackFrame> frames_view = stack_frames.view();
582 for (StackFrame f : frames_view) {
583 intptr_t ip = f.address();
584 ProcInfo pinfo = _getFuncInfo(reinterpret_cast<const void*>(ip));
585 message += (pinfo.libraryFileName() ? pinfo.libraryFileName() : "()");
586 message += " ";
587 auto file_base_address = pinfo.m_file_loaded_address;
588 // On Linux (CentOS 6 and 7), the address 0x400000 corresponds to that
589 // of the executable load, but if we subtract this address from that
590 // of the function then the symbolizer does not work (whether it is
591 // llvm-symbolize or addr2line)
592 if (file_base_address == 0x400000)
593 file_base_address = 0;
594 intptr_t offset_ip = (ip - file_base_address);
595 snprintf(hexa, buf_size, "%llx", (long long)offset_ip);
596 message += "0x";
597 message += hexa;
598 message += "\n";
599 }
600 return message;
601}
602
603/*---------------------------------------------------------------------------*/
604/*---------------------------------------------------------------------------*/
605
606/*---------------------------------------------------------------------------*/
607/*---------------------------------------------------------------------------*/
608
609class LLVMSymbolizerService
610: public TraceAccessor
611, public ISymbolizerService
612{
613 public:
614
615 explicit LLVMSymbolizerService(const ServiceBuildInfo& sbi)
617 {
618 _init();
619 }
620 explicit LLVMSymbolizerService(ITraceMng* tm)
621 : TraceAccessor(tm)
622 {
623 _init();
624 }
625
626 public:
627
628 void build() {}
629
630 public:
631
633
634 private:
635
636 String m_llvm_symbolizer_path;
637 bool m_is_check_done = false;
638 bool m_is_valid = false;
639
640 private:
641
644 {
645 if (m_is_check_done)
646 return;
647 // Before calling this method, m_llvm_symbolizer_path must contain
648 // the name of the directory where llvm-symbolizer is located.
649 Directory dir(m_llvm_symbolizer_path);
650 String fullpath = dir.file("llvm-symbolizer");
651 Int64 length = platform::getFileLength(fullpath);
652 m_llvm_symbolizer_path = fullpath;
653 if (length > 0)
654 m_is_valid = true;
655 m_is_check_done = true;
656 }
657 void _init()
658 {
659 m_llvm_symbolizer_path = platform::getEnvironmentVariable("ARCANE_LLVMSYMBOLIZER_PATH");
660 }
661};
662
663/*---------------------------------------------------------------------------*/
664/*---------------------------------------------------------------------------*/
665
668{
669 _checkValid();
670 if (!m_is_valid)
671 return String();
672 std::stringstream ostr;
673 // NOTE: the code below is similar to
674 // LibUnwindStackTraceService::_generateFileAndOffset(). It should
675 // merge the two.
676 for (Integer i = 0, n = frames.size(); i < n; ++i) {
677 Dl_info dl_info;
678 intptr_t addr = frames[i].address();
679 int r2 = dladdr((void*)addr, &dl_info);
680 const char* dli_fname = nullptr;
681 intptr_t base_address = 0;
682 if (r2 != 0) {
683 dli_fname = dl_info.dli_fname;
684 // File load base address.
685 void* dli_fbase = dl_info.dli_fbase;
686 intptr_t true_base = reinterpret_cast<intptr_t>(dli_fbase);
687 // On Linux (CentOS 6 and 7), the address 0x400000 corresponds to that
688 // of the executable load, but if we subtract this address from that
689 // of the function then the symbolizer does not work (whether it is
690 // llvm-symbolize or addr2line)
691 if (true_base == 0x400000)
692 true_base = 0;
693 base_address = addr - true_base;
694 }
695 // TODO: write base_address in hex to be able to read it back with addr2line
696 ostr << (dli_fname ? dli_fname : "??") << " " << base_address << '\n';
697 }
698
699 std::string input_str(ostr.str());
700 String output_str;
701 ProcessExecArgs args;
702 args.setCommand(m_llvm_symbolizer_path);
703 Integer input_size = arcaneCheckArraySize(input_str.length());
704 ByteConstArrayView input_bytes(input_size, reinterpret_cast<const Byte*>(input_str.c_str()));
705 args.setInputBytes(input_bytes);
706 args.addArguments("--demangle");
707 args.addArguments("--pretty-print");
708 //args.addArguments("--print-source-context-lines=5");
709 Process::execute(args);
710 return String(args.outputBytes());
711}
712
713/*---------------------------------------------------------------------------*/
714/*---------------------------------------------------------------------------*/
715
717 ServiceProperty("LibUnwind", ST_Application),
719
721 ServiceProperty("LLVMSymbolizer", ST_Application),
723
724ARCANE_DI_REGISTER_PROVIDER(LibUnwindStackTraceService,
725 DependencyInjection::ProviderProperty("LibUnwind"),
726 ARCANE_DI_INTERFACES(IStackTraceService),
727 ARCANE_DI_CONSTRUCTOR(ITraceMng*));
728
729ARCANE_DI_REGISTER_PROVIDER(LLVMSymbolizerService,
730 DependencyInjection::ProviderProperty("LLVMSymbolizer"),
731 ARCANE_DI_INTERFACES(ISymbolizerService),
732 ARCANE_DI_CONSTRUCTOR(ITraceMng*));
733
734/*---------------------------------------------------------------------------*/
735/*---------------------------------------------------------------------------*/
736
737} // End namespace Arcane
738
739/*---------------------------------------------------------------------------*/
740/*---------------------------------------------------------------------------*/
This file contains the various service factories and macros for registering services.
#define ARCANE_SERVICE_INTERFACE(ainterface)
Macro to declare an interface when registering a service.
Constant view of an array of type T.
constexpr Integer size() const noexcept
Number of elements in the array.
String file(const String &file_name) const override
Returns the full path of the file file_name in the directory.
Definition Directory.cc:120
Stores a fixed maximum size list of StackFrame.
void addFrame(const StackFrame &frame)
Adds a frame to the list of frames. If nbFrame() is greater than or equal to MAX_FRAME,...
virtual ITraceMng * traceMng() const =0
Trace manager.
Interface of a function call tracing service.
Interface of a source code symbol retrieval service.
virtual String stackTrace(ConstArrayView< StackFrame > frames)=0
Information for the call stack frames.
String stackTrace(ConstArrayView< StackFrame > frames) override
Information for the call stack frames.
void _checkValid()
Checks that the specified path is valid.
Function call trace service using libunwind.
StackTrace _backtraceStackTrace(const FixedStackFrameArray &stack_frames)
Call stack via the backtrace function.
FixedStackFrameArray _backtraceStackFrame(int first_function)
Call stack via the backtrace function.
String _generateFileAndOffset(const FixedStackFrameArray &stack_frames)
Generates a list of file names and offsets for a call stack.
StackTrace stackTrace(int first_function) override
Character string indicating the call stack.
StackTrace stackTraceFunction(int function_index) override
Returns the current call stack.
ConstArrayView< Byte > outputBytes() const
Contains the result of the process's standard output (STDOUT).
Definition Process.h:64
static ProcessExecArgs::ExecStatus execute(ProcessExecArgs &args)
Executes a process whose information is contained in args.
Definition Process.cc:42
IApplication * application() const
Access to the associated IApplication.
Structure containing the information to create a service.
Service creation properties.
Stores the addresses corresponding to a call stack. This class is internal and should not be used out...
Information about function call stacks.
Unicode character string constructor.
String toString() const
Returns the constructed character string.
TraceAccessor(ITraceMng *m)
Constructs an accessor via the trace manager m.
TraceMessage pinfo() const
Flow for a parallel information message.
ITraceMng * traceMng() const
Trace manager.
#define ARCANE_REGISTER_SERVICE(aclass, a_service_property,...)
Macro for registering a service.
Integer len(const char *s)
Returns the length of the string s.
long unsigned int getFileLength(const String &filename)
Length of the file filename. If the file is not readable or does not exist, returns 0.
ISymbolizerService * getSymbolizerService()
Service used to obtain information about source code symbols.
String getGDBStack()
Retrieves the call stack via gdb.
String getEnvironmentVariable(const String &name)
Environment variable named name.
-- tab-width: 2; indent-tabs-mode: nil; coding: utf-8-with-signature --
Integer arcaneCheckArraySize(unsigned long long size)
Checks that size can be converted into an 'Integer' to serve as the size of an array....
std::int64_t Int64
Signed integer type of 64 bits.
Int32 Integer
Type representing an integer.
@ ST_Application
The service is used at the application level.
ConstArrayView< Byte > ByteConstArrayView
C equivalent of a 1D array of characters.
Definition UtilsTypes.h:470
unsigned char Byte
Type of a byte.
Definition BaseTypes.h:42
const char * m_library_file_name
Name of the library (.so or .exe) where the method is located.
const char * libraryFileName() const
Name of the library containing the function. May be null.