diff --git a/changelog/cpptrace.dd b/changelog/cpptrace.dd new file mode 100644 index 0000000000..423095326a --- /dev/null +++ b/changelog/cpptrace.dd @@ -0,0 +1,51 @@ +Added experimental `C++` symbol demanling in the stacktrace + +This feature is availableby passing the following switches to your executable (not to the compiler): +- `--DRT-cpptrace=enable:y`: Enable demangling of C++ symbols +- `--DRT-cpptrace=prefix:`: Change the prefix preceding the demangled C++ name (by default would be `[C++]`) +- `--DRT-cpptrace=noprefix:n`: Disable adding the prefix to C ++ demangle names + +To use this function in the Posix platform you will need to link your executable to the phobos shared library, compile the program by passing -defaultlib=libphobos2.so to DMD. + +Example: + +``` +module cpp_trace.d + +extern(C++) void f1() +{ + throw new Exception("exception"); +} + +void main() +{ + try + { + f1(); + } + catch (Exception e) + { + import core.stdc.stdio; + auto str = e.toString(); + printf("%.*s\n", cast(int)str.length, str.ptr); + } +} +``` + +If you run the executable with the following switches `./ --DRT-cpptrace=enable:y`, you would get this output: + +``` +object.Exception@cpp_trace.d(3): exception +---------------- +src/cpp_trace.d:5 [C++] f1() [ADDR] +src/cpp_trace.d:12 _Dmain [ADDR] +``` + +Instead, if you run the executable with the following switches `./ --DRT-cpptrace=enable:y --DRT-cpptrace=prefix:CPP`, you would get this output: + +``` +object.Exception@cpp_trace.d(5): exception +---------------- +src/cpp_trace.d:5 [CPP] f1() [ADDR] +src/cpp_trace.d:12 _Dmain [ADDR] +``` diff --git a/mak/COPY b/mak/COPY index da1acf12dc..8c1ac30f2c 100644 --- a/mak/COPY +++ b/mak/COPY @@ -25,6 +25,8 @@ COPY=\ $(IMPDIR)\core\internal\atomic.d \ $(IMPDIR)\core\internal\attributes.d \ $(IMPDIR)\core\internal\convert.d \ + $(IMPDIR)\core\internal\cppdemangle.d \ + $(IMPDIR)\core\internal\cpptrace.d \ $(IMPDIR)\core\internal\dassert.d \ $(IMPDIR)\core\internal\destruction.d \ $(IMPDIR)\core\internal\entrypoint.d \ diff --git a/mak/SRCS b/mak/SRCS index 4470041ca5..a49a50cb28 100644 --- a/mak/SRCS +++ b/mak/SRCS @@ -25,6 +25,8 @@ SRCS=\ src\core\internal\atomic.d \ src\core\internal\attributes.d \ src\core\internal\convert.d \ + src\core\internal\cppdemangle.d \ + src\core\internal\cpptrace.d \ src\core\internal\dassert.d \ src\core\internal\destruction.d \ src\core\internal\entrypoint.d \ diff --git a/mak/WINDOWS b/mak/WINDOWS index 1ab24cc4ed..66461c0fbc 100644 --- a/mak/WINDOWS +++ b/mak/WINDOWS @@ -132,6 +132,12 @@ $(IMPDIR)\core\internal\attributes.d : src\core\internal\attributes.d $(IMPDIR)\core\internal\convert.d : src\core\internal\convert.d copy $** $@ +$(IMPDIR)\core\internal\cppdemangle.d : src\core\internal\cppdemangle.d + copy $** $@ + +$(IMPDIR)\core\internal\cpptrace.d : src\core\internal\cpptrace.d + copy $** $@ + $(IMPDIR)\core\internal\dassert.d : src\core\internal\dassert.d copy $** $@ diff --git a/posix.mak b/posix.mak index b6c03c3de7..ee589eef05 100644 --- a/posix.mak +++ b/posix.mak @@ -334,6 +334,9 @@ $(ROOT)/unittest/% : $(ROOT)/unittest/test_runner $(addsuffix /.run,$(filter-out test/shared,$(ADDITIONAL_TESTS))): $(DRUNTIME) test/shared/.run: $(DRUNTIMESO) +ifeq (1,$(SHARED)) + test/exceptions/.run: $(DRUNTIMESO) $(DRUNTIME) +endif test/%/.run: test/%/Makefile $(DMD) $(QUIET)$(MAKE) -C test/$* MODEL=$(MODEL) OS=$(OS) DMD=$(abspath $(DMD)) BUILD=$(BUILD) \ diff --git a/src/core/internal/cppdemangle.d b/src/core/internal/cppdemangle.d new file mode 100644 index 0000000000..0837a4d6fa --- /dev/null +++ b/src/core/internal/cppdemangle.d @@ -0,0 +1,311 @@ +/** +* This module provides functions to demangle C++ symbols +* +* For POSIX platforms the function `__cxa_demangle` present in Itanium C++ ABI does the demangling, therefore this module +* depends on the C++ standard library which must be linked dynamically or available for research through the dynamic loader. +* +* For Windows platform the function `UnDecorateSymbolName` present in the Debug Help Library does the demangling, +* which should be available for all versions of Windows supported by D. +* +* Copyright: Copyright © 2020, The D Language Foundation +* License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). +* Authors: Ernesto Castellotti +* Source: $(DRUNTIMESRC core/internal/_cppdemangle.d) +* See_Also: +* https://itanium-cxx-abi.github.io/cxx-abi/abi.html#demangler +* https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.3/a01696.html +* https://libcxxabi.llvm.org/spec.html +* https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-undecoratesymbolname +*/ +module core.internal.cppdemangle; + +/** +* Demangles C++ mangled names. +* +* The output buffer and return will be contains the demangled name of C++ symbol if the demangling is successful. +* +* This function will fail and the mangled name in input will be returned if: +* 1. The symbol to be processed does not follow the Itanium ABI for POSIX or Visual C++ for Windows, or more simply it is not a C++ name +* 2. The platform is not compatible or the library needed to perform the demangling was not found or linked with the executable +* 3. The size of the output buffer is not sufficient to contain the demangled name with its prefix +* +* This function will fail and null will be returned if: +* 1. The output buffer is null +* 2. The function has failed (see list above) and output buffer is unable to contain the mangled name in input +* + * Params: + * mangledName = The string to demangle. + * outputBuffer = The destination buffer, if the size of the destination buffer is <= the size of cppdemangle output the return string would be incomplete. + * prefix = Specifies the prefix to be added to the beginning of the string containing the demangled name + * + * Returns: + * The demangled name or the original string if the name is not a mangled C++ name. + */ +char[] cppdemangle(const(char)[] mangledName, char[] outputBuffer, string prefix = "") @trusted +{ + CPPDemangle.initialize(); + return CPPDemangle.instance.cppdemangle(mangledName, outputBuffer, prefix); +} + +package struct CPPDemangle +{ + private __gshared CPPDemangle _instance; + private __gshared bool isInitialized; + + version (Posix) + { + @nogc nothrow extern(C) + { + private extern(C) char* function(const char* mangledName, char* outputBuffer, size_t* length, int* status) __cxa_demangle; + } + + version (OSX) + private static immutable names = ["libc++abi.dylib", "libstdc++.dylib"]; + else + { + private static immutable names = ["libstdc++.so", "libstdc++.so.6", "libstdc++.so.5", + "libc++abi.so", "libc++abi.so.1"]; + } + + private __gshared void* _handle; + + shared static ~this() { + import core.sys.posix.dlfcn : dlclose; + + if (isInitialized) + { + dlclose(_handle); + _handle = null; + isInitialized = false; + } + } + } + + version (Windows) + { + import core.sys.windows.dbghelp : UnDecorateSymbolNameFunc; + + @nogc nothrow extern(System) + { + private UnDecorateSymbolNameFunc UnDecorateSymbolName; + } + } + + char[] cppdemangle(const(char)[] mangledName, char[] outputBuffer, string prefix = "") @safe + { + auto prefixEmpty = prefix.length <= 0; + auto demangleOffset = prefixEmpty ? 0 : prefix.length + 1; // Add prefix + space + + if (outputBuffer is null) + { + return null; + } + + if (outputBuffer.length < demangleOffset) + { + return copyResult(mangledName, outputBuffer); + } + + auto demangle = _cppdemangle(mangledName, outputBuffer[demangleOffset..$]); + + if (demangle is null) + { + return copyResult(mangledName, outputBuffer); + } + + if (!prefixEmpty) + { + outputBuffer[0..demangleOffset - 1] = prefix; + outputBuffer[demangleOffset - 1] = ' '; + } + + return outputBuffer[0..(demangle.length + demangleOffset)]; + } + + static CPPDemangleStatus initialize() + { + if (isInitialized) + { + return CPPDemangleStatus.INITIALIZED; + } + + version (iOS) + { + // Not supported (dlopen doesn't work) + // Fix me + return CPPDemangleStatus.LOAD_ERROR; + } + else version (Posix) + { + import core.sys.posix.dlfcn : dlsym, dlopen, dlclose, RTLD_LAZY; + + auto handle = dlopen(null, RTLD_LAZY); + assert(handle !is null); + auto p = dlsym(handle, "__cxa_demangle"); + + if (p !is null) + { + _handle = handle; + _instance.__cxa_demangle = cast(typeof(CPPDemangle.__cxa_demangle)) p; + isInitialized = true; + return CPPDemangleStatus.INITIALIZED; + } + + dlclose(handle); + + foreach (name; names) + { + handle = dlopen(name.ptr, RTLD_LAZY); + + if (handle !is null) + { + break; + } + } + + if (handle is null) + { + return CPPDemangleStatus.LOAD_ERROR; + } + + p = dlsym(handle, "__cxa_demangle"); + + if (p !is null) + { + _handle = handle; + _instance.__cxa_demangle = cast(typeof(CPPDemangle.__cxa_demangle)) p; + isInitialized = true; + return CPPDemangleStatus.INITIALIZED; + } + else + { + return CPPDemangleStatus.SYMBOL_ERROR; + } + } + else version (Windows) + { + import core.sys.windows.dbghelp : DbgHelp; + + auto dbgHelp = DbgHelp.get(); + + if (dbgHelp is null) + { + return CPPDemangleStatus.LOAD_ERROR; + } + + auto func = dbgHelp.UnDecorateSymbolName; + + if (dbgHelp.UnDecorateSymbolName !is null) + { + _instance.UnDecorateSymbolName = dbgHelp.UnDecorateSymbolName; + isInitialized = true; + return CPPDemangleStatus.INITIALIZED; + } + else + { + return CPPDemangleStatus.SYMBOL_ERROR; + } + } + else + { + // Platform not supported + return CPPDemangleStatus.LOAD_ERROR; + } + } + + static CPPDemangle instance() @nogc nothrow + { + return _instance; + } + + private char[] _cppdemangle(const(char)[] mangledName, char[] outputBuffer) @trusted + { + import core.memory : pureCalloc, pureFree; + import core.stdc.string : strlen; + + if (!isInitialized) + { + return null; + } + + auto mangledNamePtr = cast(char*) pureCalloc(mangledName.length + 1, char.sizeof); + + scope(exit) + { + if (mangledNamePtr !is null) + { + pureFree(mangledNamePtr); + } + } + + mangledNamePtr[0..mangledName.length] = mangledName[]; + + version (Posix) + { + int status; + auto demangledName = _instance.__cxa_demangle(mangledNamePtr, null, null, &status); + // NOTE: Due to the implementation of __cxa_demangle, the result of the function + // will be a pointer to arrays of characters of unknown size before the call to + // the function. + // It is in no way possible to pass the output buffer of this function and perform + // the demangling if its size is sufficient directly by calling __cxa_demangle, + // because if the size was insufficient __cxa_demangle would try to increase + // it through realloc. + // Tto use this function safely, it is therefore necessary to allow the necessary + // memory to be allocated (just pass null) and then copy (if the size is sufficient) + // the result into the output buffer. + + if (status != 0 && demangledName is null) + { + return null; + } + + scope(exit) + { + if (demangledName !is null) + { + pureFree(demangledName); + } + } + + return copyResult(demangledName[0..strlen(demangledName)], outputBuffer); + } + + version (Windows) + { + import core.sys.windows.windef : DWORD; + + auto maxStringLen = (outputBuffer.length > DWORD.max) ? DWORD.max : cast(DWORD) outputBuffer.length; + // NOTE: UnDecorateSymbolName expects to receive a length in DWORD (uint) instead + // outputBuffer.length would be ulong. + // To make a safe cast I make sure not to exceed DWORD.max + + auto bufferLen = _instance.UnDecorateSymbolName(mangledNamePtr, outputBuffer.ptr, maxStringLen, 0); + + if (bufferLen <= 0) + { + return null; + } + + return outputBuffer[0..bufferLen]; + } + } +} + +package enum CPPDemangleStatus +{ + INITIALIZED = 1, + LOAD_ERROR = -1, + SYMBOL_ERROR = -2 +} + +package char[] copyResult(const(char)[] input, char[] output) @safe @nogc nothrow +{ + if (input.length > output.length) + { + return null; + } + + output[0..input.length] = input[0..input.length]; + return output[0..input.length]; +} diff --git a/src/core/internal/cpptrace.d b/src/core/internal/cpptrace.d new file mode 100644 index 0000000000..1b12f46a46 --- /dev/null +++ b/src/core/internal/cpptrace.d @@ -0,0 +1,124 @@ +/** +* This module provides the demangling of C++ symbols for stacktrace +* +* Copyright: Copyright © 2020, The D Language Foundation +* License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). +* Authors: Ernesto Castellotti +* Source: $(DRUNTIMESRC core/internal/_cppdemangle.d) +*/ +module core.internal.cpptrace; + +import core.internal.cppdemangle : CPPDemangle, CPPDemangleStatus; +import core.stdc.stdio : fprintf, stderr; +import core.stdc.stdlib : exit; + +/** +* Demangles C++ mangled names passing the runtime options to cppdemangle. +* +* If it is not a C++ mangled name or cppdemangle is not supported by your platform, the original mangled C++ name will be returned. +* The optional destination buffer and return will be contains the same string if the demangle is successful. +* This function is used to demangle C++ symbols in the stacktrace. +* + * Params: + * buf = The string to demangle. + * dst = The destination buffer, if the size of the destination buffer is <= the size of cppdemangle output the return string would be incomplete. + * + * Returns: + * The demangled name or the original string if the name is not a mangled C++ name. + */ +char[] demangleCppTrace(const(char)[] buf, char[] dst) +{ + import core.internal.cppdemangle : copyResult; + + if (!CPPTrace.config.enable) + { + return copyResult(buf, dst); + } + + if (CPPTrace.config.noprefix) + { + CPPTrace.config.prefix = ""; + } + + return CPPTrace.instance.cppdemangle(buf, dst, CPPTrace.config.prefix); +} + +private struct CPPTrace +{ + __gshared CPPDemangle instance; + __gshared Config config; + + static this() + { + import core.internal.parseoptions : initConfigOptions; + initConfigOptions(config, "cpptrace"); + + version (Posix) + { + version (Shared) + { + // OK! CPPDemangling may be supported + } + else + { + if (config.enable) + { + fprintf(stderr, "C++ demangling is only supported if phobos is dynamically linked. Recompile the program by passing -defaultlib=libphobos2.so to DMD\n"); + exit(1); + assert(0); + } + } + } + + if (config.enable) + { + auto result = CPPDemangle.initialize(); + + final switch (result) + { + case CPPDemangleStatus.INITIALIZED: + { + instance = CPPDemangle.instance(); + return; + } + + case CPPDemangleStatus.LOAD_ERROR: + { + version (Posix) + { + fprintf(stderr, "The C++ library for the C++ demangle could not be loaded with dlopen. Please disable the option for C++ demangling.\n"); + } + else version (Windows) + { + fprintf(stderr, "The Debug Help Library could not be loaded. Please disable the option for C++ demangling.\n"); + } + + exit(1); + assert(0); + } + + case CPPDemangleStatus.SYMBOL_ERROR: + { + version (Posix) + { + fprintf(stderr, "The __cxa_demangle symbol was not found in the C++ standard library (maybe it's not compatible). Please disable the option for C++ demangling.\n"); + } + else version (Windows) + { + fprintf(stderr, "The UnDecorateSymbolName symbol was not found in the Debug Help Library (maybe it's not compatible). Please disable the option for C++ demangling.\n"); + } + + exit(1); + assert(0); + } + } + } + } +} + +private struct Config +{ + bool enable; + bool noprefix; + string prefix = "[C++]"; +} diff --git a/src/core/runtime.d b/src/core/runtime.d index 08aa95187a..793e98eb08 100644 --- a/src/core/runtime.d +++ b/src/core/runtime.d @@ -888,6 +888,16 @@ private: auto sym = demangle(buf[symBeg .. symEnd], fixbuf[symBeg .. $]); + version (Shared) + { + import core.internal.cpptrace; + + if (sym == buf[symBeg .. symEnd]) // Retry with demangleCppTrace + { + sym = demangleCppTrace(buf[symBeg .. symEnd], fixbuf[symBeg .. $]); + } + } + if (sym.ptr !is fixbuf.ptr + symBeg) { // demangle reallocated the buffer, copy the symbol to fixbuf diff --git a/src/core/sys/windows/stacktrace.d b/src/core/sys/windows/stacktrace.d index bb9ed28eba..794287db69 100644 --- a/src/core/sys/windows/stacktrace.d +++ b/src/core/sys/windows/stacktrace.d @@ -15,6 +15,7 @@ version (Windows): import core.demangle; import core.runtime; +import core.internal.cpptrace; import core.stdc.stdlib; import core.stdc.string; import core.sys.windows.dbghelp; @@ -295,7 +296,14 @@ private: size_t decodeIndex = 0; tempSymName = decodeDmdString(tempSymName, decodeIndex); } - res ~= demangle(tempSymName, demangleBuf); + auto demangledName = demangle(tempSymName, demangleBuf); + + if (demangledName == tempSymName) // Retry with demangleCppTrace + { + demangledName = demangleCppTrace(tempSymName, demangleBuf); + } + + res ~= demangledName; return res; } diff --git a/src/rt/backtrace/dwarf.d b/src/rt/backtrace/dwarf.d index 881453c0e3..caafa757c3 100644 --- a/src/rt/backtrace/dwarf.d +++ b/src/rt/backtrace/dwarf.d @@ -369,7 +369,21 @@ const(char)[] getDemangledSymbol(const(char)[] btSymbol, return ref char[1024] b { import core.demangle; const mangledName = getMangledSymbolName(btSymbol); - return !mangledName.length ? buffer[0..0] : demangle(mangledName, buffer[]); + if (!mangledName.length) return buffer[0..0]; + + auto demangledName = demangle(mangledName, buffer[]); + + version (Shared) + { + import core.internal.cpptrace; + + if (demangledName == mangledName) // Retry with demangleCppTrace + { + demangledName = demangleCppTrace(mangledName, buffer[]); + } + } + + return demangledName; } T read(T)(ref const(ubyte)[] buffer) @nogc nothrow diff --git a/test/exceptions/Makefile b/test/exceptions/Makefile index 88ba7dbd9c..2ab5aa3b9e 100644 --- a/test/exceptions/Makefile +++ b/test/exceptions/Makefile @@ -3,9 +3,16 @@ include ../common.mak TESTS=stderr_msg unittest_assert invalid_memory_operation unknown_gc static_dtor \ future_message refcounted rt_trap_exceptions_drt catch_in_finally +ifeq ($(shell test -e $(DRUNTIMESO) && echo 0), 0) + SHARED=1 +endif + ifeq ($(OS)-$(BUILD),linux-debug) TESTS+=line_trace long_backtrace_trunc rt_trap_exceptions LINE_TRACE_DFLAGS:=-L--export-dynamic + ifeq ($(SHARED), 1) + TESTS+=cpp_trace + endif endif ifeq ($(OS),linux) TESTS+=rt_trap_exceptions_drt_gdb @@ -13,14 +20,23 @@ endif ifeq ($(OS)-$(BUILD),freebsd-debug) TESTS+=line_trace long_backtrace_trunc LINE_TRACE_DFLAGS:=-L--export-dynamic + ifeq (SHARED, 1) + TESTS+=cpp_trace + endif endif ifeq ($(OS)-$(BUILD),dragonflybsd-debug) TESTS+=line_trace long_backtrace_trunc LINE_TRACE_DFLAGS:=-L--export-dynamic + ifeq (SHARED, 1) + TESTS+=cpp_trace + endif endif ifeq ($(OS)-$(BUILD),osx-debug) TESTS+=line_trace long_backtrace_trunc LINE_TRACE_DFLAGS:= + ifeq (SHARED, 1) + TESTS+=cpp_trace + endif endif ifeq ($(BUILD),debug) @@ -56,6 +72,14 @@ $(ROOT)/chain.done: $(ROOT)/chain @rm -f $(ROOT)/chain.output @touch $@ +$(ROOT)/cpp_trace.done: $(ROOT)/cpp_trace + @echo Testing cpp_trace + $(QUIET)$(TIMELIMIT)$(ROOT)/cpp_trace $(RUN_ARGS) > $(ROOT)/cpp_trace.output + # Use sed to canonicalize cpp_trace.output and compare against expected output in cpp_trace.exp + $(QUIET)$(SED) "s/\[0x[0-9a-f]*\]/\[ADDR\]/g; s/scope //g; s/Nl//g" $(ROOT)/cpp_trace.output | $(DIFF) cpp_trace.exp - + @rm -f $(ROOT)/cpp_trace.output + @touch $@ + $(ROOT)/stderr_msg.done: STDERR_EXP="stderr_msg msg" $(ROOT)/unittest_assert.done: STDERR_EXP="unittest_assert msg" $(ROOT)/invalid_memory_operation.done: STDERR_EXP="InvalidMemoryOperationError" @@ -99,6 +123,8 @@ $(ROOT)/line_trace: DFLAGS+=$(LINE_TRACE_DFLAGS) $(ROOT)/rt_trap_exceptions_drt: DFLAGS+=-g $(ROOT)/assert_fail: DFLAGS+=-checkaction=context $(ROOT)/refcounted: DFLAGS+=-dip1008 +$(ROOT)/cpp_trace.done: RUN_ARGS="--DRT-cpptrace=enable:y" +$(ROOT)/cpp_trace: DFLAGS+=-L$(DRUNTIMESO) $(LINE_TRACE_DFLAGS) $(ROOT)/%: $(SRC)/%.d $(DMD) $(DRUNTIME) $(QUIET)$(DMD) $(DFLAGS) -of$@ $< diff --git a/test/exceptions/cpp_trace.exp b/test/exceptions/cpp_trace.exp new file mode 100644 index 0000000000..8bd9dc4693 --- /dev/null +++ b/test/exceptions/cpp_trace.exp @@ -0,0 +1,12 @@ +object.Exception@src/cpp_trace.d(40): exception +---------------- +src/cpp_trace.d:40 [C++] f1() [ADDR] +src/cpp_trace.d:5 _Dmain [ADDR] +object.Exception@src/cpp_trace.d(47): exception +---------------- +src/cpp_trace.d:47 [C++] S1::f1() [ADDR] +src/cpp_trace.d:16 _Dmain [ADDR] +object.Exception@src/cpp_trace.d(57): exception +---------------- +src/cpp_trace.d:57 [C++] C1::f1(unsigned char, unsigned char*, unsigned char&) [ADDR] +src/cpp_trace.d:28 _Dmain [ADDR] diff --git a/test/exceptions/src/cpp_trace.d b/test/exceptions/src/cpp_trace.d new file mode 100644 index 0000000000..7d6eb50441 --- /dev/null +++ b/test/exceptions/src/cpp_trace.d @@ -0,0 +1,60 @@ +void main() +{ + try + { + f1(); + } + catch (Exception e) + { + import core.stdc.stdio; + auto str = e.toString(); + printf("%.*s\n", cast(int)str.length, str.ptr); + } + + try + { + S1.f1(); + } + catch (Exception e) + { + import core.stdc.stdio; + auto str = e.toString(); + printf("%.*s\n", cast(int)str.length, str.ptr); + } + + try + { + ubyte nothing; + C1!ubyte.f1(nothing, ¬hing, nothing); + } + catch (Exception e) + { + import core.stdc.stdio; + auto str = e.toString(); + printf("%.*s\n", cast(int)str.length, str.ptr); + } +} + +extern(C++) void f1() +{ + throw new Exception("exception"); +} + +extern(C++) struct S1 +{ + static void f1() + { + throw new Exception("exception"); + } +} + +extern(C++) +{ + class C1(T) + { + static T f1(T arg1, T* arg2, ref T arg3) + { + throw new Exception("exception"); + } + } +}