diff --git a/.ci/docker/sdk-linux/Dockerfile b/.ci/docker/sdk-linux/Dockerfile index 0af491059..57ff063bd 100644 --- a/.ci/docker/sdk-linux/Dockerfile +++ b/.ci/docker/sdk-linux/Dockerfile @@ -19,11 +19,23 @@ RUN apt update \ && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" \ && apt -qq update \ && apt-get -qq install -y docker-ce docker-ce-cli containerd.io \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* + --no-install-recommends # Install terraform RUN curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add - \ && apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ && apt-get update \ - && apt-get install terraform \ No newline at end of file + && apt-get -qq install -y terraform + +# Install rust +ENV RUSTUP_HOME='/cargo' +ENV CARGO_HOME='/cargo' +ENV PATH="/cargo/bin:${PATH}" +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +RUN rustup default 1.54.0 + +# Install cargo make +RUN apt-get -qq install -y build-essential libssl-dev pkg-config \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* +RUN cargo install --force cargo-make diff --git a/.ci/linux/remove-projects.sh b/.ci/linux/remove-projects.sh index c800f6d8b..af7cf0d0a 100755 --- a/.ci/linux/remove-projects.sh +++ b/.ci/linux/remove-projects.sh @@ -8,3 +8,9 @@ set -euxo pipefail dotnet sln remove sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj dotnet sln remove src/Elastic.Apm.AspNetFullFramework/Elastic.Apm.AspNetFullFramework.csproj dotnet sln remove test/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj + +# Remove Startup hooks test project. tested separately +dotnet sln remove test/Elastic.Apm.StartupHook.Tests/Elastic.Apm.StartupHook.Tests.csproj + +# Remove Profiler test project. tested separately +dotnet sln remove test/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj diff --git a/.ci/linux/test-profiler.sh b/.ci/linux/test-profiler.sh new file mode 100755 index 000000000..b14c147f3 --- /dev/null +++ b/.ci/linux/test-profiler.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# This script runs the tests and stored the test ouptut in a JUnit xml file +# defined in the test_results folder +# +set -euxo pipefail + +cargo make test + +dotnet test -c Release test/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj \ + --verbosity normal \ + --results-directory target \ + --diag target/diag-profiler.log \ + --logger:"junit;LogFilePath=junit-{framework}-{assembly}.xml;MethodFormat=Class;FailureBodyFormat=Verbose" \ + --collect:"XPlat Code Coverage" \ + --settings coverlet.runsettings \ + --blame-hang \ + --blame-hang-timeout 10m \ + /p:CollectCoverage=true \ + /p:CoverletOutputFormat=cobertura \ + /p:CoverletOutput=test_results/Coverage/ \ + /p:Threshold=0 \ + /p:ThresholdType=branch \ + /p:ThresholdStat=total diff --git a/.ci/linux/test-startuphooks.sh b/.ci/linux/test-startuphooks.sh new file mode 100755 index 000000000..6f9985ef7 --- /dev/null +++ b/.ci/linux/test-startuphooks.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +# This script runs the tests and stored the test ouptut in a JUnit xml file +# defined in the test_results folder +# +set -euxo pipefail + +dotnet test -c Release test/Elastic.Apm.StartupHook.Tests/Elastic.Apm.StartupHook.Tests.csproj --no-build \ + --verbosity normal \ + --results-directory target \ + --diag target/diag-startuphook.log \ + --logger:"junit;LogFilePath=junit-{framework}-{assembly}.xml;MethodFormat=Class;FailureBodyFormat=Verbose" diff --git a/.ci/linux/test.sh b/.ci/linux/test.sh index efbce83ca..ea28b9425 100755 --- a/.ci/linux/test.sh +++ b/.ci/linux/test.sh @@ -5,12 +5,9 @@ # set -euxo pipefail -# Remove Full Framework projects +# Remove projects .ci/linux/remove-projects.sh -# Build agent zip file -./build.sh agent-zip - # Run tests for all solution dotnet test -c Release ElasticApmAgent.sln \ --verbosity normal \ diff --git a/.ci/windows/dotnet.bat b/.ci/windows/dotnet.bat index 954e24d89..d60b0d6cb 100644 --- a/.ci/windows/dotnet.bat +++ b/.ci/windows/dotnet.bat @@ -11,4 +11,7 @@ dotnet sln remove test/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.cspro :: Remove startup hooks tests, which are tested separately- require agent zip to be built dotnet sln remove test/Elastic.Apm.StartupHook.Tests/Elastic.Apm.StartupHook.Tests.csproj +:: Remove profiler tests, which are tested separately- require profiler to be built +dotnet sln remove test/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj + dotnet build -c Release --verbosity detailed diff --git a/.ci/windows/msbuild-tools.ps1 b/.ci/windows/msbuild-tools.ps1 index 5d24b47d7..5de82a1c2 100644 --- a/.ci/windows/msbuild-tools.ps1 +++ b/.ci/windows/msbuild-tools.ps1 @@ -13,4 +13,4 @@ Start-Process "C:\tools\vs_BuildTools.exe" -ArgumentList "--add", "Microsoft.Vis "--add", "Microsoft.VisualStudio.Workload.NetCoreBuildTools;includeRecommended;includeOptional", ` "--add", "Microsoft.VisualStudio.Workload.MSBuildTools", ` "--add", "Microsoft.VisualStudio.Workload.WebBuildTools;includeRecommended;includeOptional", ` - "--quiet", "--norestart", "--nocache" -NoNewWindow -Wait + "--quiet", "--norestart", "--nocache" -NoNewWindow -Wait; diff --git a/.ci/windows/msbuild.bat b/.ci/windows/msbuild.bat index 1a58ad9a8..30a5f37be 100644 --- a/.ci/windows/msbuild.bat +++ b/.ci/windows/msbuild.bat @@ -3,6 +3,8 @@ :: echo "Prepare context for VsDevCmd.bat" call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools\VsDevCmd.bat" + +dotnet nuget add source --name nuget.org https://api.nuget.org/v3/index.json nuget restore -verbosity detailed -NonInteractive msbuild /p:Configuration=Release diff --git a/.ci/windows/test-profiler.bat b/.ci/windows/test-profiler.bat new file mode 100644 index 000000000..cf6c84a11 --- /dev/null +++ b/.ci/windows/test-profiler.bat @@ -0,0 +1,17 @@ +cargo make test + +dotnet test -c Release test\Elastic.Apm.Profiler.Managed.Tests\Elastic.Apm.Profiler.Managed.Tests.csproj ^ + --verbosity normal ^ + --results-directory target ^ + --diag target\diag-profiler.log ^ + --logger:"junit;LogFilePath=junit-{framework}-{assembly}.xml;MethodFormat=Class;FailureBodyFormat=Verbose" ^ + --collect:"XPlat Code Coverage" ^ + --settings coverlet.runsettings ^ + --blame-hang ^ + --blame-hang-timeout 10m ^ + /p:CollectCoverage=true ^ + /p:CoverletOutputFormat=cobertura ^ + /p:CoverletOutput=test_results/Coverage/ ^ + /p:Threshold=0 ^ + /p:ThresholdType=branch ^ + /p:ThresholdStat=total diff --git a/.ci/windows/test-zip.bat b/.ci/windows/test-startuphooks.bat similarity index 64% rename from .ci/windows/test-zip.bat rename to .ci/windows/test-startuphooks.bat index f9d612417..523359fa8 100644 --- a/.ci/windows/test-zip.bat +++ b/.ci/windows/test-startuphooks.bat @@ -1,4 +1,4 @@ -dotnet test -c Release test\Elastic.Apm.StartupHook.Tests --no-build ^ +dotnet test -c Release test\Elastic.Apm.StartupHook.Tests\Elastic.Apm.StartupHook.Tests.csproj --no-build ^ --verbosity normal ^ --results-directory target ^ --diag target\diag-startuphook.log ^ diff --git a/.ci/windows/tools.ps1 b/.ci/windows/tools.ps1 index bedd117ca..e76984882 100644 --- a/.ci/windows/tools.ps1 +++ b/.ci/windows/tools.ps1 @@ -8,11 +8,11 @@ Add-WindowsFeature NET-Framework-45-ASPNET Add-WindowsFeature Web-Asp-Net45 # Install .Net SDKs +Write-Host "Install .Net SDKs" choco install dotnetcore-sdk -m -y --no-progress -r --version 2.1.505 choco install dotnetcore-sdk -m -y --no-progress -r --version 2.2.104 choco install dotnetcore-sdk -m -y --no-progress -r --version 3.0.103 choco install dotnetcore-sdk -m -y --no-progress -r --version 3.1.100 - choco install dotnet-sdk -m -y --no-progress -r --version 5.0.100 # Install NuGet Tool @@ -20,3 +20,17 @@ choco install nuget.commandline -y --no-progress -r --version 5.8.0 # Install vswhere choco install vswhere -y --no-progress -r --version 2.8.4 + +# Install rust +Write-Host "Install rust-ms" +choco install rust-ms -y --no-progress -r --version 1.54.0 + +# Download and install cargo make +Write-Host "Download cargo-make" +Invoke-WebRequest -UseBasicParsing ` + -Uri "https://github.com/sagiegurari/cargo-make/releases/download/0.35.0/cargo-make-v0.35.0-x86_64-pc-windows-msvc.zip" ` + -OutFile "C:\tools\cargo-make.zip" + +Write-Host "Unzip cargo-make" +New-Item -ItemType directory -Path C:\tools\cargo +Expand-Archive -LiteralPath C:\tools\cargo-make.zip -DestinationPath C:\tools\cargo diff --git a/.ci/windows/zip.bat b/.ci/windows/zip.bat deleted file mode 100644 index 21f7471d1..000000000 --- a/.ci/windows/zip.bat +++ /dev/null @@ -1 +0,0 @@ -.\build.bat agent-zip diff --git a/.gitignore b/.gitignore index ff00504e0..4b5008196 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,9 @@ terraform.tfstate.backup # This file is generated on build src/Elastic.Apm.MongoDb/LICENSE + +# Rust files +target/ +Cargo.lock +expanded.rs + diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..b174224af --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,4 @@ +reorder_imports = true + +# requires nightly rustfmt +imports_granularity = "Crate" \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70bdbf2cd..3d8d3f5b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,10 +12,54 @@ feedback and ideas are always welcome. ## Prerequisites -In order to build the source code of the .NET APM Agent you need - * .NET Core 2.1 or later +### .NET source -You can use any IDE that supports .NET Core development and you can use any OS that is supported by .NET Core. +In order to build the .NET source code, you'll need + * [.NET 5.0 or later](https://dotnet.microsoft.com/download/dotnet/5.0) + * **If** you're running on Windows **and** also wish to build projects that target .NET Framework, +you'll need a minimum of .NET Framework 4.6.1 installed. + +You can use any IDE that supports .NET development, and you can use any OS that is supported by .NET. + +### Rust source + +In order to build the CLR profiler source code, you'll need + * [Rust 1.54 or later](https://www.rust-lang.org/tools/install) + * [Cargo make](https://github.com/sagiegurari/cargo-make#installation) + +You can use any IDE that supports Rust development; we typically use [CLion](https://www.jetbrains.com/clion/) +with the [Rust plugin](https://plugins.jetbrains.com/plugin/8182-rust/docs), +or [VS Code](https://code.visualstudio.com/) +with the [Rust extension](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust). + +## Work flow + +The solution contains a build project for performing build tasks, which can be invoked +with the `build.bat` or `build.sh` scripts in the solution root. + +To see the list of targets + +Windows +```shell +.\build.bat --list-targets +``` + +Linux +```shell +./build.sh --list-targets +``` + +To perform a build of projects (the default task) + +Windows +```shell +.\build.bat +``` + +Linux +```shell +./build.sh +``` ## Code contributions (please read this before your first PR) diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..99b431196 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] + +members = [ + "src/elastic_apm_profiler", +] + +# default release profile: https://doc.rust-lang.org/cargo/reference/profiles.html#release +[profile.release] +lto = true +codegen-units = 1 +# reduce binary size by not unwinding on panic to get a backtrace +#panic = "abort" \ No newline at end of file diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index 105f4bdb0..c07e07398 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -149,6 +149,34 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.CosmosDb" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.CosmosDb.Tests", "test\Elastic.Apm.Azure.CosmosDb.Tests\Elastic.Apm.Azure.CosmosDb.Tests.csproj", "{0EEA16C4-C7DF-4D90-94C2-009417D765AC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Profiler.Managed", "src\Elastic.Apm.Profiler.Managed\Elastic.Apm.Profiler.Managed.csproj", "{A88A3877-05D6-45B2-94E4-5C583BD6EEDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Profiler.Managed.Loader", "src\Elastic.Apm.Profiler.Managed.Loader\Elastic.Apm.Profiler.Managed.Loader.csproj", "{F1DD170E-2DD8-4CBD-A2BB-C19CF0033982}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NpgsqlSample", "sample\NpgsqlSample\NpgsqlSample.csproj", "{BA2D3FCD-C25E-4E34-9A30-BE1FB16E1841}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Profiler.Managed.Tests", "test\Elastic.Apm.Profiler.Managed.Tests\Elastic.Apm.Profiler.Managed.Tests.csproj", "{4983083A-87AD-496E-B0BC-84C8097D0590}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.AdoNet", "sample\Elastic.Apm.AdoNet\Elastic.Apm.AdoNet.csproj", "{14BEE605-6180-4B9E-87F5-A24BA79461B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.AdoNet.NetStandard", "sample\Elastic.Apm.AdoNet.NetStandard\Elastic.Apm.AdoNet.NetStandard.csproj", "{AAB983A1-6080-449D-95D8-7E7BA05DDFAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySqlDataSample", "sample\MySqlDataSample\MySqlDataSample.csproj", "{346D3A9A-1296-4915-BB6C-8533731D3B3B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqliteSample", "sample\SqliteSample\SqliteSample.csproj", "{F72182DC-A933-4C4B-8321-830D0DFB857C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OracleManagedDataAccessSample", "sample\OracleManagedDataAccessSample\OracleManagedDataAccessSample.csproj", "{FFA9A7E4-954F-47A1-A9FD-0FE07EB846C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OracleManagedDataAccessCoreSample", "sample\OracleManagedDataAccessCoreSample\OracleManagedDataAccessCoreSample.csproj", "{8CF18906-150C-4AD9-B108-2A39B31105C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Profiler.IntegrationsGenerator", "src\Elastic.Apm.Profiler.IntegrationsGenerator\Elastic.Apm.Profiler.IntegrationsGenerator.csproj", "{B7B0104C-CADC-42E0-B08B-5187926BFF6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Profiler.Managed.Core", "src\Elastic.Apm.Profiler.Managed.Core\Elastic.Apm.Profiler.Managed.Core.csproj", "{4E235C21-5637-4498-8335-134ACAA4884E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClientSample", "sample\SqlClientSample\SqlClientSample.csproj", "{456A8639-FE1B-426A-9C72-5252AB4D3AD5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KafkaSample", "sample\KafkaSample\KafkaSample.csproj", "{2B23487A-B340-4F5C-A49B-9B829F437A5A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -375,6 +403,62 @@ Global {0EEA16C4-C7DF-4D90-94C2-009417D765AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0EEA16C4-C7DF-4D90-94C2-009417D765AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0EEA16C4-C7DF-4D90-94C2-009417D765AC}.Release|Any CPU.Build.0 = Release|Any CPU + {A88A3877-05D6-45B2-94E4-5C583BD6EEDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A88A3877-05D6-45B2-94E4-5C583BD6EEDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A88A3877-05D6-45B2-94E4-5C583BD6EEDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A88A3877-05D6-45B2-94E4-5C583BD6EEDC}.Release|Any CPU.Build.0 = Release|Any CPU + {F1DD170E-2DD8-4CBD-A2BB-C19CF0033982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1DD170E-2DD8-4CBD-A2BB-C19CF0033982}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1DD170E-2DD8-4CBD-A2BB-C19CF0033982}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1DD170E-2DD8-4CBD-A2BB-C19CF0033982}.Release|Any CPU.Build.0 = Release|Any CPU + {BA2D3FCD-C25E-4E34-9A30-BE1FB16E1841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA2D3FCD-C25E-4E34-9A30-BE1FB16E1841}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA2D3FCD-C25E-4E34-9A30-BE1FB16E1841}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA2D3FCD-C25E-4E34-9A30-BE1FB16E1841}.Release|Any CPU.Build.0 = Release|Any CPU + {4983083A-87AD-496E-B0BC-84C8097D0590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4983083A-87AD-496E-B0BC-84C8097D0590}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4983083A-87AD-496E-B0BC-84C8097D0590}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4983083A-87AD-496E-B0BC-84C8097D0590}.Release|Any CPU.Build.0 = Release|Any CPU + {14BEE605-6180-4B9E-87F5-A24BA79461B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14BEE605-6180-4B9E-87F5-A24BA79461B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14BEE605-6180-4B9E-87F5-A24BA79461B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14BEE605-6180-4B9E-87F5-A24BA79461B5}.Release|Any CPU.Build.0 = Release|Any CPU + {AAB983A1-6080-449D-95D8-7E7BA05DDFAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAB983A1-6080-449D-95D8-7E7BA05DDFAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAB983A1-6080-449D-95D8-7E7BA05DDFAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAB983A1-6080-449D-95D8-7E7BA05DDFAA}.Release|Any CPU.Build.0 = Release|Any CPU + {346D3A9A-1296-4915-BB6C-8533731D3B3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {346D3A9A-1296-4915-BB6C-8533731D3B3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {346D3A9A-1296-4915-BB6C-8533731D3B3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {346D3A9A-1296-4915-BB6C-8533731D3B3B}.Release|Any CPU.Build.0 = Release|Any CPU + {F72182DC-A933-4C4B-8321-830D0DFB857C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F72182DC-A933-4C4B-8321-830D0DFB857C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F72182DC-A933-4C4B-8321-830D0DFB857C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F72182DC-A933-4C4B-8321-830D0DFB857C}.Release|Any CPU.Build.0 = Release|Any CPU + {FFA9A7E4-954F-47A1-A9FD-0FE07EB846C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFA9A7E4-954F-47A1-A9FD-0FE07EB846C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFA9A7E4-954F-47A1-A9FD-0FE07EB846C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFA9A7E4-954F-47A1-A9FD-0FE07EB846C6}.Release|Any CPU.Build.0 = Release|Any CPU + {8CF18906-150C-4AD9-B108-2A39B31105C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CF18906-150C-4AD9-B108-2A39B31105C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CF18906-150C-4AD9-B108-2A39B31105C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CF18906-150C-4AD9-B108-2A39B31105C5}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B0104C-CADC-42E0-B08B-5187926BFF6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7B0104C-CADC-42E0-B08B-5187926BFF6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B0104C-CADC-42E0-B08B-5187926BFF6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7B0104C-CADC-42E0-B08B-5187926BFF6F}.Release|Any CPU.Build.0 = Release|Any CPU + {4E235C21-5637-4498-8335-134ACAA4884E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E235C21-5637-4498-8335-134ACAA4884E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E235C21-5637-4498-8335-134ACAA4884E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E235C21-5637-4498-8335-134ACAA4884E}.Release|Any CPU.Build.0 = Release|Any CPU + {456A8639-FE1B-426A-9C72-5252AB4D3AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {456A8639-FE1B-426A-9C72-5252AB4D3AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {456A8639-FE1B-426A-9C72-5252AB4D3AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {456A8639-FE1B-426A-9C72-5252AB4D3AD5}.Release|Any CPU.Build.0 = Release|Any CPU + {2B23487A-B340-4F5C-A49B-9B829F437A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B23487A-B340-4F5C-A49B-9B829F437A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B23487A-B340-4F5C-A49B-9B829F437A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B23487A-B340-4F5C-A49B-9B829F437A5A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -435,6 +519,20 @@ Global {FB07C133-C353-4061-A308-B9A6EF48AB7E} = {267A241E-571F-458F-B04C-B6C4DE79E735} {FC0276AB-9063-4DB9-9078-DCEF1F1091A9} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} {0EEA16C4-C7DF-4D90-94C2-009417D765AC} = {267A241E-571F-458F-B04C-B6C4DE79E735} + {A88A3877-05D6-45B2-94E4-5C583BD6EEDC} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} + {F1DD170E-2DD8-4CBD-A2BB-C19CF0033982} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} + {BA2D3FCD-C25E-4E34-9A30-BE1FB16E1841} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {4983083A-87AD-496E-B0BC-84C8097D0590} = {267A241E-571F-458F-B04C-B6C4DE79E735} + {14BEE605-6180-4B9E-87F5-A24BA79461B5} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {AAB983A1-6080-449D-95D8-7E7BA05DDFAA} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {346D3A9A-1296-4915-BB6C-8533731D3B3B} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {F72182DC-A933-4C4B-8321-830D0DFB857C} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {FFA9A7E4-954F-47A1-A9FD-0FE07EB846C6} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {8CF18906-150C-4AD9-B108-2A39B31105C5} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {B7B0104C-CADC-42E0-B08B-5187926BFF6F} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} + {4E235C21-5637-4498-8335-134ACAA4884E} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} + {456A8639-FE1B-426A-9C72-5252AB4D3AD5} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {2B23487A-B340-4F5C-A49B-9B829F437A5A} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69E02FD9-C9DE-412C-AB6B-5B8BECC6BFA5} diff --git a/Jenkinsfile b/Jenkinsfile index 9a117e663..04860d208 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,7 +3,7 @@ @Library('apm@current') _ pipeline { - agent { label 'linux && immutable' } + agent { label 'linux && immutable && docker' } environment { REPO = 'apm-agent-dotnet' // keep it short to avoid the 248 characters PATH limit in Windows @@ -99,7 +99,11 @@ pipeline { dotnet(){ sh '.ci/linux/build.sh' whenTrue(isPR()) { + // build nuget packages and profiler sh(label: 'Package', script: '.ci/linux/release.sh true') + sh label: 'Rustup', script: 'rustup default 1.54.0' + sh label: 'Cargo make', script: 'cargo install --force cargo-make' + sh(label: 'Build profiler', script: './build.sh profiler-zip') } } } @@ -147,10 +151,56 @@ pipeline { } } } + stage('Startup Hook Tests') { + steps { + withGithubNotify(context: 'Test startup hooks - Linux', tab: 'tests') { + deleteDir() + unstash 'source' + dir("${BASE_DIR}"){ + dotnet(){ + sh label: 'Build', script: './build.sh agent-zip' + sh label: 'Test & coverage', script: '.ci/linux/test-startuphooks.sh' + } + } + } + } + post { + always { + reportTests() + } + unsuccessful { + archiveArtifacts(allowEmptyArchive: true, artifacts: "${MSBUILDDEBUGPATH}/**/MSBuild_*.failure.txt") + } + } + } + stage('Profiler Tests') { + steps { + withGithubNotify(context: 'Test profiler - Linux', tab: 'tests') { + deleteDir() + unstash 'source' + dir("${BASE_DIR}"){ + dotnet(){ + sh label: 'Rustup', script: 'rustup default 1.54.0' + sh label: 'Cargo make', script: 'cargo install --force cargo-make' + sh label: 'Build', script: './build.sh profiler-zip' + sh label: 'Test & coverage', script: '.ci/linux/test-profiler.sh' + } + } + } + } + post { + always { + reportTests() + } + unsuccessful { + archiveArtifacts(allowEmptyArchive: true, artifacts: "${MSBUILDDEBUGPATH}/**/MSBuild_*.failure.txt") + } + } + } } } stage('Windows .NET Framework'){ - agent { label 'windows-2019-immutable' } + agent { label 'windows-2019 && immutable' } options { skipDefaultCheckout() } environment { HOME = "${env.WORKSPACE}" @@ -202,8 +252,6 @@ pipeline { unstash 'source' dir("${BASE_DIR}"){ powershell label: 'Install test tools', script: '.ci\\windows\\test-tools.ps1' - bat label: 'Prepare solution', script: '.ci/windows/prepare-test.bat' - bat label: 'Build', script: '.ci/windows/msbuild.bat' bat label: 'Test & coverage', script: '.ci/windows/testnet461.bat' } } @@ -247,12 +295,13 @@ pipeline { } } stage('Windows .NET Core'){ - agent { label 'windows-2019-immutable' } + agent { label 'windows-2019 && immutable' } options { skipDefaultCheckout() } environment { HOME = "${env.WORKSPACE}" DOTNET_ROOT = "C:\\Program Files\\dotnet" - PATH = "${env.DOTNET_ROOT};${env.DOTNET_ROOT}\\tools;${env.PATH};${env.HOME}\\bin" + CARGO_MAKE_HOME = "C:\\tools\\cargo" + PATH = "${PATH};${env.DOTNET_ROOT};${env.DOTNET_ROOT}\\tools;${env.PATH};${env.HOME}\\bin;${env.CARGO_MAKE_HOME};${env.USERPROFILE}\\.cargo\\bin" MSBUILDDEBUGPATH = "${env.WORKSPACE}" } stages{ @@ -279,11 +328,20 @@ pipeline { unstash 'source' dir("${BASE_DIR}"){ bat label: 'Build', script: '.ci/windows/dotnet.bat' + whenTrue(isPR()) { + bat(label: 'Build agent', script: './build.bat agent-zip') + bat(label: 'Build profiler', script: './build.bat profiler-zip') + } } } } } post { + success { + whenTrue(isPR()) { + archiveArtifacts(allowEmptyArchive: true, artifacts: "${BASE_DIR}/build/output/*.zip") + } + } unsuccessful { archiveArtifacts(allowEmptyArchive: true, artifacts: "${MSBUILDDEBUGPATH}/**/MSBuild_*.failure.txt") @@ -326,9 +384,32 @@ pipeline { dir("${BASE_DIR}"){ powershell label: 'Install test tools', script: '.ci\\windows\\test-tools.ps1' retry(3) { - bat label: 'Build', script: '.ci/windows/zip.bat' + bat label: 'Build', script: './build.bat agent-zip' + } + bat label: 'Test & coverage', script: '.ci/windows/test-startuphooks.bat' + } + } + } + post { + always { + reportTests() + } + unsuccessful { + archiveArtifacts(allowEmptyArchive: true, artifacts: "${MSBUILDDEBUGPATH}/**/MSBuild_*.failure.txt") + } + } + } + stage('Profiler Tests') { + steps { + withGithubNotify(context: 'Test profiler - Windows', tab: 'tests') { + cleanDir("${WORKSPACE}/${BASE_DIR}") + unstash 'source' + dir("${BASE_DIR}"){ + powershell label: 'Install test tools', script: '.ci\\windows\\test-tools.ps1' + retry(3) { + bat label: 'Build', script: './build.bat profiler-zip' } - bat label: 'Test & coverage', script: '.ci/windows/test-zip.bat' + bat label: 'Test & coverage', script: '.ci/windows/test-profiler.bat' } } } @@ -541,7 +622,7 @@ def dotnet(Closure body){ ./dotnet-install.sh --install-dir "\${DOTNET_ROOT}" -version '2.1.505' ./dotnet-install.sh --install-dir "\${DOTNET_ROOT}" -version '3.0.103' ./dotnet-install.sh --install-dir "\${DOTNET_ROOT}" -version '3.1.100' - ./dotnet-install.sh --install-dir "\${DOTNET_ROOT}" -version '5.0.203' + ./dotnet-install.sh --install-dir "\${DOTNET_ROOT}" -version '5.0.100' """) withAzureCredentials(path: "${homePath}", credentialsFile: '.credentials.json') { withTerraform(){ diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 000000000..861e876aa --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,131 @@ +# Licensed to Elasticsearch B.V under +# one or more agreements. +# Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information + +[config] +default_to_workspace = false + +[env] +DOTNET_VERSION = "net5.0" +DOTNET_CONFIG = "Debug" +CORECLR_ENABLE_PROFILING = { value = "1", condition = { env_not_set = ["CORECLR_ENABLE_PROFILING"] } } +CORECLR_PROFILER = { value = "{FA65FE15-F085-4681-9B20-95E04F6C03CC}", condition = { env_not_set = ["CORECLR_PROFILER"] } } +ELASTIC_APM_PROFILER_HOME = { value = "${CARGO_MAKE_WORKING_DIRECTORY}/src/Elastic.Apm.Profiler.Managed/bin/Release", condition = { env_not_set = ["ELASTIC_APM_PROFILER_HOME"] } } +ELASTIC_APM_PROFILER_INTEGRATIONS = { value = "${CARGO_MAKE_WORKING_DIRECTORY}/src/Elastic.Apm.Profiler.Managed/integrations.yml", condition = { env_not_set = ["ELASTIC_APM_PROFILER_INTEGRATIONS"] } } +ELASTIC_APM_PROFILER_CALLTARGET_ENABLED = { value = "true", condition = { env_not_set = ["ELASTIC_APM_PROFILER_CALLTARGET_ENABLED"] } } +ELASTIC_APM_PROFILER_LOG = { value = "debug", condition = { env_not_set = ["ELASTIC_APM_PROFILER_LOG"] } } +ELASTIC_APM_PROFILER_LOG_IL = { value = "true", condition = { env_not_set = ["ELASTIC_APM_PROFILER_LOG_IL"] } } +ELASTIC_APM_PROFILER_LOG_DIR = { value = "logs", condition = { env_not_set = ["ELASTIC_APM_PROFILER_LOG_DIR"] } } +ELASTIC_APM_PROFILER_LOG_TARGETS = { value = "file;stdout", condition = { env_not_set = ["ELASTIC_APM_PROFILER_LOG_TARGETS"] } } +SAMPLE_APP = { value = "KafkaSample", condition = { env_not_set = ["SAMPLE_APP"] } } + +[tasks.build-loader] +description = "Builds Managed Profiler .NET loader" +command = "dotnet" +args = ["build", "-c", "Release", "./src/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj"] + +[tasks.build] +description = "Builds CLR Profiler" +# loader assembly is embedded in the profiler +dependencies = ["build-loader"] + +[tasks.build-release] +description = "Builds CLR Profiler for release" +# loader assembly is embedded in the profiler +dependencies = ["build-loader"] + +[tasks.build-integrations] +description = "Builds Managed Profiler .NET integrations" +command = "dotnet" +args = ["build", "-c", "Release", "./src/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj"] + +[tasks.build-example] +description = "Builds Example .NET project to instrument" +command = "dotnet" +args = ["build", "-c", "${DOTNET_CONFIG}", "./sample/${SAMPLE_APP}/${SAMPLE_APP}.csproj"] + +[tasks.generate-integrations] +description = "Generates integrations.yml file" +command = "dotnet" +args = [ + "run", + "--project", "./src/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj", + "--", + "-i", "./src/Elastic.Apm.Profiler.Managed/bin/Release/netstandard2.0/Elastic.Apm.Profiler.Managed.dll", + "-o", "./src/Elastic.Apm.Profiler.Managed", + "-f", "yml"] +dependencies = ["build-integrations"] + +[tasks.expand] +clear = true +script = ''' +cargo expand --manifest-path src/elastic_apm_profiler/Cargo.toml --color never > expanded.rs +''' + +[tasks.set-profiler-env] +private = true +env = { "CORECLR_PROFILER_PATH" = "${CARGO_MAKE_WORKING_DIRECTORY}/target/release/libelastic_apm_profiler.so" } + +[tasks.set-profiler-env.mac] +env = { "CORECLR_PROFILER_PATH" = "${CARGO_MAKE_WORKING_DIRECTORY}/target/release/libelastic_apm_profiler.dylib" } + +[tasks.set-profiler-env.windows] +env = { "CORECLR_PROFILER_PATH" = "${CARGO_MAKE_WORKING_DIRECTORY}\\target\\release\\elastic_apm_profiler.dll" } + +[tasks.run-dotnet] +command = "dotnet" +args = ["${CARGO_MAKE_WORKING_DIRECTORY}/sample/${SAMPLE_APP}/bin/${DOTNET_CONFIG}/${DOTNET_VERSION}/${SAMPLE_APP}.dll"] +dependencies = ["set-profiler-env"] + +[tasks.test-example] +clear = true +dependencies = ["build-release", "generate-integrations", "build-example", "run-dotnet"] + +[tasks.test-local] +private = true +condition = { env_false = [ "CARGO_MAKE_CI" ] } +command = "cargo" +args = ["test"] + +[tasks.test-ci] +private = true +condition = { env_true = [ "CARGO_MAKE_CI" ] } +script = ["cargo test -- -Z unstable-options --format json | tee test_results/results.json"] +dependencies = ["create-test-results-dir"] + +[tasks.test-ci.windows] +private = true +script_runner = "powershell" +script_extension = "ps1" +script = ["cargo test -- -Z unstable-options --format json | Tee-Object -FilePath 'test_results/results.json'"] + +[tasks.create-test-results-dir] +private = true +condition = { env_true = [ "CARGO_MAKE_CI" ] } +script = ["[ -d test_results ] || mkdir -p test_results"] + +[tasks.create-test-results-dir.windows] +script_runner = "powershell" +script_extension = "ps1" +script = ["if (!(Test-Path test_results)) { New-Item test_results -Type Directory }"] + +[tasks.install-cargo2junit] +private = true +condition = { env_true = [ "CARGO_MAKE_CI" ] } +script = ["cargo install cargo2junit"] + +[tasks.convert-test-results-junit] +private = true +condition = { env_true = [ "CARGO_MAKE_CI" ] } +script = ["cat test_results/results.json | cargo2junit > test_results/junit-cargo.xml"] +dependencies = ["install-cargo2junit", "create-test-results-dir"] + +[tasks.convert-test-results-junit.windows] +script_runner = "powershell" +script_extension = "ps1" +script = ["Get-Content test_results/results.json | cargo2junit | Out-File -encoding utf8 -filepath test_results/junit-cargo.xml"] + +[tasks.test] +clear = true +dependencies = ["generate-integrations", "test-local", "test-ci", "convert-test-results-junit"] diff --git a/build/scripts/Build.fs b/build/scripts/Build.fs index 9c8789b9c..2a77618bb 100644 --- a/build/scripts/Build.fs +++ b/build/scripts/Build.fs @@ -16,21 +16,19 @@ open Fake.Core open Fake.DotNet open Fake.IO open Fake.IO +open Fake.IO open Fake.IO.Globbing.Operators open Fake.SystemHelper open Fake.SystemHelper +open TestEnvironment open Tooling module Build = - let private isCI = Environment.hasEnvironVar "BUILD_ID" - let private oldDiagnosticSourceVersion = SemVer.parse "4.6.0" let mutable private currentDiagnosticSourceVersion = None - - let private isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - + let private aspNetFullFramework = Paths.SrcProjFile "Elastic.Apm.AspNetFullFramework" let private allSrcProjects = !! "src/**/*.csproj" @@ -144,10 +142,14 @@ module Build = dotnet "build" Paths.SolutionNetCore if isWindows && not isCI then msBuild "Build" aspNetFullFramework copyBinRelease() + + /// Builds the CLR profiler and supporting .NET managed assemblies + let BuildProfiler () = + dotnet "build" (Paths.SrcProjFile "Elastic.Apm.Profiler.Managed") + Cargo.Exec [ "make"; "build-release"; ] /// Publishes all projects with framework versions - let Publish targets = - + let Publish targets = let projs = match targets with | Some t -> t @@ -186,12 +188,20 @@ module Build = Shell.cleanDir Paths.BuildOutputFolder dotnet "clean" Paths.SolutionNetCore if isWindows && not isCI then msBuild "Clean" aspNetFullFramework + + let CleanProfiler () = + Cargo.Exec ["make"; "clean"] /// Restores all packages for the solution let Restore () = DotNet.Exec ["restore" ; Paths.SolutionNetCore; "-v"; "q"] if isWindows then DotNet.Exec ["restore" ; aspNetFullFramework; "-v"; "q"] + let private copyDllsAndPdbs (destination: DirectoryInfo) (source: DirectoryInfo) = + source.GetFiles() + |> Seq.filter (fun file -> file.Extension = ".dll" || file.Extension = ".pdb") + |> Seq.iter (fun file -> file.CopyTo(Path.combine destination.FullName file.Name, true) |> ignore) + /// Creates versioned ElasticApmAgent.zip file let AgentZip (canary:bool) = let name = "ElasticApmAgent" @@ -204,31 +214,25 @@ module Build = let agentDir = Paths.BuildOutput name |> DirectoryInfo agentDir.Create() - // all files of interest are top level files in the source directory - let copy (destination: DirectoryInfo) (source: DirectoryInfo) = - source.GetFiles() - |> Seq.filter (fun file -> file.Extension = ".dll" || file.Extension = ".pdb") - |> Seq.iter (fun file -> file.CopyTo(Path.combine destination.FullName file.Name, true) |> ignore) - // copy startup hook to root of agent directory !! (Paths.BuildOutput "ElasticApmAgentStartupHook/netcoreapp2.2") |> Seq.filter Path.isDirectory |> Seq.map DirectoryInfo - |> Seq.iter (copy agentDir) + |> Seq.iter (copyDllsAndPdbs agentDir) // assemblies compiled against "current" version of System.Diagnostics.DiagnosticSource !! (Paths.BuildOutput "Elastic.Apm.StartupHook.Loader/netcoreapp2.2") ++ (Paths.BuildOutput "Elastic.Apm/netstandard2.0") |> Seq.filter Path.isDirectory |> Seq.map DirectoryInfo - |> Seq.iter (copy (agentDir.CreateSubdirectory(sprintf "%i.0.0" getCurrentApmDiagnosticSourceVersion.Major))) + |> Seq.iter (copyDllsAndPdbs (agentDir.CreateSubdirectory(sprintf "%i.0.0" getCurrentApmDiagnosticSourceVersion.Major))) // assemblies compiled against older version of System.Diagnostics.DiagnosticSource !! (Paths.BuildOutput (sprintf "Elastic.Apm.StartupHook.Loader_%i.0.0/netcoreapp2.2" oldDiagnosticSourceVersion.Major)) ++ (Paths.BuildOutput (sprintf "Elastic.Apm_%i.0.0/netstandard2.0" oldDiagnosticSourceVersion.Major)) |> Seq.filter Path.isDirectory |> Seq.map DirectoryInfo - |> Seq.iter (copy (agentDir.CreateSubdirectory(sprintf "%i.0.0" oldDiagnosticSourceVersion.Major))) + |> Seq.iter (copyDllsAndPdbs (agentDir.CreateSubdirectory(sprintf "%i.0.0" oldDiagnosticSourceVersion.Major))) // include version in the zip file name ZipFile.CreateFromDirectory(agentDir.FullName, Paths.BuildOutput versionedName + ".zip") @@ -243,4 +247,46 @@ module Build = Versioning.CurrentVersion.AssemblyVersion.ToString() Docker.Exec [ "build"; "--file"; "./build/docker/Dockerfile"; - "--tag"; sprintf "observability/apm-agent-dotnet:%s" agentVersion; "./build/output/ElasticApmAgent" ] \ No newline at end of file + "--tag"; sprintf "observability/apm-agent-dotnet:%s" agentVersion; "./build/output/ElasticApmAgent" ] + + let ProfilerIntegrations () = + DotNet.Exec ["run"; "--project"; Paths.SrcProjFile "Elastic.Apm.Profiler.IntegrationsGenerator"; "--" + "-i"; Paths.Src "Elastic.Apm.Profiler.Managed/bin/Release/netstandard2.0/Elastic.Apm.Profiler.Managed.dll" + "-o"; Paths.Src "Elastic.Apm.Profiler.Managed"; "-f"; "yml"] + + /// Creates versioned elastic_apm_profiler.zip file containing all components needed for profiler auto-instrumentation + let ProfilerZip (canary:bool) = + let name = "elastic_apm_profiler" + let versionedName = + let os = + if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then "win-x64" + else "linux-x64" + if canary then + sprintf "%s_%s-%s-%s" name (Versioning.CurrentVersion.AssemblyVersion.ToString()) os versionSuffix + else + sprintf "%s_%s-%s" name (Versioning.CurrentVersion.AssemblyVersion.ToString()) os + + let profilerDir = Paths.BuildOutput name |> DirectoryInfo + profilerDir.Create() + + seq { + Paths.Src "Elastic.Apm.Profiler.Managed/integrations.yml" + "target/release/elastic_apm_profiler.dll" + "target/release/libelastic_apm_profiler.so" + Paths.Src "elastic_apm_profiler/NOTICE" + "LICENSE" + } + |> Seq.map FileInfo + |> Seq.filter (fun file -> file.Exists) + |> Seq.iter (fun file -> file.CopyTo(Path.combine profilerDir.FullName file.Name, true) |> ignore) + + Directory.GetDirectories((Paths.BuildOutput "Elastic.Apm.Profiler.Managed"), "*", SearchOption.TopDirectoryOnly) + |> Seq.map DirectoryInfo + |> Seq.iter (fun sourceDir -> copyDllsAndPdbs (profilerDir.CreateSubdirectory(sourceDir.Name)) sourceDir) + + // include version in the zip file name + ZipFile.CreateFromDirectory(profilerDir.FullName, Paths.BuildOutput versionedName + ".zip") + + + + \ No newline at end of file diff --git a/build/scripts/Targets.fs b/build/scripts/Targets.fs index d84bb8630..304abf9b7 100644 --- a/build/scripts/Targets.fs +++ b/build/scripts/Targets.fs @@ -13,6 +13,7 @@ open Fake.IO open ProcNet open Fake.Core open Fake.IO.Globbing.Operators +open TestEnvironment module Main = let excludeBullsEyeOptions = Set.ofList [ @@ -77,7 +78,19 @@ module Main = Targets.Target("version", fun _ -> Versioning.CurrentVersion |> ignore) - Targets.Target("clean", Build.Clean) + Targets.Target("clean", fun _ -> + if isCI then + printfn "skipping clean as running on CI" + else + Build.Clean() + ) + + Targets.Target("clean-profiler", fun _ -> + if isCI then + printfn "skipping clean-profiler as running on CI" + else + Build.CleanProfiler() + ) Targets.Target("netcore-sln", Build.GenerateNetCoreSln) @@ -85,9 +98,19 @@ module Main = Targets.Target("build", ["restore"; "clean"; "version"], Build.Build) - Targets.Target("publish", ["restore"; "clean"; "version"], fun _ -> Build.Publish None) + Targets.Target("build-profiler", ["restore"; "clean"; "version"; "clean-profiler"], Build.BuildProfiler) + + Targets.Target("profiler-integrations", ["build-profiler"], Build.ProfilerIntegrations) - Targets.Target("pack", ["agent-zip"], fun _ -> Build.Pack (cmdLine.ValueForOption("canary"))) + Targets.Target("profiler-zip", ["profiler-integrations"], fun _ -> + let projs = !! (Paths.SrcProjFile "Elastic.Apm.Profiler.Managed") + Build.Publish (Some projs) + Build.ProfilerZip (cmdLine.ValueForOption("canary")) + ) + + Targets.Target("publish", ["restore"; "clean"; "version"], fun _ -> Build.Publish None) + + Targets.Target("pack", ["agent-zip", "profiler-zip"], fun _ -> Build.Pack (cmdLine.ValueForOption("canary"))) Targets.Target("agent-zip", ["build"], fun _ -> let projs = !! (Paths.SrcProjFile "Elastic.Apm") diff --git a/build/scripts/TestEnvironment.fs b/build/scripts/TestEnvironment.fs new file mode 100644 index 000000000..6103216fc --- /dev/null +++ b/build/scripts/TestEnvironment.fs @@ -0,0 +1,9 @@ +namespace Scripts + +open System.Runtime.InteropServices +open Fake.Core + +module TestEnvironment = + let isCI = Environment.hasEnvironVar "BUILD_ID" + let isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + let isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) \ No newline at end of file diff --git a/build/scripts/Tooling.fs b/build/scripts/Tooling.fs index dfa5f3c96..e1eebe150 100644 --- a/build/scripts/Tooling.fs +++ b/build/scripts/Tooling.fs @@ -63,6 +63,8 @@ module Tooling = let Docker = BuildTooling(None, "docker") + let Cargo = BuildTooling(None, "cargo") + let private restoreDotnetTools = lazy(DotNet.Exec ["tool"; "restore"]) let Diff args = diff --git a/build/scripts/scripts.fsproj b/build/scripts/scripts.fsproj index da57fd19e..cf6c239ff 100644 --- a/build/scripts/scripts.fsproj +++ b/build/scripts/scripts.fsproj @@ -6,6 +6,7 @@ false + @@ -19,12 +20,12 @@ - - - - - - + + + + + + diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 3fa44cabd..d14563449 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -12,6 +12,8 @@ include::./intro.asciidoc[] include::./setup.asciidoc[] +include::./packages.asciidoc[] + include::./supported-technologies.asciidoc[] include::./configuration.asciidoc[] @@ -20,8 +22,6 @@ include::./public-api.asciidoc[] include::./metrics.asciidoc[] -// OpenTracing API - include::./log-correlation.asciidoc[] include::./performance-tuning.asciidoc[] diff --git a/docs/integrations.asciidoc b/docs/integrations.asciidoc new file mode 100644 index 000000000..75978550d --- /dev/null +++ b/docs/integrations.asciidoc @@ -0,0 +1,57 @@ +:star: * + +|=== +|Integration |Assembly |Supported assembly version range +| AdoNet +| System.Data +| 4.0.0 - 4.{star}.{star} + +| AdoNet +| System.Data.Common +| 4.0.0 - 5.{star}.{star} + +| AspNet +| System.Web +| 4.0.0 - 4.{star}.{star} + +| Kafka +| Confluent.Kafka +| 1.4.0 - 1.{star}.{star} + +| MySqlCommand +| MySql.Data +| 6.7.0 - 8.{star}.{star} + +| NpgsqlCommand +| Npgsql +| 4.0.0 - 5.{star}.{star} + +| OracleCommand +| Oracle.ManagedDataAccess +| 4.122.0 - 4.122.{star} + +| OracleCommand +| Oracle.ManagedDataAccess +| 2.0.0 - 2.{star}.{star} + +| SqlCommand +| System.Data +| 4.0.0 - 4.{star}.{star} + +| SqlCommand +| System.Data.SqlClient +| 4.0.0 - 4.{star}.{star} + +| SqlCommand +| Microsoft.Data.SqlClient +| 1.0.0 - 2.{star}.{star} + +| SqliteCommand +| Microsoft.Data.Sqlite +| 2.0.0 - 5.{star}.{star} + +| SqliteCommand +| System.Data.SQLite +| 1.0.0 - 2.{star}.{star} + +|=== diff --git a/docs/packages.asciidoc b/docs/packages.asciidoc new file mode 100644 index 000000000..75f8675d0 --- /dev/null +++ b/docs/packages.asciidoc @@ -0,0 +1,297 @@ +:nuget: https://www.nuget.org/packages +:dot: . + +[[packages]] +== NuGet packages + +Agent instrumentations are released as a set of NuGet packages available on https://nuget.org[nuget.org]. +You can add the Agent and specific instrumentations to your .NET application +by referencing one or more of these packages. + +[float] +== Get started + +* <> +* <> +* <> +* <> +* <> +* <> +* <> + +[float] +== Packages + +The following NuGet packages are available: + +{nuget}/Elastic.Apm[**Elastic.Apm**]:: + +The core agent package, containing the <> of the agent. It also contains every tracing component to trace classes that are part of .NET Standard 2.0, which includes the monitoring part for `HttpClient`. Every other Elastic APM package references this package. + +{nuget}/Elastic.Apm.NetCoreAll[**Elastic.Apm.NetCoreAll**]:: + +A meta package that references all other Elastic APM .NET agent package that can automatically +configure instrumentation. ++ +If you plan to monitor a typical ASP.NET Core application that depends on the {nuget}/Microsoft.AspNetCore.All[Microsoft.AspNetCore.All] package, you should reference this package. ++ +In order to avoid adding unnecessary dependencies in applications that aren’t dependent on the {nuget}/Microsoft.AspNetCore.All[Microsoft.AspNetCore.All] package, we also offer some other packages - those are all referenced by the `Elastic.Apm.NetCoreAll` package. + +{nuget}/Elastic.Apm.Extensions.Hosting[**Elastic.Apm.Extensions.Hosting**] (added[1.6.0-beta]):: + +A package for agent registration integration with `Microsoft.Extensions.Hosting.IHostBuilder` registration. + +[[setup-asp-net]] +{nuget}/Elastic.Apm.AspNetCore[**Elastic.Apm.AspNetCore**]:: + +A package for instrumenting ASP.NET Core applications. The main difference between this package and the `Elastic.Apm.NetCoreAll` package is that this package only instruments ASP.NET Core by default, whereas +`Elastic.Apm.NetCoreAll` instruments all components that can be automatically configured, such as +Entity Framework Core, HTTP calls with `HttpClient`, database calls to SQL Server with `SqlClient`, etc. +Additional instrumentations can be added when using `Elastic.Apm.AspNetCore` by referencing the +respective NuGet packages and including their configuration code in agent setup. + +{nuget}/Elastic.Apm.AspNetFullFramework[**Elastic.Apm.AspNetFullFramework**]:: + +A package containing ASP.NET .NET Framework instrumentation. + +{nuget}/Elastic.Apm.EntityFrameworkCore[**Elastic.Apm.EntityFrameworkCore**]:: + +A package containing Entity Framework Core instrumentation. + +{nuget}/Elastic.Apm.EntityFramework6[**Elastic.Apm.EntityFramework6**]:: + +A package containing an interceptor to automatically create spans for database operations +executed by Entity Framework 6 on behalf of the application. + +{nuget}/Elastic.Apm.SqlClient[**Elastic.Apm.SqlClient**]:: + +A package containing {nuget}/System.Data.SqlClient[System.Data.SqlClient] and {nuget}/Microsoft.Data.SqlClient[Microsoft.Data.SqlClient] instrumentation. + +{nuget}/Elastic.Apm.StackExchange.Redis[**Elastic.Apm.StackExchange.Redis**]:: + +A package containing instrumentation to capture spans for commands sent to redis with {nuget}/StackExchange.Redis/[StackExchange.Redis] package. + +{nuget}/Elastic.Apm.Azure.CosmosDb[**Elastic.Apm.Azure.CosmosDb**]:: + +A package containing instrumentation to capture spans for Azure Cosmos DB with +{nuget}/Microsoft.Azure.Cosmos[Microsoft.Azure.Cosmos], {nuget}/Microsoft.Azure.DocumentDb[Microsoft.Azure.DocumentDb], and {nuget}/Microsoft.Azure.DocumentDb.Core[Microsoft.Azure.DocumentDb.Core] packages. + +{nuget}/Elastic.Apm.Azure.ServiceBus[**Elastic.Apm.Azure.ServiceBus**]:: + +A package containing instrumentation to capture transactions and spans for messages sent and received from Azure Service Bus with {nuget}/Microsoft.Azure.ServiceBus/[Microsoft.Azure.ServiceBus] and {nuget}/Azure.Messaging.ServiceBus/[Azure.Messaging.ServiceBus] packages. + + +{nuget}/Elastic.Apm.Azure.Storage[**Elastic.Apm.Azure.Storage**]:: + +A package containing instrumentation to capture spans for interaction with Azure Storage with {nuget}/azure.storage.queues/[Azure.Storage.Queues], {nuget}/azure.storage.blobs/[Azure.Storage.Blobs] and {nuget}/azure.storage.files.shares/[Azure.Storage.Files.Shares] packages. + + +{nuget}/Elastic.Apm.MongoDb[**Elastic.Apm.MongoDb**]:: + +A package containing support for {nuget}/MongoDB.Driver/[MongoDB.Driver]. + + +[[setup-ef6]] +=== Entity Framework 6 + +[float] +==== Quick start + +You can enable auto instrumentation for Entity Framework 6 by referencing the {nuget}/Elastic.Apm.EntityFramework6[`Elastic.Apm.EntityFramework6`] package +and including the `Ef6Interceptor` interceptor in your application's `web.config`: + +[source,xml] +---- + + + + + + + + +---- + +As an alternative to registering the interceptor via the configuration, you can register it in the application code: + +[source,csharp] +---- +DbInterception.Add(new Elastic.Apm.EntityFramework6.Ef6Interceptor()); +---- + +For example, in an ASP.NET application, you can place the above call in the `Application_Start` method. + +NOTE: Be careful not to execute `DbInterception.Add` for the same interceptor more than once, +or you'll get additional interceptor instances. +For example, if you add `Ef6Interceptor` interceptor twice, you'll see two spans for every SQL query. + +[[setup-sqlclient]] +=== SqlClient + +[float] +==== Quick start + +You can enable auto instrumentation for `System.Data.SqlClient` or `Microsoft.Data.SqlClient` by referencing {nuget}/Elastic.Apm.SqlClient[`Elastic.Apm.SqlClient`] package +and passing `SqlClientDiagnosticSubscriber` to the `UseElasticApm` method in case of ASP.NET Core as it shown in example: + +[source,csharp] +---- +app.UseElasticApm(Configuration, + new SqlClientDiagnosticSubscriber()); /* Enable tracing of outgoing db requests */ +---- + +or passing `SqlClientDiagnosticSubscriber` to the `Subscribe` method and make sure that the code is called only once, otherwise the same database call could be captured multiple times: + +[source,csharp] +---- +// you need add custom code to be sure that Subscribe called only once and in a thread-safe manner +if (Agent.IsConfigured) Agent.Subscribe(new SqlClientDiagnosticSubscriber()); /* Enable tracing of outgoing db requests */ +---- + +[NOTE] +-- +Auto instrumentation for `System.Data.SqlClient` is available for both .NET Core and .NET Framework applications, however, support of .NET Framework has one limitation: +command text cannot be captured. + +Auto instrumentation for `Microsoft.Data.SqlClient` is available only for .NET Core at the moment. + +As an alternative to using the `Elastic.Apm.SqlClient` package to instrument database calls, see <>. +-- + +[[setup-stackexchange-redis]] +=== StackExchange.Redis + +[float] +==== Quick start + +Instrumentation can be enabled for `StackExchange.Redis` by referencing {nuget}/Elastic.Apm.StackExchange.Redis[`Elastic.Apm.StackExchange.Redis`] package +and calling the `UseElasticApm()` extension method defined in `Elastic.Apm.StackExchange.Redis`, on `IConnectionMultiplexer` + +[source,csharp] +---- +// using Elastic.Apm.StackExchange.Redis; + +var connection = await ConnectionMultiplexer.ConnectAsync(""); +connection.UseElasticApm(); +---- + +A callback is registered with the `IConnectionMultiplexer` to provide a profiling session for each transaction and span that captures redis commands +sent with `IConnectionMultiplexer`. + +[[setup-azure-cosmosdb]] +=== Azure Cosmos DB + +[float] +==== Quick start + +Instrumentation can be enabled for Azure Cosmos DB by referencing https://www.nuget.org/packages/Elastic.Apm.Azure.CosmosDb[`Elastic.Apm.Azure.CosmosDb`] +package and subscribing to diagnostic events. + +[source, csharp] +---- +Agent.Subscribe(new AzureCosmosDbDiagnosticsSubscriber()); +---- + +Diagnostic events from `Microsoft.Azure.Cosmos`, `Microsoft.Azure.DocumentDb`, and `Microsoft.Azure.DocumentDb.Core` are captured as DB spans. + +[[setup-azure-servicebus]] +=== Azure Service Bus + +[float] +==== Quick start + +Instrumentation can be enabled for Azure Service Bus by referencing {nuget}/Elastic.Apm.Azure.ServiceBus[`Elastic.Apm.Azure.ServiceBus`] package and subscribing to diagnostic events +using one of the subscribers: + +. If the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, the subscribers will be automatically subscribed with the agent, and no further action is required. +. If you're using `Microsoft.Azure.ServiceBus`, subscribe `MicrosoftAzureServiceBusDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new MicrosoftAzureServiceBusDiagnosticsSubscriber()); +---- +. If you're using `Azure.Messaging.ServiceBus`, subscribe `AzureMessagingServiceBusDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureMessagingServiceBusDiagnosticsSubscriber()); +---- + +A new transaction is created when + +* one or more messages are received from a queue or topic subscription. +* a message is receive deferred from a queue or topic subscription. + +A new span is created when there is a current transaction, and when + +* one or more messages are sent to a queue or topic. +* one or more messages are scheduled to a queue or a topic. + +[[setup-azure-storage]] +=== Azure Storage + +[float] +==== Quick start + +Instrumentation can be enabled for Azure Storage by referencing {nuget}/Elastic.Apm.Azure.Storage[`Elastic.Apm.Azure.Storage`] package and subscribing to diagnostic events using one of the subscribers: + +. If the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, the subscribers will be automatically subscribed with the agent, and no further action is required. +. If you're using `Azure.Storage.Blobs`, subscribe `AzureBlobStorageDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureBlobStorageDiagnosticsSubscriber()); +---- +. If you're using `Azure.Storage.Queues`, subscribe `AzureQueueStorageDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureQueueStorageDiagnosticsSubscriber()); +---- +. If you're using `Azure.Storage.Files.Shares`, subscribe `AzureFileShareStorageDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureFileShareStorageDiagnosticsSubscriber()); +---- + +For Azure Queue storage, + +* A new transaction is created when one or more messages are received from a queue +* A new span is created when there is a current transaction, and when a message is sent to a queue + +For Azure Blob storage, a new span is created when there is a current transaction and when + +* A container is created, enumerated, or deleted +* A page blob is created, uploaded, downloaded, or deleted +* A block blob is created, copied, uploaded, downloaded or deleted + +For Azure File Share storage, a new span is crated when there is a current transaction and when + +* A share is created or deleted +* A directory is created or deleted +* A file is created, uploaded, or deleted. + +[[setup-mongo-db]] +=== MongoDB.Driver + +[float] +==== Quick start + +A prerequisite for auto instrumentation with [`MongoDb.Driver`] is to configure the `MongoClient` with `MongoDbEventSubscriber`: + +[source,csharp] +---- +var settings = MongoClientSettings.FromConnectionString(mongoConnectionString); + +settings.ClusterConfigurator = builder => builder.Subscribe(new MongoDbEventSubscriber()); +var mongoClient = new MongoClient(settings); +---- + +Once the above configuration is in place, and if the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, it will automatically capture calls to MongoDB on every active transaction. +Otherwise, you can manually activate auto instrumentation from the `Elastic.Apm.MongoDb` package by calling + +[source,csharp] +---- +Agent.Subscribe(new MongoDbDiagnosticsSubscriber()); +---- diff --git a/docs/setup-asp-dot-net.asciidoc b/docs/setup-asp-dot-net.asciidoc new file mode 100644 index 000000000..841950bdd --- /dev/null +++ b/docs/setup-asp-dot-net.asciidoc @@ -0,0 +1,84 @@ +:nuget: https://www.nuget.org/packages +:dot: . + +[[setup-asp-dot-net]] +=== ASP.NET + +[float] +==== Quick start + +To enable auto instrumentation for ASP.NET (Full .NET Framework), you need to install the `Elastic.Apm.AspNetFullFramework` package, add a reference +to the package in your `web.config` file, and then compile and deploy your application. + +. Ensure you have access to the application source code and install the {nuget}/Elastic.Apm.AspNetFullFramework[`Elastic.Apm.AspNetFullFramework`] +package. + +. Reference the `Elastic.Apm.AspNetFullFramework` in your application's `web.config` file by adding the `ElasticApmModule` IIS module: ++ +[source,xml] +---- + + + + + + + + +---- ++ +NOTE: There are two available configuration sources. To learn more, see <>. ++ +By default, the agent creates transactions for all HTTP requests, including static content: +.html pages, images, etc. ++ +To create transactions only for HTTP requests with dynamic content, +such as `.aspx` pages, add the `managedHandler` preCondition to your `web.config` file: ++ +[source,xml] +---- + + + + + + + + +---- ++ +NOTE: To learn more about adding modules, see the https://docs.microsoft.com/en-us/iis/configuration/system.webserver/modules/add[Microsoft docs]. + +. Recompile your application and deploy it. ++ +The `ElasticApmModule` instantiates the APM agent on the first initialization. However, there may be some scenarios where +you want to control the agent instantiation, such as configuring filters in the application start. ++ +To do so, the `ElasticApmModule` exposes a `CreateAgentComponents()` method that returns agent components configured to work with +ASP.NET Full Framework, which can then instantiate the agent. ++ +For example, you can add transaction filters to the agent in the application start: ++ +[source, c#] +---- +public class MvcApplication : HttpApplication +{ + protected void Application_Start() + { + // other application startup e.g. RouteConfig, etc. + + // set up agent with components + var agentComponents = ElasticApmModule.CreateAgentComponents(); + Agent.Setup(agentComponents); + + // add transaction filter + Agent.AddFilter((ITransaction t) => + { + t.SetLabel("foo", "bar"); + return t; + }); + } +} +---- ++ +Now, the `ElasticApmModule` will use the instantiated instance of the APM agent upon initialization. \ No newline at end of file diff --git a/docs/setup-asp-net-core.asciidoc b/docs/setup-asp-net-core.asciidoc new file mode 100644 index 000000000..fb1136558 --- /dev/null +++ b/docs/setup-asp-net-core.asciidoc @@ -0,0 +1,52 @@ +:nuget: https://www.nuget.org/packages +:dot: . + +[[setup-asp-net-core]] +=== ASP.NET Core + +[float] +==== Quick start + +[NOTE] +-- +We suggest using the approach described in the <>, +to register the agent on `IHostBuilder`, as opposed to using `IApplicationBuilder` as described below. + +We keep the `IApplicationBuilder` introduced here only for backwards compatibility. +-- + +For ASP.NET Core, once you reference the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package, you can enable auto instrumentation by calling the `UseAllElasticApm()` extension method: + +[source,csharp] +---- +using Elastic.Apm.NetCoreAll; + +public class Startup +{ + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + app.UseAllElasticApm(Configuration); + //…rest of the method + } + //…rest of the class +} +---- + +The `app.UseAllElasticApm(...)` line **must** be the first line in the `Configure` method, otherwise the agent won't be able to properly measure the timing of your requests, and complete requests may potentially be missed by the agent. + +With this you enable every agent component including ASP.NET Core tracing, monitoring of outgoing HTTP request, Entity Framework Core database tracing, etc. + +In case you only reference the {nuget}/Elastic.Apm.AspNetCore[`Elastic.Apm.AspNetCore`] package, you won't find the `UseAllElasticApm`. Instead you need to use the `UseElasticApm()` method from the `Elastic.Apm.AspNetCore` namespace. This method turns on ASP.NET Core tracing, and gives you the opportunity to manually turn on other components. By default it will only trace ASP.NET Core requests - No HTTP request tracing, database call tracing or any other tracing component will be turned on. + +In case you would like to turn on specific tracing components you can pass those to the `UseElasticApm` method. + +For example: + +[source,csharp] +---- +app.UseElasticApm(Configuration, + new HttpDiagnosticsSubscriber(), /* Enable tracing of outgoing HTTP requests */ + new EfCoreDiagnosticsSubscriber()); /* Enable tracing of database calls through EF Core*/ +---- + +In case you only want to use the <>, you don't need to do any initialization, you can simply start using the API and the agent will send the data to the APM Server. \ No newline at end of file diff --git a/docs/setup-auto-instrumentation.asciidoc b/docs/setup-auto-instrumentation.asciidoc new file mode 100644 index 000000000..84cb0eac0 --- /dev/null +++ b/docs/setup-auto-instrumentation.asciidoc @@ -0,0 +1,352 @@ +:nuget: https://www.nuget.org/packages +:dot: . + +[[setup-auto-instrumentation]] +=== Profiler Auto instrumentation + +[float] +==== Quick start + +The agent can automatically instrument .NET Framework, .NET Core, and .NET applications using the .NET CLR Profiling API. +The Profiling APIs provide a way to instrument an application or dependency code without code changes. + +This approach works with the following + +|=== +| 2.+^|**Operating system** +|Architecture |Windows |Linux +|x64 +|.NET Framework 4.6.1+ + +.NET Core 2.1+ + +.NET 5 + +|.NET Core 2.1+ + +.NET 5 +|=== + +and instruments the following assemblies + +include::integrations.asciidoc[] + +Note that NuGet package versions do not necessarily align with the version of assemblies contained therein. +https://nuget.info/packages[NuGet package explorer] can be used to inspect the assembly version of assemblies within a NuGet package. + +[IMPORTANT] +-- +The .NET CLR Profiling API allows only one profiler to be attached to a .NET process. In light of this limitation, only one +solution that uses the .NET CLR profiling API should be used by an application. + +Auto instrumentation using the .NET CLR Profiling API can be used in conjunction with + +* the <> to perform manual instrumentation. +* NuGet packages that perform instrumentation using a `IDiagnosticsSubscriber` to subscribe to diagnostic events. + +The version number of NuGet packages referenced by a project instrumented with a profiler +must be the same as the version number of profiler zip file used. +-- + +Steps: + +. Download the `elastic-apm-dotnet-auto-instrumentation-.zip` file from the https://github.com/elastic/apm-agent-dotnet/releases[Releases] page of the .NET APM Agent GitHub repository, where `` is the version number to download. You can find the file under Assets. +. Unzip the zip file into a folder on the host that is hosting the application to instrument. +. Configure the following environment variables ++ +.{dot}NET Framework +[source,sh] +---- +set COR_ENABLE_PROFILING = "1" +set COR_PROFILER = "{FA65FE15-F085-4681-9B20-95E04F6C03CC}" +set COR_PROFILER_PATH = "\elastic_apm_profiler.dll" <1> +set ELASTIC_APM_PROFILER_HOME = "" +set ELASTIC_APM_PROFILER_INTEGRATIONS = "\integrations.yml" +---- +<1> `` is the directory to which the zip file +was unzipped in step 2. ++ +.{dot}NET Core / .NET 5 on Windows +[source,sh] +---- +set CORECLR_ENABLE_PROFILING = "1" +set CORECLR_PROFILER = "{FA65FE15-F085-4681-9B20-95E04F6C03CC}" +set CORECLR_PROFILER_PATH = "\elastic_apm_profiler.dll" <1> +set ELASTIC_APM_PROFILER_HOME = "" +set ELASTIC_APM_PROFILER_INTEGRATIONS = "\integrations.yml" +---- +<1> `` is the directory to which the zip file +was unzipped in step 2. ++ +.{dot}NET Core / .NET 5 on Linux +[source,sh] +---- +export CORECLR_ENABLE_PROFILING=1 +export CORECLR_PROFILER={FA65FE15-F085-4681-9B20-95E04F6C03CC} +export CORECLR_PROFILER_PATH="/libelastic_apm_profiler.so" <1> +export ELASTIC_APM_PROFILER_HOME="" +export ELASTIC_APM_PROFILER_INTEGRATIONS="/integrations.yml" +---- +<1> `` is the directory to which the zip file +was unzipped in step 2. +. Start your application in a context where the set environment variables are visible. + +With this setup, the .NET runtime loads Elastic's CLR profiler into the .NET process, which loads and instantiates the APM agent early +in application startup. The profiler monitors methods of interest and injects code to instrument their execution. + +[float] +=== Instrumenting containers and services + +Using global environment variables causes the Elastic profiler auto instrumentation to be loaded for **any** .NET process started on the +host. Often, the environment variables should be set only for specific services or containers. The following sections demonstrate how to configure common containers and services. + +[float] +==== Docker containers + +A build image containing the files for profiler auto instrumentation +can be used as part of a https://docs.docker.com/develop/develop-images/multistage-build/[multi-stage build] + +[source,sh] +---- +ARG AGENT_VERSION=1.11.1 + +FROM alpine:latest AS build +ARG AGENT_VERSION +WORKDIR /source + +# install unzip +RUN apt-get -y update && apt-get -yq install unzip + +# pull down the zip file based on ${AGENT_VERSION} ARG and unzip +RUN curl -L -o ElasticApmAgent_${AGENT_VERSION}.zip https://github.com/elastic/apm-agent-dotnet/releases/download/${AGENT_VERSION}/elastic-apm-dotnet-auto-instrumentation-${AGENT_VERSION}.zip && \ + unzip elastic-apm-dotnet-auto-instrumentation-${AGENT_VERSION}.zip -d /elastic-apm-dotnet-auto-instrumentation-${AGENT_VERSION} + +---- + +The files can then be copied into a subsequent stage + +[source,sh] +---- +COPY --from=build /elastic-apm-dotnet-auto-instrumentation-${AGENT_VERSION} /elastic-apm-dotnet-auto-instrumentation +---- + +Environment variables can be added to a Dockerfile to configure profiler auto instrumentation + +[source,sh] +---- +ENV CORECLR_ENABLE_PROFILING=1 +ENV CORECLR_PROFILER={FA65FE15-F085-4681-9B20-95E04F6C03CC} +ENV CORECLR_PROFILER_PATH=/elastic-apm-dotnet-auto-instrumentation/libelastic_apm_profiler.so +ENV ELASTIC_APM_PROFILER_HOME=/elastic-apm-dotnet-auto-instrumentation +ENV ELASTIC_APM_PROFILER_INTEGRATIONS=/elastic-apm-dotnet-auto-instrumentation/integrations.yml + +ENTRYPOINT ["dotnet", "your-application.dll"] +---- + +[float] +==== Windows services + +Environment variables can be added to specific Windows services by +adding an entry to the Windows registry. Using PowerShell + +.{dot}NET Framework service +[source,powershell] +---- +$environment = [string[]]@( + "COR_ENABLE_PROFILING=1", + "COR_PROFILER={FA65FE15-F085-4681-9B20-95E04F6C03CC}", + "COR_PROFILER_PATH=\elastic_apm_profiler.dll", + "ELASTIC_APM_PROFILER_HOME=", + "ELASTIC_APM_PROFILER_INTEGRATIONS=\integrations.yml") + +Set-ItemProperty HKLM:SYSTEM\CurrentControlSet\Services\ -Name Environment -Value $environment +---- + +.{dot}NET Core service +[source,powershell] +---- +$environment = [string[]]@( + "CORECLR_ENABLE_PROFILING=1", + "CORECLR_PROFILER={FA65FE15-F085-4681-9B20-95E04F6C03CC}", + "CORECLR_PROFILER_PATH=\elastic_apm_profiler.dll", <1> + "ELASTIC_APM_PROFILER_HOME=", + "ELASTIC_APM_PROFILER_INTEGRATIONS=\integrations.yml") + +Set-ItemProperty HKLM:SYSTEM\CurrentControlSet\Services\ -Name Environment -Value $environment <2> +---- +<1> `` is the directory to which the zip file +was unzipped +<2> `` is the name of the Windows service. + +With PowerShell + +[source,powershell] +---- +Restart-Service +---- + +[float] +==== Internet Information Services (IIS) + +For IIS versions _before_ IIS 10, it is **not** possible to set environment variables scoped to a specific application pool, so environment variables +need to set globally. + +For IIS 10 _onwards_, environment variables can be set for an application +pool using https://docs.microsoft.com/en-us/iis/get-started/getting-started-with-iis/getting-started-with-appcmdexe[AppCmd.exe]. With PowerShell + +.{dot}NET Framework +[source,powershell] +---- +$appcmd = "$($env:systemroot)\system32\inetsrv\AppCmd.exe" +$appPool = "" <1> +$environment = @{ + COR_ENABLE_PROFILING = "1" + COR_PROFILER = "{FA65FE15-F085-4681-9B20-95E04F6C03CC}" + COR_PROFILER_PATH = "\elastic_apm_profiler.dll" <2> + ELASTIC_APM_PROFILER_HOME = "" + ELASTIC_APM_PROFILER_INTEGRATIONS = "\integrations.yml" + COMPlus_LoaderOptimization = "1" <3> +} + +$environment.Keys | ForEach-Object { + & $appcmd set config -section:system.applicationHost/applicationPools /+"[name='$appPool'].environmentVariables.[name='$_',value='$($environment[$_])']" +} +---- +<1> `` is the name of the Application Pool your application uses +<2> `` is the directory to which the zip file +was unzipped +<3> Forces assemblies **not** to be loaded domain-neutral. There is currently a limitation +where Profiler auto-instrumentation cannot instrument assemblies when they are loaded +domain-neutral. This limitation is expected to be removed in future, but for now, can be worked +around by setting this environment variable. See the https://docs.microsoft.com/en-us/dotnet/framework/app-domains/application-domains#the-complus_loaderoptimization-environment-variable[Microsoft documentation for further details]. + +.{dot}NET Core +[source,powershell] +---- +$appcmd = "$($env:systemroot)\system32\inetsrv\AppCmd.exe" +$appPool = "" <1> +$environment = @{ + CORECLR_ENABLE_PROFILING = "1" + CORECLR_PROFILER = "{FA65FE15-F085-4681-9B20-95E04F6C03CC}" + CORECLR_PROFILER_PATH = "\elastic_apm_profiler.dll" <2> + ELASTIC_APM_PROFILER_HOME = "" + ELASTIC_APM_PROFILER_INTEGRATIONS = "\integrations.yml" +} + +$environment.Keys | ForEach-Object { + & $appcmd set config -section:system.applicationHost/applicationPools /+"[name='$appPool'].environmentVariables.[name='$_',value='$($environment[$_])']" +} +---- +<1> `` is the name of the Application Pool your application uses +<2> `` is the directory to which the zip file +was unzipped + +[IMPORTANT] +-- +Ensure that the location of the `` is accessible and executable to the https://docs.microsoft.com/en-us/iis/manage/configuring-security/application-pool-identities[Identity +account under which the Application Pool runs]. +-- + +Once environment variables have been set, stop and start IIS so that applications hosted in +IIS will see the new environment variables + +[source,sh] +---- +net stop /y was +net start w3svc +---- + +[float] +==== systemd / systemctl + +Environment variables can be added to specific services run with systemd +by creating an environment.env file containing the following + +[source,sh] +---- +CORECLR_ENABLE_PROFILING=1 +CORECLR_PROFILER={FA65FE15-F085-4681-9B20-95E04F6C03CC} +CORECLR_PROFILER_PATH=/elastic-apm-dotnet-auto-instrumentation/libelastic_apm_profiler.so +ELASTIC_APM_PROFILER_HOME=/elastic-apm-dotnet-auto-instrumentation +ELASTIC_APM_PROFILER_INTEGRATIONS=/elastic-apm-dotnet-auto-instrumentation/integrations.yml +---- + +Then adding an https://www.freedesktop.org/software/systemd/man/systemd.service.html#Command%20lines[`EnvironmentFile`] entry to the service's configuration file +that references the path to the environment.env file + +[source,sh] +---- +[Service] +EnvironmentFile=/path/to/environment.env +ExecStart= <1> +---- +<1> the command that starts your service + +After adding the `EnvironmentFile` entry, restart the service + +[source,sh] +---- +systemctl reload-or-restart +---- + +[float] +=== Profiler environment variables + +The profiler auto instrumentation has its own set of environment variables to manage +the instrumentation + +`ELASTIC_APM_PROFILER_HOME`:: + +The home directory of the profiler auto instrumentation. + +`ELASTIC_APM_PROFILER_INTEGRATIONS` _(optional)_:: + +The path to the integrations.yml file that determines which methods to target for +auto instrumentation. If not specified, the profiler will assume an +integrations.yml exists in the home directory specified by `ELASTIC_APM_PROFILER_HOME` +environment variable. + +`ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS` _(optional)_:: + +A semi-colon separated list of integrations to exclude from auto-instrumentation. +Valid values are those defined in the `Integration` column in the integrations +table above. + +`ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES` _(optional)_:: + +A semi-colon separated list of process names to exclude from auto-instrumentation. +For example, `dotnet.exe;powershell.exe`. Can be used in scenarios where profiler +environment variables have a global scope that would end up auto-instrumenting +applications that should not be. + +`ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES` _(optional)_:: + +A semi-colon separated list of APM service names to exclude from auto-instrumentation. +Values defined are checked against the value of `ELASTIC_APM_SERVICE_NAME` environment +variable. + +`ELASTIC_APM_PROFILER_LOG` _(optional)_:: + +The log level at which the profiler should log. Valid values are + +* trace +* debug +* info +* warn +* error +* none + +The default value is `warn`. More verbose log levels like `trace` and `debug` can +affect the runtime performance of profiler auto instrumentation, so are recommended +_only_ for diagnostics purposes. + +`ELASTIC_APM_PROFILER_LOG_DIR` _(optional)_:: + +The directory in which to write profiler log files. If unset, defaults to + +* `%PROGRAMDATA%\elastic\apm-agent-dotnet\logs` on Windows +* `/var/log/elastic/apm-agent-dotnet` on Linux + +If the default directory cannot be written to for some reason, the profiler +will try to write log files to a `logs` directory in the home directory specified +by `ELASTIC_APM_PROFILER_HOME` environment variable. diff --git a/docs/setup-dotnet-net-core.asciidoc b/docs/setup-dotnet-net-core.asciidoc new file mode 100644 index 000000000..63206688a --- /dev/null +++ b/docs/setup-dotnet-net-core.asciidoc @@ -0,0 +1,117 @@ +:nuget: https://www.nuget.org/packages +:dot: . + +[[setup-dotnet-net-core]] +=== .NET Core + +[float] +==== Quick start + +On .NET Core, the agent can be registered on the `IHostBuilder`. This applies to both ASP.NET Core and to other .NET Core applications that depend on `IHostBuilder`, like https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services[background tasks]. In this case, you need to reference the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package. + + +[source,csharp] +---- +using Elastic.Apm.NetCoreAll; + +namespace MyApplication +{ + public class Program + { + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) + .UseAllElasticApm(); + + public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); + } +} +---- + +With the `UseAllElasticApm()`, the agent with all its components is turned on. On ASP.NET Core, it'll automatically capture incoming requests, database calls through supported technologies, outgoing HTTP requests, and so on. + +[float] +==== Manual instrumentation + +The `UseAllElasticApm` will add an `ITracer` to the Dependency Injection system, which can be used in your code to manually instrument your application, using the <> + +[source,csharp] +---- +using Elastic.Apm.Api; + +namespace WebApplication.Controllers +{ + public class HomeController : Controller + { + private readonly ITracer _tracer; + + //ITracer injected through Dependency Injection + public HomeController(ITracer tracer) => _tracer = tracer; + + public IActionResult Index() + { + //use ITracer + var span = _tracer.CurrentTransaction?.StartSpan("MySampleSpan", "Sample"); + try + { + //your code here + } + catch (Exception e) + { + span?.CaptureException(e); + throw; + } + finally + { + span?.End(); + } + return View(); + } + } +} +---- + +Similarly to this ASP.NET Core controller, you can use the same approach with `IHostedService` implementations. + +[float] +==== Instrumentation modules + +The `Elastic.Apm.NetCoreAll` package references every agent component that can be automatically configured. This is usually not a problem, but if you want to keep dependencies minimal, you can instead reference the `Elastic.Apm.Extensions.Hosting` package and use the `UseElasticApm` method, instead of `UseAllElasticApm`. With this setup you can control what the agent will listen for. + +The following example only turns on outgoing HTTP monitoring (so, for instance, database or Elasticsearch calls won't be automatically captured): + +[source,csharp] +---- +public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) + .UseElasticApm(new HttpDiagnosticsSubscriber()); +---- + + +[float] +[[zero-code-change-setup]] +==== Zero code change setup on .NET Core (added[1.7]) + +If you can't or don't want to reference NuGet packages in your application, you can use the startup hook feature to inject the agent during startup, if your application runs on .NET Core. This feature is supported on .NET Core 3.0 and newer versions. + +To configure startup hooks + +. Download the `ElasticApmAgent_.zip` file from the https://github.com/elastic/apm-agent-dotnet/releases[Releases] page of the .NET APM Agent GitHub repository. You can find the file under Assets. +. Unzip the zip file into a folder. +. Set the `DOTNET_STARTUP_HOOKS` environment variable to point to the `ElasticApmAgentStartupHook.dll` file in the unzipped folder ++ +[source,sh] +---- +set DOTNET_STARTUP_HOOKS=\ElasticApmAgentStartupHook.dll <1> +---- +<1> `` is the unzipped directory from step 2. + +. Start your .NET Core application in a context where the `DOTNET_STARTUP_HOOKS` environment variable is visible. + +With this setup the agent will be injected into the application during startup and it will start every auto instrumentation feature. On ASP.NET Core (including gRPC), incoming requests will be automatically captured. + +[NOTE] +-- +Agent configuration can be controlled through environment variables with the startup hook feature. +-- \ No newline at end of file diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index 3d98df25d..595b3e37c 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -1,522 +1,43 @@ +:nuget: https://www.nuget.org/packages +:dot: . + [[setup]] == Set up the Agent -The .NET Agent ships as a set of NuGet packages via https://nuget.org[nuget.org]. -You can add the Agent to your .NET application by referencing one of these packages. - -On .NET Core the agent also supports auto instrumentation without any code change and without any recompilation of your projects. See section <> for more details. - -[float] -== Get started - -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -[float] -== Packages - -The following packages are available: - -https://www.nuget.org/packages/Elastic.Apm.NetCoreAll[**Elastic.Apm.NetCoreAll**]:: - -This is a meta package that references every other Elastic APM .NET agent package. If you plan to monitor a typical ASP.NET Core application that depends on the https://www.nuget.org/packages/Microsoft.AspNetCore.All[Microsoft.AspNetCore.All] package and uses Entity Framework Core then you should reference this package. -In order to avoid adding unnecessary dependencies in applications that aren’t depending on the https://www.nuget.org/packages/Microsoft.AspNetCore.All[Microsoft.AspNetCore.All] package we also offer some other packages - those are all referenced by the `Elastic.Apm.NetCoreAll` package. - -https://www.nuget.org/packages/Elastic.Apm[**Elastic.Apm**]:: - -This is the core of the agent, which we didn’t name “Core”, because someone already took that name :) This package also contains the <> and it is a .NET Standard 2.0 package. We also ship every tracing component that traces classes that are part of .NET Standard 2.0 in this package, which includes the monitoring part for HttpClient. Every other Elastic APM package references this package. - -https://www.nuget.org/packages/Elastic.Apm.Extensions.Hosting[**Elastic.Apm.Extensions.Hosting**](added[1.6.0-beta]):: - -This package offers integration with `Microsoft.Extensions.Hosting.IHostBuilder` for agent registration. - -[[setup-asp-net]] -https://www.nuget.org/packages/Elastic.Apm.AspNetCore[**Elastic.Apm.AspNetCore**]:: -This package contains ASP.NET Core monitoring related code. The main difference between this package and the `Elastic.Apm.NetCoreAll` package is that this package does not reference the `Elastic.Apm.EntityFrameworkCore` package, so if you have an ASP.NET Core application that does not use EF Core and you want to avoid adding additional unused references, you should use this package. -https://www.nuget.org/packages/Elastic.Apm.EntityFrameworkCore[**Elastic.Apm.EntityFrameworkCore**]:: +The .NET agent can be added to an application in three different ways -This package contains EF Core monitoring related code. -https://www.nuget.org/packages/Elastic.Apm.AspNetFullFramework[**Elastic.Apm.AspNetFullFramework**]:: +NuGet packages:: +The agent ships as a set of <> available on https://nuget.org[nuget.org]. +You can add the Agent and specific instrumentations to a .NET application by +referencing one or more of these packages and following the package documentation. -This package contains ASP.NET (Full .NET Framework) monitoring related code. +Profiler runtime instrumentation:: +The agent supports auto instrumentation without any code change and without +any recompilation of your projects. See <>. -https://www.nuget.org/packages/Elastic.Apm.EntityFramework6[**Elastic.Apm.EntityFramework6**]:: +Host startup hook:: +On **.NET Core 3.0+**, the agent supports auto instrumentation without any code change and without +any recompilation of your projects. See <> +for more details. -This package contains an interceptor that automatically creates spans for DB operations executed by Entity Framework 6 (EF6) on behalf of the application. - -https://www.nuget.org/packages/Elastic.Apm.SqlClient[**Elastic.Apm.SqlClient**]:: - -This package contains https://www.nuget.org/packages/System.Data.SqlClient[System.Data.SqlClient] and https://www.nuget.org/packages/Microsoft.Data.SqlClient[Microsoft.Data.SqlClient] monitoring related code. - - -https://www.nuget.org/packages/Elastic.Apm.StackExchange.Redis[**Elastic.Apm.StackExchange.Redis**]:: - -This packages contains instrumentation to capture spans for commands sent to redis with https://www.nuget.org/packages/StackExchange.Redis/[StackExchange.Redis] package. - -https://www.nuget.org/packages/Elastic.Apm.Azure.ServiceBus[**Elastic.Apm.Azure.ServiceBus**]:: - -This packages contains instrumentation to capture transactions and spans for messages sent and received from Azure Service Bus with https://www.nuget.org/packages/Microsoft.Azure.ServiceBus/[Microsoft.Azure.ServiceBus] and https://www.nuget.org/packages/Azure.Messaging.ServiceBus/[Azure.Messaging.ServiceBus] packages. - - -https://www.nuget.org/packages/Elastic.Apm.Azure.Storage[**Elastic.Apm.Azure.Storage**]:: - -This packages contains instrumentation to capture spans for interaction with Azure Storage with https://www.nuget.org/packages/azure.storage.queues/[Azure.Storage.Queues], https://www.nuget.org/packages/azure.storage.blobs/[Azure.Storage.Blobs] and https://www.nuget.org/packages/azure.storage.files.shares/[Azure.Storage.Files.Shares] packages. - - -https://www.nuget.org/packages/Elastic.Apm.MongoDb[**Elastic.Apm.MongoDb**] - -This package contains support for https://www.nuget.org/packages/MongoDB.Driver/[MongoDB.Driver]. - - -[[setup-dotnet-net-core]] -=== .NET Core [float] -==== Quick start - -On .NET Core, the agent can be registered on the `IHostBuilder`. This applies to both ASP.NET Core and to other .NET Core applications that depend on `IHostBuilder`, like https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services[background tasks]. In this case, you need to reference the https://www.nuget.org/packages/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package. - - -[source,csharp] ----- -using Elastic.Apm.NetCoreAll; - -namespace MyApplication -{ - public class Program - { - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) - .UseAllElasticApm(); - - public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); - } -} ----- - -With the `UseAllElasticApm()`, the agent with all its components is turned on. On ASP.NET Core, it'll automatically capture incoming requests, database calls through supported technologies, outgoing HTTP requests, and so on. - -[float] -==== Manual instrumentation - -The `UseAllElasticApm` will add an `ITracer` to the Dependency Injection system. This means you can use the <> in your code to manually instrument your application: - -[source,csharp] ----- -using Elastic.Apm.Api; - -namespace WebApplication.Controllers -{ - public class HomeController : Controller - { - private readonly ITracer _tracer; - public HomeController(ITracer tracer) //inject ITracer - => _tracer = tracer; - - public IActionResult Index() - { - //use ITracer - var span = _tracer.CurrentTransaction?.StartSpan("MySampleSpan", "Sample"); - try - { - //your code here - } - catch (Exception e) - { - span?.CaptureException(e); - throw; - } - finally { } - { - span?.End(); - } - return View(); - } - } -} ----- - -Similarly to this ASP.NET Core controller, you can use the same approach with `IHostedService` implementations. - -[float] -==== Instrumentation modules - -The `Elastic.Apm.NetCoreAll` will reference every agent component. This is usually not a problem, but if you want to keep dependencies minimal, you can also reference the `Elastic.Apm.Extensions.Hosting` and use the `UseElasticApm` method instead of `UseAllElasticApm`. With this you can control what the agent will listen for. - -The following example only turns on outgoing HTTP monitoring (so, for instance, database or Elasticsearch calls won't be automatically captured): - -[source,csharp] ----- - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) - .UseElasticApm(new HttpDiagnosticsSubscriber()); - ----- - - -[float] -[[zero-code-change-setup]] -==== Zero code change setup on .NET Core (added[1.7]) - -If you can't or don't want to reference NuGet packages in your application, you can use the startup hook feature to inject the agent during startup, if your application runs on .NET Core. This feature is supported on .NET Core 3.0 and newer versions. - -Steps: - -. Download the `ElasticApmAgent_[version].zip` file from the https://github.com/elastic/apm-agent-dotnet/releases[Releases] page of the .NET APM Agent GitHub repository. You can find the file under Assets. -. Unzip the zip file into a folder. -. Set the `DOTNET_STARTUP_HOOKS` environment variable to point to the `ElasticApmAgentStartupHook.dll` file in the unzipped folder - -[source,sh] ----- -set DOTNET_STARTUP_HOOKS=[pathToAgent]\ElasticApmAgentStartupHook.dll ----- - -. Start your .NET Core application in a context where the `DOTNET_STARTUP_HOOKS` environment variable is visible. - -With this setup the agent will be injected into the application during startup and it will start every auto instrumentation feature. On ASP.NET Core (including gRPC), incoming requests will be automatically captured. - -[NOTE] --- -Agent configuration can be controlled through environment variables with the startup hook feature. --- - -[[setup-asp-net-core]] -=== ASP.NET Core - -[float] -==== Quick start - -We suggest using the approach described in the <>. We keep the `IApplicationBuilder` introduced here only for backwards compatibility. - -For ASP.NET Core, once you reference the https://www.nuget.org/packages/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package, you can enable auto instrumentation by calling the `UseAllElasticApm()` extension method: - -[source,csharp] ----- -using Elastic.Apm.NetCoreAll; - -public class Startup -{ - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - app.UseAllElasticApm(Configuration); - //…rest of the method - } - //…rest of the class -} ----- - -The `app.UseAllElasticApm(...)` line must be the first line in the `Configure` method, otherwise the agent won't be able to properly measure the timing of your requests, and potentially complete requests may be missed by the agent. - -With this you enable every agent component including ASP.NET Core tracing, monitoring of outgoing HTTP request, Entity Framework Core database tracing, etc. - -In case you only reference the https://www.nuget.org/packages/Elastic.Apm.AspNetCore[`Elastic.Apm.AspNetCore`] package, you won't find the `UseAllElasticApm`. Instead you need to use the `UseElasticApm()` method from the `Elastic.Apm.AspNetCore` namespace. This method turns on ASP.NET Core tracing, and gives you the opportunity to manually turn on other components. By default it will only trace ASP.NET Core requests - No HTTP request tracing, database call tracing or any other tracing component will be turned on. - -In case you would like to turn on specific tracing components you can pass those to the `UseElasticApm` method. - -For example: - -[source,csharp] ----- -app.UseElasticApm(Configuration, - new HttpDiagnosticsSubscriber(), /* Enable tracing of outgoing HTTP requests */ - new EfCoreDiagnosticsSubscriber()); /* Enable tracing of database calls through EF Core*/ ----- - -In case you only want to use the <>, you don't need to do any initialization, you can simply start using the API and the agent will send the data to the APM Server. - - -[[setup-asp-dot-net]] -=== ASP.NET - -[float] -==== Quick start - -To enable auto instrumentation for ASP.NET (Full .NET Framework), you need to install the `Elastic.Apm.AspNetFullFramework` package, add a reference -to the package in your `web.config` file, and then compile and deploy your application. - -. Ensure you have access to the application source code and install the https://www.nuget.org/packages/Elastic.Apm.AspNetFullFramework[`Elastic.Apm.AspNetFullFramework`] -package. - -. Reference the `Elastic.Apm.AspNetFullFramework` in your application's `web.config` file by adding the `ElasticApmModule` IIS module: -+ -[source,xml] ----- - - - - - - - - ----- -+ -NOTE: There are two available configuration sources. To learn more, see <>. -+ -By default, the agent creates transactions for all HTTP requests, including static content: -.html pages, images, etc. -+ -To create transactions only for HTTP requests with dynamic content, -such as `.aspx` pages, add the `managedHandler` preCondition to your `web.config` file: -+ -[source,xml] ----- - - - - - - - - ----- -+ -NOTE: To learn more about adding modules, see the https://docs.microsoft.com/en-us/iis/configuration/system.webserver/modules/add[Microsoft docs]. - -. Recompile your application and deploy it. -+ -The `ElasticApmModule` instantiates the APM agent on the first initialization. However, there may be some scenarios where -you want to control the agent instantiation, such as configuring filters in the application start. -+ -To do so, the `ElasticApmModule` exposes a `CreateAgentComponents()` method that returns agent components configured to work with -ASP.NET Full Framework, which can then instantiate the agent. -+ -For example, you can add transaction filters to the agent in the application start: -+ -[source, c#] ----- -public class MvcApplication : HttpApplication -{ - protected void Application_Start() - { - // other application startup e.g. RouteConfig, etc. - - // set up agent with components - var agentComponents = ElasticApmModule.CreateAgentComponents(); - Agent.Setup(agentComponents); - - // add transaction filter - Agent.AddFilter((ITransaction t) => - { - t.SetLabel("foo", "bar"); - return t; - }); - } -} ----- -+ -Now, the `ElasticApmModule` will use the instantiated instance of the APM agent upon initialization. - -[[setup-ef6]] -=== Entity Framework 6 - -[float] -==== Quick start - -You can enable auto instrumentation for Entity Framework 6 by referencing the https://www.nuget.org/packages/Elastic.Apm.EntityFramework6[`Elastic.Apm.EntityFramework6`] package -and including the `Ef6Interceptor` interceptor in your application's `web.config`: - -[source,xml] ----- - - - - - - - - ----- - -As an alternative to registering the interceptor via the configuration, you can register it in the application code: -[source,csharp] ----- -DbInterception.Add(new Elastic.Apm.EntityFramework6.Ef6Interceptor()); ----- -For example, in an ASP.NET MVC application, you can place the above call in the `Application_Start` method. - -NOTE: Be careful not to execute `DbInterception.Add` for the same interceptor more than once, -or you'll get additional interceptor instances. -For example, if you add `Ef6Interceptor` interceptor twice, you'll see two spans for every SQL query. - -[[setup-sqlclient]] -=== SqlClient - -[float] -==== Quick start - -You can enable auto instrumentation for `System.Data.SqlClient` or `Microsoft.Data.SqlClient` by referencing https://www.nuget.org/packages/Elastic.Apm.SqlClient[`Elastic.Apm.SqlClient`] package -and passing `SqlClientDiagnosticSubscriber` to the `UseElasticApm` method in case of ASP.NET Core as it shown in example: - -[source,csharp] ----- -app.UseElasticApm(Configuration, - new SqlClientDiagnosticSubscriber()); /* Enable tracing of outgoing db requests */ ----- - -or passing `SqlClientDiagnosticSubscriber` to the `Subscribe` method and make sure that the code is called only once, otherwise the same database call could be captured multiple times: - -[source,csharp] ----- -// you need add custom code to be sure that Subscribe called only once and in a thread-safe manner -if (Agent.IsConfigured) Agent.Subscribe(new SqlClientDiagnosticSubscriber()); /* Enable tracing of outgoing db requests */ ----- - -NOTE: Auto instrumentation for `System.Data.SqlClient` is available for both, .NET Core and .NET Framework applications, however, support of .NET Framework has one limitation: -command text cannot be captured. In case of auto instrumentation for `Microsoft.Data.SqlClient`, only .NET Core is supported, at the moment. - -[[setup-stackexchange-redis]] -=== StackExchange.Redis - -[float] -==== Quick start - -Instrumentation can be enabled for `StackExchange.Redis` by referencing https://www.nuget.org/packages/Elastic.Apm.StackExchange.Redis[`Elastic.Apm.StackExchange.Redis`] package -and calling the `UseElasticApm()` extension method defined in `Elastic.Apm.StackExchange.Redis`, on `IConnectionMultiplexer` - -[source,csharp] ----- -// using Elastic.Apm.StackExchange.Redis; - -var connection = await ConnectionMultiplexer.ConnectAsync(""); -connection.UseElasticApm(); ----- - -A callback is registered with the `IConnectionMultiplexer` to provide a profiling session for each transaction and span that captures redis commands -sent with `IConnectionMultiplexer`. - -[[setup-azure-cosmosdb]] -=== Azure Cosmos DB - -[float] -==== Quick start - -Instrumentation can be enabled for Azure Cosmos DB by referencing https://www.nuget.org/packages/Elastic.Apm.Azure.CosmosDb[`Elastic.Apm.Azure.CosmosDb`] -package and subscribing to diagnostic events. - -[source, csharp] ----- -Agent.Subscribe(new AzureCosmosDbDiagnosticsSubscriber()); ----- - -Diagnostic events from `Microsoft.Azure.Cosmos`, `Microsoft.Azure.DocumentDb`, and `Microsoft.Azure.DocumentDb.Core` are captured as DB spans. - -[[setup-azure-servicebus]] -=== Azure Service Bus - -[float] -==== Quick start - -Instrumentation can be enabled for Azure Service Bus by referencing https://www.nuget.org/packages/Elastic.Apm.Azure.ServiceBus[`Elastic.Apm.Azure.ServiceBus`] package and subscribing to diagnostic events -using one of the subscribers: - -. If the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, the subscribers will be automatically subscribed with the agent, and no further action is required. -. If you're using `Microsoft.Azure.ServiceBus`, subscribe `MicrosoftAzureServiceBusDiagnosticsSubscriber` with the agent -+ -[source, csharp] ----- -Agent.Subscribe(new MicrosoftAzureServiceBusDiagnosticsSubscriber()); ----- -. If you're using `Azure.Messaging.ServiceBus`, subscribe `AzureMessagingServiceBusDiagnosticsSubscriber` with the agent -+ -[source, csharp] ----- -Agent.Subscribe(new AzureMessagingServiceBusDiagnosticsSubscriber()); ----- - -A new transaction is created when - -* one or more messages are received from a queue or topic subscription. -* a message is receive deferred from a queue or topic subscription. - -A new span is created when there is a current transaction, and when - -* one or more messages are sent to a queue or topic. -* one or more messages are scheduled to a queue or a topic. - -[[setup-azure-storage]] -=== Azure Storage - -[float] -==== Quick start - -Instrumentation can be enabled for Azure Storage by referencing https://www.nuget.org/packages/Elastic.Apm.Azure.Storage[`Elastic.Apm.Azure.Storage`] package and subscribing to diagnostic events using one of the subscribers: - -. If the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, the subscribers will be automatically subscribed with the agent, and no further action is required. -. If you're using `Azure.Storage.Blobs`, subscribe `AzureBlobStorageDiagnosticsSubscriber` with the agent -+ -[source, csharp] ----- -Agent.Subscribe(new AzureBlobStorageDiagnosticsSubscriber()); ----- -. If you're using `Azure.Storage.Queues`, subscribe `AzureQueueStorageDiagnosticsSubscriber` with the agent -+ -[source, csharp] ----- -Agent.Subscribe(new AzureQueueStorageDiagnosticsSubscriber()); ----- -. If you're using `Azure.Storage.Files.Shares`, subscribe `AzureFileShareStorageDiagnosticsSubscriber` with the agent -+ -[source, csharp] ----- -Agent.Subscribe(new AzureFileShareStorageDiagnosticsSubscriber()); ----- - -For Azure Queue storage, - -* A new transaction is created when one or more messages are received from a queue -* A new span is created when there is a current transaction, and when a message is sent to a queue - -For Azure Blob storage, a new span is created when there is a current transaction and when - -* A container is created, enumerated, or deleted -* A page blob is created, uploaded, downloaded, or deleted -* A block blob is created, copied, uploaded, downloaded or deleted - -For Azure File Share storage, a new span is created when there is a current transaction and when - -* A share is created or deleted -* A directory is created or deleted -* A file is created, uploaded, or deleted. - -[[setup-mongo-db]] -=== MongoDB.Driver - -[float] -==== Quick start - -A prerequisite for auto instrumentation with [`MongoDb.Driver`] is to configure the `MongoClient` with `MongoDbEventSubscriber`: - -[source,csharp] ----- -var settings = MongoClientSettings.FromConnectionString(mongoConnectionString); - -settings.ClusterConfigurator = builder => builder.Subscribe(new MongoDbEventSubscriber()); -var mongoClient = new MongoClient(settings); ----- +== Get started -Once the above configuration is in place, and if the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, it will automatically capture calls to MongoDB on every active transaction. -Otherwise, you can manually activate auto instrumentation from the `Elastic.Apm.MongoDb` package by calling +* <> +* <> +* <> +* <> +* <> -[source,csharp] ----- -Agent.Subscribe(new MongoDbDiagnosticsSubscriber()); ----- +include::./setup-auto-instrumentation.asciidoc[] +include::./setup-asp-net-core.asciidoc[] +include::./setup-dotnet-net-core.asciidoc[] +include::./setup-asp-dot-net.asciidoc[] [[setup-general]] === Other .NET applications If you have a .NET application that is not covered in this section, you can still use the agent and instrument your application manually. -To do this, add the https://www.nuget.org/packages/Elastic.Apm[Elastic.Apm] package to your application and use the <> to manually create spans and transactions. +To do this, add the {nuget}/Elastic.Apm[Elastic.Apm] package to your application and use the <> to manually create spans and transactions. diff --git a/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj b/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj index 746d160db..e3a563c03 100644 --- a/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj +++ b/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj @@ -336,7 +336,6 @@ - diff --git a/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandExecutor.cs b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandExecutor.cs new file mode 100644 index 000000000..416e5eebc --- /dev/null +++ b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandExecutor.cs @@ -0,0 +1,48 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Apm.AdoNet.NetStandard +{ + public abstract class DbCommandExecutor : IDbCommandExecutor + where TDbCommand : IDbCommand + { + public abstract string CommandTypeName { get; } + + public abstract bool SupportsAsyncMethods { get; } + + public abstract void ExecuteNonQuery(TDbCommand command); + public abstract Task ExecuteNonQueryAsync(TDbCommand command); + public abstract Task ExecuteNonQueryAsync(TDbCommand command, CancellationToken cancellationToken); + public abstract void ExecuteScalar(TDbCommand command); + public abstract Task ExecuteScalarAsync(TDbCommand command); + public abstract Task ExecuteScalarAsync(TDbCommand command, CancellationToken cancellationToken); + public abstract void ExecuteReader(TDbCommand command); + public abstract void ExecuteReader(TDbCommand command, CommandBehavior behavior); + public abstract Task ExecuteReaderAsync(TDbCommand command); + public abstract Task ExecuteReaderAsync(TDbCommand command, CommandBehavior behavior); + public abstract Task ExecuteReaderAsync(TDbCommand command, CancellationToken cancellationToken); + public abstract Task ExecuteReaderAsync(TDbCommand command, CommandBehavior behavior, CancellationToken cancellationToken); + + void IDbCommandExecutor.ExecuteNonQuery(IDbCommand command) => ExecuteNonQuery((TDbCommand)command); + Task IDbCommandExecutor.ExecuteNonQueryAsync(IDbCommand command) => ExecuteNonQueryAsync((TDbCommand)command); + Task IDbCommandExecutor.ExecuteNonQueryAsync(IDbCommand command, CancellationToken cancellationToken) => ExecuteNonQueryAsync((TDbCommand)command, cancellationToken); + void IDbCommandExecutor.ExecuteScalar(IDbCommand command) => ExecuteScalar((TDbCommand)command); + Task IDbCommandExecutor.ExecuteScalarAsync(IDbCommand command) => ExecuteScalarAsync((TDbCommand)command); + Task IDbCommandExecutor.ExecuteScalarAsync(IDbCommand command, CancellationToken cancellationToken) => ExecuteScalarAsync((TDbCommand)command, cancellationToken); + void IDbCommandExecutor.ExecuteReader(IDbCommand command) => ExecuteReader((TDbCommand)command); + void IDbCommandExecutor.ExecuteReader(IDbCommand command, CommandBehavior behavior) => ExecuteReader((TDbCommand)command, behavior); + Task IDbCommandExecutor.ExecuteReaderAsync(IDbCommand command) => ExecuteReaderAsync((TDbCommand)command); + Task IDbCommandExecutor.ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior) => ExecuteReaderAsync((TDbCommand)command, behavior); + Task IDbCommandExecutor.ExecuteReaderAsync(IDbCommand command, CancellationToken cancellationToken) => ExecuteReaderAsync((TDbCommand)command, cancellationToken); + Task IDbCommandExecutor.ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior, CancellationToken cancellationToken) => ExecuteReaderAsync((TDbCommand)command, behavior, cancellationToken); + } +} diff --git a/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardClassExecutor.cs b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardClassExecutor.cs new file mode 100644 index 000000000..e78cdcca2 --- /dev/null +++ b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardClassExecutor.cs @@ -0,0 +1,64 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Apm.AdoNet.NetStandard +{ + public class DbCommandNetStandardClassExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(DbCommand) + "-NetStandard"; + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(DbCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(DbCommand command) => command.ExecuteNonQueryAsync(); + + public override Task ExecuteNonQueryAsync(DbCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + + public override void ExecuteScalar(DbCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(DbCommand command) => command.ExecuteScalarAsync(); + + public override Task ExecuteScalarAsync(DbCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + + public override void ExecuteReader(DbCommand command) + { + using var reader = command.ExecuteReader(); + } + + public override void ExecuteReader(DbCommand command, CommandBehavior behavior) + { + using var reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(DbCommand command) + { + using var reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(DbCommand command, CommandBehavior behavior) + { + using var reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(DbCommand command, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(DbCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardInterfaceExecutor.cs b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardInterfaceExecutor.cs new file mode 100644 index 000000000..3d5278b68 --- /dev/null +++ b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardInterfaceExecutor.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Apm.AdoNet.NetStandard +{ + public class DbCommandNetStandardInterfaceExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(IDbCommand) + "-NetStandard"; + + public override bool SupportsAsyncMethods => false; + + public override void ExecuteNonQuery(IDbCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(IDbCommand command) => Task.CompletedTask; + + public override Task ExecuteNonQueryAsync(IDbCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteScalar(IDbCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(IDbCommand command) => Task.CompletedTask; + + public override Task ExecuteScalarAsync(IDbCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteReader(IDbCommand command) + { + using var reader = command.ExecuteReader(); + } + + public override void ExecuteReader(IDbCommand command, CommandBehavior behavior) + { + using var reader = command.ExecuteReader(behavior); + } + + public override Task ExecuteReaderAsync(IDbCommand command) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(IDbCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior, CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardInterfaceGenericExecutor.cs b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardInterfaceGenericExecutor.cs new file mode 100644 index 000000000..15eddcf01 --- /dev/null +++ b/sample/Elastic.Apm.AdoNet.NetStandard/DbCommandNetStandardInterfaceGenericExecutor.cs @@ -0,0 +1,52 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Apm.AdoNet.NetStandard +{ + public class DbCommandNetStandardInterfaceGenericExecutor : DbCommandExecutor + where TCommand : IDbCommand + { + public override string CommandTypeName => "IDbCommandGenericConstraint<" + typeof(TCommand).Name + ">-NetStandard"; + + public override bool SupportsAsyncMethods => false; + + public override void ExecuteNonQuery(TCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(TCommand command) => Task.CompletedTask; + + public override Task ExecuteNonQueryAsync(TCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteScalar(TCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(TCommand command) => Task.CompletedTask; + + public override Task ExecuteScalarAsync(TCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteReader(TCommand command) + { + using var reader = command.ExecuteReader(); + } + + public override void ExecuteReader(TCommand command, CommandBehavior behavior) + { + using var reader = command.ExecuteReader(behavior); + } + + public override Task ExecuteReaderAsync(TCommand command) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(TCommand command, CommandBehavior behavior) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(TCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(TCommand command, CommandBehavior behavior, CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/sample/Elastic.Apm.AdoNet.NetStandard/Elastic.Apm.AdoNet.NetStandard.csproj b/sample/Elastic.Apm.AdoNet.NetStandard/Elastic.Apm.AdoNet.NetStandard.csproj new file mode 100644 index 000000000..3b93f788b --- /dev/null +++ b/sample/Elastic.Apm.AdoNet.NetStandard/Elastic.Apm.AdoNet.NetStandard.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/sample/Elastic.Apm.AdoNet.NetStandard/IDbCommandExecutor.cs b/sample/Elastic.Apm.AdoNet.NetStandard/IDbCommandExecutor.cs new file mode 100644 index 000000000..4e7190405 --- /dev/null +++ b/sample/Elastic.Apm.AdoNet.NetStandard/IDbCommandExecutor.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Apm.AdoNet.NetStandard +{ + public interface IDbCommandExecutor + { + string CommandTypeName { get; } + bool SupportsAsyncMethods { get; } + + void ExecuteNonQuery(IDbCommand command); + Task ExecuteNonQueryAsync(IDbCommand command); + Task ExecuteNonQueryAsync(IDbCommand command, CancellationToken cancellationToken); + + void ExecuteScalar(IDbCommand command); + Task ExecuteScalarAsync(IDbCommand command); + Task ExecuteScalarAsync(IDbCommand command, CancellationToken cancellationToken); + + void ExecuteReader(IDbCommand command); + void ExecuteReader(IDbCommand command, CommandBehavior behavior); + Task ExecuteReaderAsync(IDbCommand command); + Task ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior); + Task ExecuteReaderAsync(IDbCommand command, CancellationToken cancellationToken); + Task ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior, CancellationToken cancellationToken); + } +} diff --git a/sample/Elastic.Apm.AdoNet/DbCommandClassExecutor.cs b/sample/Elastic.Apm.AdoNet/DbCommandClassExecutor.cs new file mode 100644 index 000000000..3bbdb08b1 --- /dev/null +++ b/sample/Elastic.Apm.AdoNet/DbCommandClassExecutor.cs @@ -0,0 +1,59 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; + +namespace Elastic.Apm.AdoNet +{ + public class DbCommandClassExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(DbCommand); + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(DbCommand command) => command.ExecuteNonQuery(); + public override Task ExecuteNonQueryAsync(DbCommand command) => command.ExecuteNonQueryAsync(); + public override Task ExecuteNonQueryAsync(DbCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + public override void ExecuteScalar(DbCommand command) => command.ExecuteScalar(); + public override Task ExecuteScalarAsync(DbCommand command) => command.ExecuteScalarAsync(); + public override Task ExecuteScalarAsync(DbCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + public override void ExecuteReader(DbCommand command) + { + using var reader = command.ExecuteReader(); + } + + public override void ExecuteReader(DbCommand command, CommandBehavior behavior) + { + using var reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(DbCommand command) + { + using var reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(DbCommand command, CommandBehavior behavior) + { + using var reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(DbCommand command, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(DbCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/Elastic.Apm.AdoNet/DbCommandExtensions.cs b/sample/Elastic.Apm.AdoNet/DbCommandExtensions.cs new file mode 100644 index 000000000..f930b39ab --- /dev/null +++ b/sample/Elastic.Apm.AdoNet/DbCommandExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; + +namespace Elastic.Apm.AdoNet +{ + public static class DbCommandExtensions + { + public static IDbDataParameter CreateParameterWithValue(this IDbCommand command, string name, object value) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value; + return parameter; + } + + public static IDbDataParameter AddParameterWithValue(this IDbCommand command, string name, object value) + { + var parameter = CreateParameterWithValue(command, name, value); + command.Parameters.Add(parameter); + return parameter; + } + } +} diff --git a/sample/Elastic.Apm.AdoNet/DbCommandFactory.cs b/sample/Elastic.Apm.AdoNet/DbCommandFactory.cs new file mode 100644 index 000000000..1a4e3666a --- /dev/null +++ b/sample/Elastic.Apm.AdoNet/DbCommandFactory.cs @@ -0,0 +1,74 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; + +namespace Elastic.Apm.AdoNet +{ + public class DbCommandFactory + { + protected string TableName { get; set; } + + protected IDbConnection Connection { get; } + + public DbCommandFactory(IDbConnection connection, string tableName) + { + Connection = connection; + TableName = tableName; + } + + public virtual IDbCommand GetCreateTableCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"DROP TABLE IF EXISTS {TableName}; CREATE TABLE {TableName} (Id int PRIMARY KEY, Name varchar(100));"; + return command; + } + + public virtual IDbCommand GetInsertRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"INSERT INTO {TableName} (Id, Name) VALUES (@Id, @Name);"; + command.AddParameterWithValue("Id", 1); + command.AddParameterWithValue("Name", "Name1"); + return command; + } + + public virtual IDbCommand GetUpdateRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"UPDATE {TableName} SET Name=@Name WHERE Id=@Id;"; + command.AddParameterWithValue("Name", "Name2"); + command.AddParameterWithValue("Id", 1); + return command; + } + + public virtual IDbCommand GetSelectScalarCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"SELECT Name FROM {TableName} WHERE Id=@Id;"; + command.AddParameterWithValue("Id", 1); + return command; + } + + public virtual IDbCommand GetSelectRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"SELECT * FROM {TableName} WHERE Id=@Id;"; + command.AddParameterWithValue("Id", 1); + return command; + } + + public virtual IDbCommand GetDeleteRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"DELETE FROM {TableName} WHERE Id=@Id;"; + command.AddParameterWithValue("Id", 1); + return command; + } + } +} diff --git a/sample/Elastic.Apm.AdoNet/DbCommandInterfaceExecutor.cs b/sample/Elastic.Apm.AdoNet/DbCommandInterfaceExecutor.cs new file mode 100644 index 000000000..5e263e38a --- /dev/null +++ b/sample/Elastic.Apm.AdoNet/DbCommandInterfaceExecutor.cs @@ -0,0 +1,52 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; + +namespace Elastic.Apm.AdoNet +{ + public class DbCommandInterfaceExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(IDbCommand); + + public override bool SupportsAsyncMethods => false; + + public override void ExecuteNonQuery(IDbCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(IDbCommand command) => Task.CompletedTask; + + public override Task ExecuteNonQueryAsync(IDbCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteScalar(IDbCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(IDbCommand command) => Task.CompletedTask; + + public override Task ExecuteScalarAsync(IDbCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteReader(IDbCommand command) + { + using var reader = command.ExecuteReader(); + } + + public override void ExecuteReader(IDbCommand command, CommandBehavior behavior) + { + using var reader = command.ExecuteReader(behavior); + } + + public override Task ExecuteReaderAsync(IDbCommand command) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(IDbCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(IDbCommand command, CommandBehavior behavior, CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/sample/Elastic.Apm.AdoNet/DbCommandInterfaceGenericExecutor.cs b/sample/Elastic.Apm.AdoNet/DbCommandInterfaceGenericExecutor.cs new file mode 100644 index 000000000..eedeed83e --- /dev/null +++ b/sample/Elastic.Apm.AdoNet/DbCommandInterfaceGenericExecutor.cs @@ -0,0 +1,53 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; + +namespace Elastic.Apm.AdoNet +{ + public class DbCommandInterfaceGenericExecutor : DbCommandExecutor + where TCommand : IDbCommand + { + public override string CommandTypeName => "IDbCommandGenericConstraint<" + typeof(TCommand).Name + ">"; + + public override bool SupportsAsyncMethods => false; + + public override void ExecuteNonQuery(TCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(TCommand command) => Task.CompletedTask; + + public override Task ExecuteNonQueryAsync(TCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteScalar(TCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(TCommand command) => Task.CompletedTask; + + public override Task ExecuteScalarAsync(TCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override void ExecuteReader(TCommand command) + { + using var reader = command.ExecuteReader(); + } + + public override void ExecuteReader(TCommand command, CommandBehavior behavior) + { + using var reader = command.ExecuteReader(behavior); + } + + public override Task ExecuteReaderAsync(TCommand command) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(TCommand command, CommandBehavior behavior) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(TCommand command, CancellationToken cancellationToken) => Task.CompletedTask; + + public override Task ExecuteReaderAsync(TCommand command, CommandBehavior behavior, CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/sample/Elastic.Apm.AdoNet/DbCommandRunner.cs b/sample/Elastic.Apm.AdoNet/DbCommandRunner.cs new file mode 100644 index 000000000..0e696ca39 --- /dev/null +++ b/sample/Elastic.Apm.AdoNet/DbCommandRunner.cs @@ -0,0 +1,274 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; +using Elastic.Apm.Api; + +namespace Elastic.Apm.AdoNet +{ + public class DbCommandRunner + { + /// + /// Creates a RunAllAsync<TDbCommand> transaction with the following spans, + /// when the profiler is attached: + /// + /// nameof(TDbCommand) command span: + /// + /// sync span + 7 command spans + /// async span + 7 command spans (if supports async) + /// async with cancellation span + 7 command spans (if supports async) + /// + /// 25 spans (if supports async), otherwise 9 spans + /// + /// + /// DbCommand command span: + /// + /// sync span + 7 command spans + /// async span + 7 command spans + /// async with cancellation span + 7 command spans + /// + /// 25 spans + /// + /// + /// IDbCommand command span: + /// + /// sync span + 7 command spans + /// + /// 9 spans + /// + /// + /// IDbCommandGenericConstraint<TDbCommand> command span: + /// + /// sync span + 7 command spans + /// + /// 9 spans + /// + /// + /// DbCommand-NetStandard command span: + /// + /// sync span + 7 command spans + /// async span + 7 command spans + /// async with cancellation span + 7 command spans + /// + /// 25 spans + /// + /// + /// IDbCommand-NetStandard command span: + /// + /// sync span + 7 command spans + /// + /// 9 spans + /// + /// + /// IDbCommandGenericConstraint<TDbCommand>-NetStandard command span: + /// + /// sync span + 7 command spans + /// + /// 9 spans + /// + /// + /// 111 spans total (if supports async), otherwise 95 spans + /// + public static async Task RunAllAsync( + DbCommandFactory commandFactory, + IDbCommandExecutor providerCommandExecutor, + CancellationToken token + ) where TDbCommand : IDbCommand + { + var executors = new List + { + providerCommandExecutor, + new DbCommandClassExecutor(), + new DbCommandInterfaceExecutor(), + new DbCommandInterfaceGenericExecutor(), + + // call methods referencing netstandard.dll + new DbCommandNetStandardClassExecutor(), + new DbCommandNetStandardInterfaceExecutor(), + new DbCommandNetStandardInterfaceGenericExecutor(), + }; + + await Agent.Tracer.CaptureTransaction("RunAllAsync", "test", async transaction => + { + foreach (var executor in executors) + await RunAsync(transaction, commandFactory, executor, token); + }); + } + + /// + /// Creates a RunBaseTypesAsync transaction with the following spans, + /// when the profiler is attached: + /// + /// DbCommand command span: + /// + /// sync span + 7 command spans + /// async span + 7 command spans + /// async with cancellation span + 7 command spans + /// + /// 25 spans + /// + /// + /// IDbCommand command span: + /// + /// sync span + 7 command spans + /// + /// 9 spans + /// + /// + /// DbCommand-NetStandard command span: + /// + /// sync span + 7 command spans + /// async span + 7 command spans + /// async with cancellation span + 7 command spans + /// + /// 25 spans + /// + /// + /// IDbCommand-NetStandard command span: + /// + /// sync span + 7 command spans + /// + /// 9 spans + /// + /// + /// 68 spans total + /// + public static async Task RunBaseTypesAsync( + DbCommandFactory commandFactory, + CancellationToken token + ) + { + var executors = new List + { + new DbCommandClassExecutor(), + new DbCommandInterfaceExecutor(), + + // call methods referencing netstandard.dll + new DbCommandNetStandardClassExecutor(), + new DbCommandNetStandardInterfaceExecutor(), + }; + + await Agent.Tracer.CaptureTransaction("RunBaseTypesAsync", "test", async transaction => + { + foreach (var executor in executors) + await RunAsync(transaction, commandFactory, executor, token); + }); + } + + private static async Task RunAsync( + ITransaction transaction, + DbCommandFactory commandFactory, + IDbCommandExecutor commandExecutor, + CancellationToken cancellationToken + ) + { + var commandName = commandExecutor.CommandTypeName; + Console.WriteLine(commandName); + + await transaction.CaptureSpan($"{commandName} command", "command", async span => + { + IDbCommand command; + + await span.CaptureSpan($"{commandName} sync", "sync", async childSpan => + { + Console.WriteLine(" synchronous"); + await Task.Delay(100, cancellationToken); + + command = commandFactory.GetCreateTableCommand(); + commandExecutor.ExecuteNonQuery(command); + + command = commandFactory.GetInsertRowCommand(); + commandExecutor.ExecuteNonQuery(command); + + command = commandFactory.GetSelectScalarCommand(); + commandExecutor.ExecuteScalar(command); + + command = commandFactory.GetUpdateRowCommand(); + commandExecutor.ExecuteNonQuery(command); + + command = commandFactory.GetSelectRowCommand(); + commandExecutor.ExecuteReader(command); + + command = commandFactory.GetSelectRowCommand(); + commandExecutor.ExecuteReader(command, CommandBehavior.Default); + + command = commandFactory.GetDeleteRowCommand(); + commandExecutor.ExecuteNonQuery(command); + }); + + if (commandExecutor.SupportsAsyncMethods) + { + await Task.Delay(100, cancellationToken); + + await span.CaptureSpan($"{commandName} async", "async", async childSpan => + { + Console.WriteLine(" asynchronous"); + await Task.Delay(100, cancellationToken); + + command = commandFactory.GetCreateTableCommand(); + await commandExecutor.ExecuteNonQueryAsync(command); + + command = commandFactory.GetInsertRowCommand(); + await commandExecutor.ExecuteNonQueryAsync(command); + + command = commandFactory.GetSelectScalarCommand(); + await commandExecutor.ExecuteScalarAsync(command); + + command = commandFactory.GetUpdateRowCommand(); + await commandExecutor.ExecuteNonQueryAsync(command); + + command = commandFactory.GetSelectRowCommand(); + await commandExecutor.ExecuteReaderAsync(command); + + command = commandFactory.GetSelectRowCommand(); + await commandExecutor.ExecuteReaderAsync(command, CommandBehavior.Default); + + command = commandFactory.GetDeleteRowCommand(); + await commandExecutor.ExecuteNonQueryAsync(command); + }); + + await Task.Delay(100, cancellationToken); + + await span.CaptureSpan($"{commandName} async with cancellation", "async-cancellation", async childSpan => + { + Console.WriteLine(" asynchronous with cancellation"); + await Task.Delay(100, cancellationToken); + + command = commandFactory.GetCreateTableCommand(); + await commandExecutor.ExecuteNonQueryAsync(command, cancellationToken); + + command = commandFactory.GetInsertRowCommand(); + await commandExecutor.ExecuteNonQueryAsync(command, cancellationToken); + + command = commandFactory.GetSelectScalarCommand(); + await commandExecutor.ExecuteScalarAsync(command, cancellationToken); + + command = commandFactory.GetUpdateRowCommand(); + await commandExecutor.ExecuteNonQueryAsync(command, cancellationToken); + + command = commandFactory.GetSelectRowCommand(); + await commandExecutor.ExecuteReaderAsync(command, cancellationToken); + + command = commandFactory.GetSelectRowCommand(); + await commandExecutor.ExecuteReaderAsync(command, CommandBehavior.Default, cancellationToken); + + command = commandFactory.GetDeleteRowCommand(); + await commandExecutor.ExecuteNonQueryAsync(command, cancellationToken); + }); + } + }); + + await Task.Delay(100, cancellationToken); + } + } +} diff --git a/sample/Elastic.Apm.AdoNet/Elastic.Apm.AdoNet.csproj b/sample/Elastic.Apm.AdoNet/Elastic.Apm.AdoNet.csproj new file mode 100644 index 000000000..a9f362d62 --- /dev/null +++ b/sample/Elastic.Apm.AdoNet/Elastic.Apm.AdoNet.csproj @@ -0,0 +1,14 @@ + + + + net461;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0; + + + + + + + + + + diff --git a/sample/KafkaSample/Consumer.cs b/sample/KafkaSample/Consumer.cs new file mode 100644 index 000000000..8094d0191 --- /dev/null +++ b/sample/KafkaSample/Consumer.cs @@ -0,0 +1,208 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on Producer type in https://github.com/DataDog/dd-trace-dotnet/blob/master/tracer/test/test-applications/integrations/Samples.Kafka/Consumer.cs +// Licensed under Apache 2.0 + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Confluent.Kafka; +using Elastic.Apm; +using Elastic.Apm.Api; +using Newtonsoft.Json; + +namespace KafkaSample +{ + internal class Consumer: IDisposable + { + private readonly string _consumerName; + private readonly IConsumer _consumer; + + public static int TotalAsyncMessages = 0; + public static int TotalSyncMessages = 0; + public static int TotalTombstones = 0; + + private Consumer(ConsumerConfig config, string topic, string consumerName) + { + _consumerName = consumerName; + _consumer = new ConsumerBuilder(config).Build(); + _consumer.Subscribe(topic); + } + + public bool Consume(int retries, int timeoutMilliSeconds) + { + try + { + for (var i = 0; i < retries; i++) + { + try + { + // will block until a message is available + // on 1.5.3 this will throw if the topic doesn't exist + var consumeResult = _consumer.Consume(timeoutMilliSeconds); + if (consumeResult is null) + { + Console.WriteLine($"{_consumerName}: Null consume result"); + return true; + } + + if (consumeResult.IsPartitionEOF) + { + Console.WriteLine($"{_consumerName}: Reached EOF"); + return true; + } + + HandleMessage(consumeResult); + return true; + } + catch (ConsumeException ex) + { + Console.WriteLine($"Consume Exception in manual consume: {ex}"); + } + + Task.Delay(500); + } + } + catch (TaskCanceledException) + { + Console.WriteLine($"{_consumerName}: Cancellation requested, exiting."); + } + + return false; + } + + public void Consume(CancellationToken cancellationToken = default) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + // will block until a message is available + var consumeResult = _consumer.Consume(cancellationToken); + + if (consumeResult.IsPartitionEOF) + Console.WriteLine($"{_consumerName}: Reached EOF"); + else + HandleMessage(consumeResult); + } + } + catch (TaskCanceledException) + { + Console.WriteLine($"{_consumerName}: Cancellation requested, exiting."); + } + } + + public void ConsumeWithExplicitCommit(int commitEveryXMessages, CancellationToken cancellationToken = default) + { + ConsumeResult consumeResult = null; + try + { + while (!cancellationToken.IsCancellationRequested) + { + // will block until a message is available + consumeResult = _consumer.Consume(cancellationToken); + + if (consumeResult.IsPartitionEOF) + Console.WriteLine($"{_consumerName}: Reached EOF"); + else + HandleMessage(consumeResult); + + if (consumeResult.Offset % commitEveryXMessages == 0) + { + try + { + Console.WriteLine($"{_consumerName}: committing..."); + _consumer.Commit(consumeResult); + } + catch (KafkaException e) + { + Console.WriteLine($"{_consumerName}: commit error: {e.Error.Reason}"); + } + } + } + } + catch (TaskCanceledException) + { + Console.WriteLine($"{_consumerName}: Cancellation requested, exiting."); + } + + // As we're doing manual commit, make sure we force a commit now + if (consumeResult is not null) + { + Console.WriteLine($"{_consumerName}: committing..."); + _consumer.Commit(consumeResult); + } + } + + private void HandleMessage(ConsumeResult consumeResult) + { + var transaction = Agent.Tracer.CurrentTransaction; + + ISpan span = null; + if (transaction != null) + span = transaction.StartSpan("Consume message", "kafka"); + + var kafkaMessage = consumeResult.Message; + Console.WriteLine($"{_consumerName}: Consuming {kafkaMessage.Key}, {consumeResult.TopicPartitionOffset}"); + + var headers = kafkaMessage.Headers; + var traceParent = headers.TryGetLastBytes("traceparent", out var traceParentHeader) + ? Encoding.UTF8.GetString(traceParentHeader) + : null; + + var traceState = headers.TryGetLastBytes("tracestate", out var traceStateHeader) + ? Encoding.UTF8.GetString(traceStateHeader) + : null; + + if (traceParent is null || traceState is null) + { + // For kafka brokers < 0.11.0, we can't inject custom headers, so context will not be propagated + var errorMessage = $"Error extracting trace context for {kafkaMessage.Key}, {consumeResult.TopicPartitionOffset}"; + Console.WriteLine(errorMessage); + } + else + Console.WriteLine($"Successfully extracted trace context from message: {traceParent}, {traceState}"); + + + if (string.IsNullOrEmpty(kafkaMessage.Value)) + { + Console.WriteLine($"Received Tombstone for {kafkaMessage.Key}"); + Interlocked.Increment(ref TotalTombstones); + } + else + { + var sampleMessage = JsonConvert.DeserializeObject(kafkaMessage.Value); + Console.WriteLine($"Received {(sampleMessage.IsProducedAsync ? "async" : "sync")}message for {kafkaMessage.Key}"); + if (sampleMessage.IsProducedAsync) + Interlocked.Increment(ref TotalAsyncMessages); + else + Interlocked.Increment(ref TotalSyncMessages); + } + + span?.End(); + } + + public void Dispose() + { + Console.WriteLine($"{_consumerName}: Closing consumer"); + _consumer?.Close(); + _consumer?.Dispose(); + } + + public static Consumer Create(ClientConfig clientConfig, bool enableAutoCommit, string topic, string consumerName) + { + Console.WriteLine($"Creating consumer '{consumerName}' and subscribing to topic {topic}"); + var config = new ConsumerConfig(clientConfig) + { + GroupId = "KafkaSample", + AutoOffsetReset = AutoOffsetReset.Earliest, + EnableAutoCommit = enableAutoCommit, + }; + return new Consumer(config, topic, consumerName); + } + } +} diff --git a/sample/KafkaSample/KafkaSample.csproj b/sample/KafkaSample/KafkaSample.csproj new file mode 100644 index 000000000..52f8555c7 --- /dev/null +++ b/sample/KafkaSample/KafkaSample.csproj @@ -0,0 +1,18 @@ + + + + 1.4.3 + Exe + net5.0 + + + + + + + + + + + + diff --git a/sample/KafkaSample/Producer.cs b/sample/KafkaSample/Producer.cs new file mode 100644 index 000000000..e8cabfe0e --- /dev/null +++ b/sample/KafkaSample/Producer.cs @@ -0,0 +1,103 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on Producer type in https://github.com/DataDog/dd-trace-dotnet/blob/master/tracer/test/test-applications/integrations/Samples.Kafka/Producer.cs +// Licensed under Apache 2.0 + +using System; +using System.Threading; +using System.Threading.Tasks; +using Confluent.Kafka; +using Elastic.Apm; +using Elastic.Apm.Api; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace KafkaSample +{ + internal static class Producer + { + // Flush every x messages + private const int FlushInterval = 3; + private static readonly TimeSpan FlushTimeout = TimeSpan.FromSeconds(5); + private static int MessageNumber = 0; + + public static async Task ProduceAsync(string topic, int numMessages, ClientConfig config, bool isTombstone) => + await Agent.Tracer.CaptureTransaction($"Produce Messages Async {topic}", ApiConstants.TypeMessaging, async () => + { + using var producer = new ProducerBuilder(config).Build(); + for (var i = 0; i < numMessages; ++i) + { + var messageNumber = Interlocked.Increment(ref MessageNumber); + var key = $"{messageNumber}-Async{(isTombstone ? "-tombstone" : "")}"; + var value = isTombstone ? null : GetMessage(i, isProducedAsync: true); + var message = new Message { Key = key, Value = value }; + + Console.WriteLine($"Producing record {i}: {key}..."); + + try + { + var deliveryResult = await producer.ProduceAsync(topic, message); + Console.WriteLine($"Produced message to: {deliveryResult.TopicPartitionOffset}"); + + } + catch (ProduceException ex) + { + Console.WriteLine($"Failed to deliver message: {ex.Error.Reason}"); + } + } + + Flush(producer); + Console.WriteLine($"Finished producing {numMessages} messages to topic {topic}"); + }); + + private static void Flush(IProducer producer) + { + var queueLength = 1; + while (queueLength > 0) + queueLength = producer.Flush(FlushTimeout); + } + + public static void Produce(string topic, int numMessages, ClientConfig config, bool handleDelivery, bool isTombstone) => + Produce(topic, numMessages, config, handleDelivery ? HandleDelivery : null, isTombstone); + + private static void Produce(string topic, int numMessages, ClientConfig config, Action> deliveryHandler, bool isTombstone) => + Agent.Tracer.CaptureTransaction($"Produce Messages Sync {topic}", ApiConstants.TypeMessaging, () => + { + using (var producer = new ProducerBuilder(config).Build()) + { + for (var i = 0; i < numMessages; ++i) + { + var messageNumber = Interlocked.Increment(ref MessageNumber); + var hasHandler = deliveryHandler is not null; + var key = $"{messageNumber}-Sync-{hasHandler}{(isTombstone ? "-tombstone" : "")}"; + var value = isTombstone ? null : GetMessage(i, isProducedAsync: false); + var message = new Message { Key = key, Value = value }; + + Console.WriteLine($"Producing record {i}: {message.Key}..."); + + producer.Produce(topic, message, deliveryHandler); + + if (numMessages % FlushInterval == 0) + producer.Flush(FlushTimeout); + } + Flush(producer); + + Console.WriteLine($"Finished producing {numMessages} messages to topic {topic}"); + } + }); + + private static void HandleDelivery(DeliveryReport deliveryReport) => + Console.WriteLine(deliveryReport.Error.Code != ErrorCode.NoError + ? $"Failed to deliver message: {deliveryReport.Error.Reason}" + : $"Produced message to: {deliveryReport.TopicPartitionOffset}"); + + private static string GetMessage(int iteration, bool isProducedAsync) + { + var message = new SampleMessage("fruit", iteration, isProducedAsync); + return JObject.FromObject(message).ToString(Formatting.None); + } + } +} diff --git a/sample/KafkaSample/Program.cs b/sample/KafkaSample/Program.cs new file mode 100644 index 000000000..7981104a3 --- /dev/null +++ b/sample/KafkaSample/Program.cs @@ -0,0 +1,167 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on Producer type in https://github.com/DataDog/dd-trace-dotnet/blob/master/tracer/test/test-applications/integrations/Samples.Kafka +// Licensed under Apache 2.0 + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Confluent.Kafka; +using Confluent.Kafka.Admin; + +namespace KafkaSample +{ + internal class Program + { + private static async Task Main(string[] args) + { + var kafkaHost = Environment.GetEnvironmentVariable("KAFKA_HOST"); + + if (string.IsNullOrEmpty(kafkaHost)) + throw new InvalidOperationException( + "KAFKA_HOST environment variable is empty. Kafka host must be specified with KAFKA_HOST environment variable"); + + var config = new ClientConfig { BootstrapServers = kafkaHost }; + var topic = Guid.NewGuid().ToString("N"); + + await CreateTopic(config, topic); + + await ConsumeAndProduceMessages(topic, config); + + // allow time for agent to send APM data + await Task.Delay(TimeSpan.FromSeconds(30)); + + Console.WriteLine("finished"); + } + + private static async Task ConsumeAndProduceMessages(string topic, ClientConfig config) + { + var commitPeriod = 3; + var cts = new CancellationTokenSource(); + + using var consumer1 = Consumer.Create(config, true, topic, "AutoCommitConsumer1"); + using var consumer2 = Consumer.Create(config, false, topic, "ManualCommitConsumer2"); + + Console.WriteLine("Starting consumers..."); + + var consumeTask1 = Task.Run(() => consumer1.Consume(cts.Token)); + var consumeTask2 = Task.Run(() => consumer2.ConsumeWithExplicitCommit(commitEveryXMessages: commitPeriod, cts.Token)); + + Console.WriteLine($"Producing messages"); + + var messagesProduced = await ProduceMessages(topic, config); + + // Wait for all messages to be consumed + // This assumes that the topic starts empty, and nothing else is producing to the topic + var deadline = DateTime.UtcNow.AddSeconds(30); + while (true) + { + var syncCount = Volatile.Read(ref Consumer.TotalSyncMessages); + var asyncCount = Volatile.Read(ref Consumer.TotalAsyncMessages); + var tombstoneCount = Volatile.Read(ref Consumer.TotalTombstones); + + if (syncCount >= messagesProduced.SyncMessages + && asyncCount >= messagesProduced.AsyncMessages + && tombstoneCount >= messagesProduced.TombstoneMessages) + { + Console.WriteLine($"All messages produced and consumed"); + break; + } + + if (DateTime.UtcNow > deadline) + { + Console.WriteLine($"Exiting consumer: did not consume all messages syncCount {syncCount}, asyncCount {asyncCount}"); + break; + } + + await Task.Delay(1000); + } + + cts.Cancel(); + Console.WriteLine($"Waiting for graceful exit..."); + + await Task.WhenAny( + Task.WhenAll(consumeTask1, consumeTask2), + Task.Delay(TimeSpan.FromSeconds(5))); + } + + private static async Task ProduceMessages(string topic, ClientConfig config) + { + // produce messages sync and async + const int numberOfMessagesPerProducer = 10; + + // Send valid messages + Producer.Produce(topic, numberOfMessagesPerProducer, config, false, false); + Producer.Produce(topic, numberOfMessagesPerProducer, config, true, false); + await Producer.ProduceAsync(topic, numberOfMessagesPerProducer, config, false); + + // Send tombstone messages + Producer.Produce(topic, numberOfMessagesPerProducer, config, false, true); + Producer.Produce(topic, numberOfMessagesPerProducer, config, true, true); + await Producer.ProduceAsync(topic, numberOfMessagesPerProducer, config, true); + + // try to produce invalid messages + const string invalidTopic = "INVALID-TOPIC"; + + Producer.Produce(invalidTopic, 1, config, true, false); // failure should be logged by delivery handler + + try + { + await Producer.ProduceAsync(invalidTopic, 1, config, isTombstone: false); + } + catch (Exception ex) + { + Console.WriteLine($"Error producing a message to an unknown topic (expected): {ex}"); + } + + return new MessagesProduced + { + SyncMessages = numberOfMessagesPerProducer * 2, + AsyncMessages = numberOfMessagesPerProducer * 1, + TombstoneMessages = numberOfMessagesPerProducer * 3, + }; + } + + private struct MessagesProduced + { + public int SyncMessages; + public int AsyncMessages; + public int TombstoneMessages; + } + + private static async Task CreateTopic(ClientConfig config, string topic) + { + using var adminClient = new AdminClientBuilder(config).Build(); + try + { + Console.WriteLine($"Creating topic {topic}..."); + + await adminClient.CreateTopicsAsync(new List + { + new() + { + Name = topic, + NumPartitions = 1, + ReplicationFactor = 1 + } + }); + + Console.WriteLine($"Topic created"); + } + catch (CreateTopicsException e) + { + if (e.Results[0].Error.Code == ErrorCode.TopicAlreadyExists) + Console.WriteLine("Topic already exists"); + else + { + Console.WriteLine($"An error occured creating topic {topic}: {e.Results[0].Error.Reason}"); + throw; + } + } + } + } +} diff --git a/sample/KafkaSample/SampleMessage.cs b/sample/KafkaSample/SampleMessage.cs new file mode 100644 index 000000000..5a720d298 --- /dev/null +++ b/sample/KafkaSample/SampleMessage.cs @@ -0,0 +1,24 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on Producer type in https://github.com/DataDog/dd-trace-dotnet/blob/master/tracer/test/test-applications/integrations/Samples.Kafka/Producer.cs#L107-L119 +// Licensed under Apache 2.0 + +namespace KafkaSample +{ + public class SampleMessage + { + public string Category { get; } + public int MessageNumber { get; } + public bool IsProducedAsync { get; } + + public SampleMessage(string category, int messageNumber, bool isProducedAsync) + { + Category = category; + MessageNumber = messageNumber; + IsProducedAsync = isProducedAsync; + } + } +} diff --git a/sample/MySqlDataSample/MySqlDataCommandExecutor.cs b/sample/MySqlDataSample/MySqlDataCommandExecutor.cs new file mode 100644 index 000000000..ec7860a74 --- /dev/null +++ b/sample/MySqlDataSample/MySqlDataCommandExecutor.cs @@ -0,0 +1,65 @@ +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; +using MySql.Data.MySqlClient; + +namespace MySqlDataSample +{ + public class MySqlCommandExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(MySqlCommand); + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(MySqlCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(MySqlCommand command) => command.ExecuteNonQueryAsync(); + + public override Task ExecuteNonQueryAsync(MySqlCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + + public override void ExecuteScalar(MySqlCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(MySqlCommand command) => command.ExecuteScalarAsync(); + + public override Task ExecuteScalarAsync(MySqlCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + + public override void ExecuteReader(MySqlCommand command) + { + using DbDataReader reader = command.ExecuteReader(); + } + + public override void ExecuteReader(MySqlCommand command, CommandBehavior behavior) + { + using DbDataReader reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(MySqlCommand command) + { + using var reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(MySqlCommand command, CommandBehavior behavior) + { + using var reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(MySqlCommand command, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(MySqlCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/MySqlDataSample/MySqlDataSample.csproj b/sample/MySqlDataSample/MySqlDataSample.csproj new file mode 100644 index 000000000..aa9ba1692 --- /dev/null +++ b/sample/MySqlDataSample/MySqlDataSample.csproj @@ -0,0 +1,17 @@ + + + + 8.0.26 + Exe + net461;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0; + + + + + + + + + + + \ No newline at end of file diff --git a/sample/MySqlDataSample/Program.cs b/sample/MySqlDataSample/Program.cs new file mode 100644 index 000000000..4c82fdaac --- /dev/null +++ b/sample/MySqlDataSample/Program.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Data.Common; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet; +using MySql.Data.MySqlClient; + +namespace MySqlDataSample +{ + internal static class Program + { + private static async Task Main(string[] args) + { + var cancellationTokenSource = new CancellationTokenSource(); + var connectionType = typeof(MySqlConnection); + var guid = Guid.NewGuid().ToString("N"); + + Console.WriteLine($"Run commands ({guid})"); + + using (var connection = CreateAndOpenConnection(connectionType)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"mysql_all_{guid}"); + + await DbCommandRunner.RunAllAsync( + dbCommandFactory, + new MySqlCommandExecutor(), + cancellationTokenSource.Token); + } + + var npgsqlAssembly = Assembly.LoadFile(connectionType.Assembly.Location); + var npgsqlConnection = npgsqlAssembly.GetType(connectionType.FullName); + + using (var connection = CreateAndOpenConnection(npgsqlConnection)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"mysql_basetypes_{guid}"); + + await DbCommandRunner.RunBaseTypesAsync( + dbCommandFactory, + cancellationTokenSource.Token); + } + + Console.WriteLine("Finished sending commands"); + + // allow the agent time to send the spans + await Task.Delay(TimeSpan.FromSeconds(40), cancellationTokenSource.Token); + return 0; + } + + private static DbConnection CreateAndOpenConnection(Type connectionType) + { + var connectionString = Environment.GetEnvironmentVariable("MYSQL_CONNECTION_STRING"); + var connection = Activator.CreateInstance(connectionType, connectionString) as DbConnection; + connection.Open(); + return connection; + } + } +} diff --git a/sample/NpgsqlSample/NpgsqlCommandExecutor.cs b/sample/NpgsqlSample/NpgsqlCommandExecutor.cs new file mode 100644 index 000000000..71bce39f6 --- /dev/null +++ b/sample/NpgsqlSample/NpgsqlCommandExecutor.cs @@ -0,0 +1,65 @@ +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; +using Npgsql; + +namespace NpgsqlSample +{ + public class NpgsqlCommandExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(NpgsqlCommand); + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(NpgsqlCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(NpgsqlCommand command) => command.ExecuteNonQueryAsync(); + + public override Task ExecuteNonQueryAsync(NpgsqlCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + + public override void ExecuteScalar(NpgsqlCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(NpgsqlCommand command) => command.ExecuteScalarAsync(); + + public override Task ExecuteScalarAsync(NpgsqlCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + + public override void ExecuteReader(NpgsqlCommand command) + { + using DbDataReader reader = command.ExecuteReader(); + } + + public override void ExecuteReader(NpgsqlCommand command, CommandBehavior behavior) + { + using DbDataReader reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(NpgsqlCommand command) + { + using DbDataReader reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(NpgsqlCommand command, CommandBehavior behavior) + { + using DbDataReader reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(NpgsqlCommand command, CancellationToken cancellationToken) + { + using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(NpgsqlCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using DbDataReader reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/NpgsqlSample/NpgsqlSample.csproj b/sample/NpgsqlSample/NpgsqlSample.csproj new file mode 100644 index 000000000..756c30442 --- /dev/null +++ b/sample/NpgsqlSample/NpgsqlSample.csproj @@ -0,0 +1,17 @@ + + + + 5.0.7 + Exe + net461;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0; + + + + + + + + + + + diff --git a/sample/NpgsqlSample/Program.cs b/sample/NpgsqlSample/Program.cs new file mode 100644 index 000000000..a5a0ebe55 --- /dev/null +++ b/sample/NpgsqlSample/Program.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Data.Common; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet; +using Npgsql; + +namespace NpgsqlSample +{ + internal static class Program + { + private static async Task Main(string[] args) + { + var cancellationTokenSource = new CancellationTokenSource(); + var connectionType = typeof(NpgsqlConnection); + var guid = Guid.NewGuid().ToString("N"); + + Console.WriteLine($"Run commands ({guid})"); + + using (var connection = CreateAndOpenConnection(connectionType)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"\"npgsql_all_{guid}\""); + + await DbCommandRunner.RunAllAsync( + dbCommandFactory, + new NpgsqlCommandExecutor(), + cancellationTokenSource.Token); + } + + var npgsqlAssembly = Assembly.LoadFile(connectionType.Assembly.Location); + var npgsqlConnection = npgsqlAssembly.GetType(connectionType.FullName); + + using (var connection = CreateAndOpenConnection(npgsqlConnection)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"\"npgsql_basetypes_{guid}\""); + + await DbCommandRunner.RunBaseTypesAsync( + dbCommandFactory, + cancellationTokenSource.Token); + } + + Console.WriteLine("Finished sending commands"); + + // allow the agent time to send the spans + await Task.Delay(TimeSpan.FromSeconds(40), cancellationTokenSource.Token); + return 0; + } + + private static DbConnection CreateAndOpenConnection(Type connectionType) + { + var connectionString = Environment.GetEnvironmentVariable("POSTGRES_CONNECTION_STRING"); + var connection = Activator.CreateInstance(connectionType, connectionString) as DbConnection; + connection.Open(); + return connection; + } + } +} diff --git a/sample/OracleManagedDataAccessCoreSample/OracleCommandExecutor.cs b/sample/OracleManagedDataAccessCoreSample/OracleCommandExecutor.cs new file mode 100644 index 000000000..93813f17a --- /dev/null +++ b/sample/OracleManagedDataAccessCoreSample/OracleCommandExecutor.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; +using Oracle.ManagedDataAccess.Client; + +namespace OracleManagedDataAccessCoreSample +{ + public class OracleCommandExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(OracleCommand); + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(OracleCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(OracleCommand command) => command.ExecuteNonQueryAsync(); + + public override Task ExecuteNonQueryAsync(OracleCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + + public override void ExecuteScalar(OracleCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(OracleCommand command) => command.ExecuteScalarAsync(); + + public override Task ExecuteScalarAsync(OracleCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + + public override void ExecuteReader(OracleCommand command) + { + using DbDataReader reader = command.ExecuteReader(); + } + + public override void ExecuteReader(OracleCommand command, CommandBehavior behavior) + { + using DbDataReader reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(OracleCommand command) + { + using var reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(OracleCommand command, CommandBehavior behavior) + { + using var reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(OracleCommand command, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(OracleCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/OracleManagedDataAccessCoreSample/OracleDbCommandFactory.cs b/sample/OracleManagedDataAccessCoreSample/OracleDbCommandFactory.cs new file mode 100644 index 000000000..5c3229783 --- /dev/null +++ b/sample/OracleManagedDataAccessCoreSample/OracleDbCommandFactory.cs @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Data; +using Elastic.Apm.AdoNet; + +namespace OracleManagedDataAccessCoreSample +{ + public class OracleDbCommandFactory : DbCommandFactory + { + private int _count = 0; + private readonly string _tableName; + + public OracleDbCommandFactory(IDbConnection connection, string tableName) + : base(connection, tableName) => + _tableName = tableName; + + public override IDbCommand GetCreateTableCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"CREATE TABLE {TableName} (Id number(10) not null, Name varchar2(100) not null)"; + return command; + } + + public override IDbCommand GetInsertRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"INSERT INTO {TableName} (Id, Name) VALUES (:Id, :Name)"; + command.AddParameterWithValue("Id", 1); + command.AddParameterWithValue("Name", "Name1"); + return command; + } + + public override IDbCommand GetSelectScalarCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"SELECT Name FROM {TableName} WHERE Id=:Id"; + command.AddParameterWithValue("Id", 1); + return command; + } + + public override IDbCommand GetUpdateRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"UPDATE {TableName} SET Name=:Name WHERE Id=:Id"; + command.AddParameterWithValue("Name", "Name2"); + command.AddParameterWithValue("Id", 1); + return command; + } + + public override IDbCommand GetSelectRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"SELECT * FROM {TableName} WHERE Id=:Id"; + command.AddParameterWithValue("Id", 1); + return command; + } + + public override IDbCommand GetDeleteRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"DELETE FROM {TableName} WHERE Id=:Id"; + command.AddParameterWithValue("Id", 1); + TableName = _tableName + _count++; + return command; + } + } +} diff --git a/sample/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj b/sample/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj new file mode 100644 index 000000000..f5845097a --- /dev/null +++ b/sample/OracleManagedDataAccessCoreSample/OracleManagedDataAccessCoreSample.csproj @@ -0,0 +1,17 @@ + + + + 2.19.120 + Exe + net461;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0; + + + + + + + + + + + diff --git a/sample/OracleManagedDataAccessCoreSample/Program.cs b/sample/OracleManagedDataAccessCoreSample/Program.cs new file mode 100644 index 000000000..b304adad0 --- /dev/null +++ b/sample/OracleManagedDataAccessCoreSample/Program.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Data.Common; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet; +using Oracle.ManagedDataAccess.Client; + +namespace OracleManagedDataAccessCoreSample +{ + internal static class Program + { + private static async Task Main(string[] args) + { + var cancellationTokenSource = new CancellationTokenSource(); + var connectionType = typeof(OracleConnection); + var guid = Guid.NewGuid().ToString("N").Substring(0, 8); + + Console.WriteLine($"Run commands ({guid})"); + + using (var connection = CreateAndOpenConnection(connectionType)) + { + var dbCommandFactory = new OracleDbCommandFactory(connection, $"oracle_all_{guid}"); + + await DbCommandRunner.RunAllAsync( + dbCommandFactory, + new OracleCommandExecutor(), + cancellationTokenSource.Token); + } + + var oracleAssembly = Assembly.LoadFile(connectionType.Assembly.Location); + var oracleConnection = oracleAssembly.GetType(connectionType.FullName); + + using (var connection = CreateAndOpenConnection(oracleConnection)) + { + var dbCommandFactory = new OracleDbCommandFactory(connection, $"oracle_basetypes_{guid}"); + + await DbCommandRunner.RunBaseTypesAsync( + dbCommandFactory, + cancellationTokenSource.Token); + } + + Console.WriteLine("Finished sending commands"); + + // allow the agent time to send the spans + await Task.Delay(TimeSpan.FromSeconds(40), cancellationTokenSource.Token); + return 0; + } + + private static DbConnection CreateAndOpenConnection(Type connectionType) + { + var connectionString = Environment.GetEnvironmentVariable("ORACLE_CONNECTION_STRING"); + var connection = Activator.CreateInstance(connectionType, connectionString) as DbConnection; + connection.Open(); + return connection; + } + } +} diff --git a/sample/OracleManagedDataAccessSample/OracleCommandExecutor.cs b/sample/OracleManagedDataAccessSample/OracleCommandExecutor.cs new file mode 100644 index 000000000..6fb4d9fd7 --- /dev/null +++ b/sample/OracleManagedDataAccessSample/OracleCommandExecutor.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; +using Oracle.ManagedDataAccess.Client; + +namespace OracleManagedDataAccessSample +{ + public class OracleCommandExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(OracleCommand); + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(OracleCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(OracleCommand command) => command.ExecuteNonQueryAsync(); + + public override Task ExecuteNonQueryAsync(OracleCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + + public override void ExecuteScalar(OracleCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(OracleCommand command) => command.ExecuteScalarAsync(); + + public override Task ExecuteScalarAsync(OracleCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + + public override void ExecuteReader(OracleCommand command) + { + using DbDataReader reader = command.ExecuteReader(); + } + + public override void ExecuteReader(OracleCommand command, CommandBehavior behavior) + { + using DbDataReader reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(OracleCommand command) + { + using var reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(OracleCommand command, CommandBehavior behavior) + { + using var reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(OracleCommand command, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(OracleCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using var reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/OracleManagedDataAccessSample/OracleDbCommandFactory.cs b/sample/OracleManagedDataAccessSample/OracleDbCommandFactory.cs new file mode 100644 index 000000000..960727d14 --- /dev/null +++ b/sample/OracleManagedDataAccessSample/OracleDbCommandFactory.cs @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Data; +using Elastic.Apm.AdoNet; + +namespace OracleManagedDataAccessSample +{ + public class OracleDbCommandFactory : DbCommandFactory + { + private int _count = 0; + private readonly string _tableName; + + public OracleDbCommandFactory(IDbConnection connection, string tableName) + : base(connection, tableName) => + _tableName = tableName; + + public override IDbCommand GetCreateTableCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"CREATE TABLE {TableName} (Id number(10) not null, Name varchar2(100) not null)"; + return command; + } + + public override IDbCommand GetInsertRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"INSERT INTO {TableName} (Id, Name) VALUES (:Id, :Name)"; + command.AddParameterWithValue("Id", 1); + command.AddParameterWithValue("Name", "Name1"); + return command; + } + + public override IDbCommand GetSelectScalarCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"SELECT Name FROM {TableName} WHERE Id=:Id"; + command.AddParameterWithValue("Id", 1); + return command; + } + + public override IDbCommand GetUpdateRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"UPDATE {TableName} SET Name=:Name WHERE Id=:Id"; + command.AddParameterWithValue("Name", "Name2"); + command.AddParameterWithValue("Id", 1); + return command; + } + + public override IDbCommand GetSelectRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"SELECT * FROM {TableName} WHERE Id=:Id"; + command.AddParameterWithValue("Id", 1); + return command; + } + + public override IDbCommand GetDeleteRowCommand() + { + var command = Connection.CreateCommand(); + command.CommandText = $"DELETE FROM {TableName} WHERE Id=:Id"; + command.AddParameterWithValue("Id", 1); + TableName = _tableName + _count++; + return command; + } + } +} diff --git a/sample/OracleManagedDataAccessSample/OracleManagedDataAccessSample.csproj b/sample/OracleManagedDataAccessSample/OracleManagedDataAccessSample.csproj new file mode 100644 index 000000000..66c4fabd1 --- /dev/null +++ b/sample/OracleManagedDataAccessSample/OracleManagedDataAccessSample.csproj @@ -0,0 +1,17 @@ + + + + 19.12.0 + Exe + net461 + + + + + + + + + + + \ No newline at end of file diff --git a/sample/OracleManagedDataAccessSample/Program.cs b/sample/OracleManagedDataAccessSample/Program.cs new file mode 100644 index 000000000..29833c465 --- /dev/null +++ b/sample/OracleManagedDataAccessSample/Program.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Data.Common; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet; +using Oracle.ManagedDataAccess.Client; + +namespace OracleManagedDataAccessSample +{ + internal static class Program + { + private static async Task Main(string[] args) + { + var cancellationTokenSource = new CancellationTokenSource(); + var connectionType = typeof(OracleConnection); + var guid = Guid.NewGuid().ToString("N").Substring(0,8); + + Console.WriteLine($"Run commands ({guid})"); + + using (var connection = CreateAndOpenConnection(connectionType)) + { + var dbCommandFactory = new OracleDbCommandFactory(connection, $"oracle_all_{guid}"); + + await DbCommandRunner.RunAllAsync( + dbCommandFactory, + new OracleCommandExecutor(), + cancellationTokenSource.Token); + } + + var oracleAssembly = Assembly.LoadFile(connectionType.Assembly.Location); + var oracleConnection = oracleAssembly.GetType(connectionType.FullName); + + using (var connection = CreateAndOpenConnection(oracleConnection)) + { + var dbCommandFactory = new OracleDbCommandFactory(connection, $"oracle_basetypes_{guid}"); + + await DbCommandRunner.RunBaseTypesAsync( + dbCommandFactory, + cancellationTokenSource.Token); + } + + Console.WriteLine("Finished sending commands"); + + // allow the agent time to send the spans + await Task.Delay(TimeSpan.FromSeconds(40), cancellationTokenSource.Token); + return 0; + } + + private static DbConnection CreateAndOpenConnection(Type connectionType) + { + var connectionString = Environment.GetEnvironmentVariable("ORACLE_CONNECTION_STRING"); + var connection = Activator.CreateInstance(connectionType, connectionString) as DbConnection; + connection.Open(); + return connection; + } + } +} diff --git a/sample/SqlClientSample/Program.cs b/sample/SqlClientSample/Program.cs new file mode 100644 index 000000000..347c8267d --- /dev/null +++ b/sample/SqlClientSample/Program.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet; + +namespace SqlClientSample +{ + internal static class Program + { + private static async Task Main(string[] args) + { + var cancellationTokenSource = new CancellationTokenSource(); + var connectionType = typeof(SqlConnection); + var guid = Guid.NewGuid().ToString("N"); + + Console.WriteLine($"Run commands ({guid})"); + + using (var connection = CreateAndOpenConnection(connectionType)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"sqlserver_all_{guid}"); + + await DbCommandRunner.RunAllAsync( + dbCommandFactory, + new SqlCommandExecutor(), + cancellationTokenSource.Token); + } + + var sqlAssembly = Assembly.LoadFile(connectionType.Assembly.Location); + var sqlConnection = sqlAssembly.GetType(connectionType.FullName); + + using (var connection = CreateAndOpenConnection(sqlConnection)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"sqlserver_basetypes_{guid}"); + + await DbCommandRunner.RunBaseTypesAsync( + dbCommandFactory, + cancellationTokenSource.Token); + } + + Console.WriteLine("Finished sending commands"); + + // allow the agent time to send the spans + await Task.Delay(TimeSpan.FromSeconds(40), cancellationTokenSource.Token); + return 0; + } + + private static DbConnection CreateAndOpenConnection(Type connectionType) + { + var connectionString = Environment.GetEnvironmentVariable("SQLSERVER_CONNECTION_STRING"); + var connection = Activator.CreateInstance(connectionType, connectionString) as DbConnection; + connection.Open(); + return connection; + } + } +} diff --git a/sample/SqlClientSample/SqlClientSample.csproj b/sample/SqlClientSample/SqlClientSample.csproj new file mode 100644 index 000000000..be3c34023 --- /dev/null +++ b/sample/SqlClientSample/SqlClientSample.csproj @@ -0,0 +1,21 @@ + + + + 4.7.0 + Exe + net461;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0; + + + + + + + + + + + + + + + diff --git a/sample/SqlClientSample/SqlCommandExecutor.cs b/sample/SqlClientSample/SqlCommandExecutor.cs new file mode 100644 index 000000000..14fed4872 --- /dev/null +++ b/sample/SqlClientSample/SqlCommandExecutor.cs @@ -0,0 +1,66 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet + +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; + +namespace SqlClientSample +{ + public class SqlCommandExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(SqlCommand); + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(SqlCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(SqlCommand command) => command.ExecuteNonQueryAsync(); + + public override Task ExecuteNonQueryAsync(SqlCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + + public override void ExecuteScalar(SqlCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(SqlCommand command) => command.ExecuteScalarAsync(); + + public override Task ExecuteScalarAsync(SqlCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + + public override void ExecuteReader(SqlCommand command) + { + using DbDataReader reader = command.ExecuteReader(); + } + + public override void ExecuteReader(SqlCommand command, CommandBehavior behavior) + { + using DbDataReader reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(SqlCommand command) + { + using DbDataReader reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(SqlCommand command, CommandBehavior behavior) + { + using DbDataReader reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(SqlCommand command, CancellationToken cancellationToken) + { + using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(SqlCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using DbDataReader reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/SqliteSample/Program.cs b/sample/SqliteSample/Program.cs new file mode 100644 index 000000000..be7cac36d --- /dev/null +++ b/sample/SqliteSample/Program.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Data.Common; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet; +using Microsoft.Data.Sqlite; + +namespace SqliteSample +{ + internal static class Program + { + private static async Task Main(string[] args) + { + var cancellationTokenSource = new CancellationTokenSource(); + var connectionType = typeof(SqliteConnection); + var guid = Guid.NewGuid().ToString("N"); + + Console.WriteLine($"Run commands ({guid})"); + + using (var connection = CreateAndOpenConnection(connectionType)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"test_sqlite_all_{guid}"); + + await DbCommandRunner.RunAllAsync( + dbCommandFactory, + new SqliteCommandExecutor(), + cancellationTokenSource.Token); + } + + var sqliteAssembly = Assembly.LoadFile(connectionType.Assembly.Location); + var sqliteConnection = sqliteAssembly.GetType(connectionType.FullName); + + using (var connection = CreateAndOpenConnection(sqliteConnection)) + { + var dbCommandFactory = new DbCommandFactory(connection, $"test_sqlite_basetypes_{guid}"); + + await DbCommandRunner.RunBaseTypesAsync( + dbCommandFactory, + cancellationTokenSource.Token); + } + + Console.WriteLine("Finished sending commands"); + + // allow the agent time to send the spans + await Task.Delay(TimeSpan.FromSeconds(40), cancellationTokenSource.Token); + return 0; + } + + private static DbConnection CreateAndOpenConnection(Type connectionType) + { + var connectionString = "Data Source=:memory:"; + var connection = Activator.CreateInstance(connectionType, connectionString) as DbConnection; + connection.Open(); + return connection; + } + } +} diff --git a/sample/SqliteSample/SqliteCommandExecutor.cs b/sample/SqliteSample/SqliteCommandExecutor.cs new file mode 100644 index 000000000..3486767be --- /dev/null +++ b/sample/SqliteSample/SqliteCommandExecutor.cs @@ -0,0 +1,65 @@ +// Based on .NET Tracer for Datadog APM by Datadog +// https://github.com/DataDog/dd-trace-dotnet +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.AdoNet.NetStandard; +using Microsoft.Data.Sqlite; + +namespace SqliteSample +{ + public class SqliteCommandExecutor : DbCommandExecutor + { + public override string CommandTypeName => nameof(SqliteCommand); + + public override bool SupportsAsyncMethods => true; + + public override void ExecuteNonQuery(SqliteCommand command) => command.ExecuteNonQuery(); + + public override Task ExecuteNonQueryAsync(SqliteCommand command) => command.ExecuteNonQueryAsync(); + + public override Task ExecuteNonQueryAsync(SqliteCommand command, CancellationToken cancellationToken) => command.ExecuteNonQueryAsync(cancellationToken); + + public override void ExecuteScalar(SqliteCommand command) => command.ExecuteScalar(); + + public override Task ExecuteScalarAsync(SqliteCommand command) => command.ExecuteScalarAsync(); + + public override Task ExecuteScalarAsync(SqliteCommand command, CancellationToken cancellationToken) => command.ExecuteScalarAsync(cancellationToken); + + public override void ExecuteReader(SqliteCommand command) + { + using DbDataReader reader = command.ExecuteReader(); + } + + public override void ExecuteReader(SqliteCommand command, CommandBehavior behavior) + { + using DbDataReader reader = command.ExecuteReader(behavior); + } + + public override async Task ExecuteReaderAsync(SqliteCommand command) + { + using DbDataReader reader = await command.ExecuteReaderAsync(); + } + + public override async Task ExecuteReaderAsync(SqliteCommand command, CommandBehavior behavior) + { + using DbDataReader reader = await command.ExecuteReaderAsync(behavior); + } + + public override async Task ExecuteReaderAsync(SqliteCommand command, CancellationToken cancellationToken) + { + using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); + } + + public override async Task ExecuteReaderAsync(SqliteCommand command, CommandBehavior behavior, CancellationToken cancellationToken) + { + using DbDataReader reader = await command.ExecuteReaderAsync(behavior, cancellationToken); + } + } +} diff --git a/sample/SqliteSample/SqliteSample.csproj b/sample/SqliteSample/SqliteSample.csproj new file mode 100644 index 000000000..236da0c7f --- /dev/null +++ b/sample/SqliteSample/SqliteSample.csproj @@ -0,0 +1,17 @@ + + + + 5.0.8 + Exe + net461;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0; + + + + + + + + + + + diff --git a/src/Elastic.Apm.Profiler.IntegrationsGenerator/CommandLineOptions.cs b/src/Elastic.Apm.Profiler.IntegrationsGenerator/CommandLineOptions.cs new file mode 100644 index 000000000..7ef79db25 --- /dev/null +++ b/src/Elastic.Apm.Profiler.IntegrationsGenerator/CommandLineOptions.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using CommandLine; + +namespace Elastic.Apm.Profiler.IntegrationsGenerator +{ + public class CommandLineOptions + { + [Option('i', "input", Required = true, HelpText = "The input path to the managed assembly containing integrations")] + public string Input { get; set; } + + [Option('o', "output", Required = false, HelpText = "The output directory for the generated integrations file", Default = "")] + public string Output { get; set; } + + [Option('f', "format", Required = false, HelpText = "The output format for the generated integrations file")] + public OutputFormat Format { get; set; } + + public enum OutputFormat + { + Yml, + Asciidoc + } + } +} diff --git a/src/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj new file mode 100644 index 000000000..ea6894435 --- /dev/null +++ b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Elastic.Apm.Profiler.IntegrationsGenerator.csproj @@ -0,0 +1,18 @@ + + + + Exe + net5.0 + false + + + + + + + + + + + + diff --git a/src/Elastic.Apm.Profiler.IntegrationsGenerator/Integration.cs b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Integration.cs new file mode 100644 index 000000000..1472bbd00 --- /dev/null +++ b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Integration.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections.Generic; + +namespace Elastic.Apm.Profiler.IntegrationsGenerator +{ + public class Integration + { + public string Name { get; set; } + + public IEnumerable MethodReplacements { get; set; } + } +} diff --git a/src/Elastic.Apm.Profiler.IntegrationsGenerator/MethodReplacement.cs b/src/Elastic.Apm.Profiler.IntegrationsGenerator/MethodReplacement.cs new file mode 100644 index 000000000..172a96458 --- /dev/null +++ b/src/Elastic.Apm.Profiler.IntegrationsGenerator/MethodReplacement.cs @@ -0,0 +1,13 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Apm.Profiler.IntegrationsGenerator +{ + public class MethodReplacement + { + public Target Target { get; set; } + public Wrapper Wrapper { get; set; } + } +} diff --git a/src/Elastic.Apm.Profiler.IntegrationsGenerator/Program.cs b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Program.cs new file mode 100644 index 000000000..921e4f09b --- /dev/null +++ b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Program.cs @@ -0,0 +1,133 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using CommandLine; +using Elastic.Apm.Profiler.Managed.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Elastic.Apm.Profiler.IntegrationsGenerator +{ + internal class Program + { + private static void Main(string[] args) => + new Parser(cfg => cfg.CaseInsensitiveEnumValues = true) + .ParseArguments(args) + .MapResult( + opts => Run(opts), + errs => 1); + + private static int Run(CommandLineOptions opts) + { + try + { + // assumes the assembly is compatible to load for the TFM of this executable + var targetAssembly = Assembly.LoadFrom(opts.Input); + + var classesInstrumentMethodAttributes = + from wrapperType in targetAssembly.GetTypes() + let attributes = wrapperType.GetCustomAttributes(false) + .OfType() + .Select(a => + { + a.CallTargetType = wrapperType; + return a; + }) + .ToList() + from attribute in attributes + select attribute; + + var callTargetIntegrations = from attribute in classesInstrumentMethodAttributes + let integrationName = attribute.Group + let assembly = attribute.CallTargetType.Assembly + let wrapperType = attribute.CallTargetType + orderby integrationName + group new { assembly, wrapperType, attribute } by integrationName + into g + select new Integration + { + Name = g.Key, + MethodReplacements = from item in g + select new MethodReplacement + { + Target = new Target + { + Assembly = item.attribute.Assembly, + Type = item.attribute.Type, + Method = item.attribute.Method, + SignatureTypes = new[] { item.attribute.ReturnType } + .Concat(item.attribute.ParameterTypes ?? Enumerable.Empty()) + .ToArray(), + MinimumVersion = item.attribute.MinimumVersion, + MaximumVersion = item.attribute.MaximumVersion + }, + Wrapper = new Wrapper + { + Assembly = item.assembly.FullName, + Type = item.wrapperType.FullName, + Action = "CallTargetModification" + } + } + }; + + string output; + switch (opts.Format) + { + case CommandLineOptions.OutputFormat.Yml: + var serializer = new SerializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); + output = serializer.Serialize(callTargetIntegrations); + break; + case CommandLineOptions.OutputFormat.Asciidoc: + output = GenerateAsciidoc(callTargetIntegrations); + break; + default: throw new ArgumentOutOfRangeException("format","Unknown format"); + } + + var filename = Path.Combine(opts.Output, "integrations." + opts.Format.ToString().ToLowerInvariant()); + File.WriteAllText(filename, output, new UTF8Encoding(false)); + return 0; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return 1; + } + } + + private static string GenerateAsciidoc(IEnumerable integrations) + { + var builder = new StringBuilder(); + builder + .AppendLine(":star: *") + .AppendLine() + .AppendLine("|===") + .AppendLine("|Integration |Assembly |Supported assembly version range"); + + foreach (var integration in integrations) + { + foreach (var integrationMethod in + integration.MethodReplacements.GroupBy(m => (m.Target.Assembly, m.Target.MinimumVersion, m.Target.MaximumVersion))) + { + builder.AppendLine($"| {integration.Name}") + .AppendLine($"| {integrationMethod.Key.Assembly}") + .AppendLine($"| {integrationMethod.Key.MinimumVersion.Replace("*", "{star}")} - " + + $"{integrationMethod.Key.MaximumVersion.Replace("*", "{star}")}") + .AppendLine(); + } + } + + builder.AppendLine("|==="); + return builder.ToString(); + } + } +} diff --git a/src/Elastic.Apm.Profiler.IntegrationsGenerator/Target.cs b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Target.cs new file mode 100644 index 000000000..a2565b535 --- /dev/null +++ b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Target.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Apm.Profiler.IntegrationsGenerator +{ + public class Target + { + public string Assembly { get; set; } + public string Type { get; set; } + public string Method { get; set; } + public string[] SignatureTypes { get; set; } + public string MinimumVersion { get; set; } + public string MaximumVersion { get; set; } + } +} diff --git a/src/Elastic.Apm.Profiler.IntegrationsGenerator/Wrapper.cs b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Wrapper.cs new file mode 100644 index 000000000..bb38b087b --- /dev/null +++ b/src/Elastic.Apm.Profiler.IntegrationsGenerator/Wrapper.cs @@ -0,0 +1,14 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Apm.Profiler.IntegrationsGenerator +{ + public class Wrapper + { + public string Assembly { get; set; } + public string Type { get; set; } + public string Action { get; set; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed.Core/ClrTypeNames.cs b/src/Elastic.Apm.Profiler.Managed.Core/ClrTypeNames.cs new file mode 100644 index 000000000..b343f3806 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Core/ClrTypeNames.cs @@ -0,0 +1,49 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Core +{ + public static class ClrTypeNames + { + public const string Ignore = "_"; + + public const string Void = "System.Void"; + public const string Object = "System.Object"; + public const string Bool = "System.Boolean"; + public const string String = "System.String"; + + public const string SByte = "System.SByte"; + public const string Byte = "System.Byte"; + + public const string Int16 = "System.Int16"; + public const string Int32 = "System.Int32"; + public const string Int64 = "System.Int64"; + + public const string UInt16 = "System.UInt16"; + public const string UInt32 = "System.UInt32"; + public const string UInt64 = "System.UInt64"; + + public const string Stream = "System.IO.Stream"; + + public const string Task = "System.Threading.Tasks.Task"; + public const string CancellationToken = "System.Threading.CancellationToken"; + + // ReSharper disable once InconsistentNaming + public const string IAsyncResult = "System.IAsyncResult"; + public const string AsyncCallback = "System.AsyncCallback"; + + public const string HttpRequestMessage = "System.Net.Http.HttpRequestMessage"; + public const string HttpResponseMessage = "System.Net.Http.HttpResponseMessage"; + public const string HttpResponseMessageTask = "System.Threading.Tasks.Task`1"; + + public const string GenericTask = "System.Threading.Tasks.Task`1"; + public const string GenericParameterTask = "System.Threading.Tasks.Task`1"; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed.Core/Elastic.Apm.Profiler.Managed.Core.csproj b/src/Elastic.Apm.Profiler.Managed.Core/Elastic.Apm.Profiler.Managed.Core.csproj new file mode 100644 index 000000000..8eb7b492c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Core/Elastic.Apm.Profiler.Managed.Core.csproj @@ -0,0 +1,8 @@ + + + + net461;netstandard2.0;netcoreapp3.1 + false + + + diff --git a/src/Elastic.Apm.Profiler.Managed.Core/InstrumentAttribute.cs b/src/Elastic.Apm.Profiler.Managed.Core/InstrumentAttribute.cs new file mode 100644 index 000000000..19cbe1370 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Core/InstrumentAttribute.cs @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Elastic.Apm.Profiler.Managed.Core +{ + /// + /// Decorated class instruments a method + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public class InstrumentAttribute : Attribute + { + /// + /// The name of the group to which this instrumentation belongs + /// + public string Group { get; set; } + + /// + /// The name of the assembly containing the target method to instrument + /// + public string Assembly { get; set; } + + /// + /// The fully qualified name of the type containing the target method to instrument + /// + public string Type { get; set; } + + /// + /// The name of the method to instrument + /// + public string Method { get; set; } + + /// + /// The fully qualified name of the return type of the method to instrument + /// + public string ReturnType { get; set; } + + /// + /// The fully qualified names of the parameter types of the method to instrument + /// + public string[] ParameterTypes { get; set; } + + /// + /// The minimum assembly version that can be instrumented + /// + public string MinimumVersion { get; set; } + + /// + /// The maximum assembly version that can be instrumented + /// + public string MaximumVersion { get; set; } + + /// + /// The type to which this instrumentation applies. If null, the type will + /// be determined from the type to which the attribute is applied. + /// + public Type CallTargetType { get; set; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj b/src/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj new file mode 100644 index 000000000..3b082bf60 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Loader/Elastic.Apm.Profiler.Managed.Loader.csproj @@ -0,0 +1,27 @@ + + + + Elastic.Apm.Profiler.Managed.Loader + net461;netcoreapp2.0 + netcoreapp2.0 + false + false + + + + portable + + + + + + + + + + + + diff --git a/src/Elastic.Apm.Profiler.Managed.Loader/Logger.cs b/src/Elastic.Apm.Profiler.Managed.Loader/Logger.cs new file mode 100644 index 000000000..6deab4db8 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Loader/Logger.cs @@ -0,0 +1,138 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Elastic.Apm.Profiler.Managed.Loader +{ + // match the log levels of the profiler logger + internal enum LogLevel + { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Off = 5, + } + + internal static class Logger + { + static Logger() + { + Level = GetLogLevel(LogLevel.Warn); + var logDirectory = GetLogDirectory(); + LogFile = GetLogFile(logDirectory); + Levels = new Dictionary + { + [LogLevel.Off] = "OFF ", + [LogLevel.Error] = "ERROR", + [LogLevel.Warn] = "WARN ", + [LogLevel.Info] = "INFO ", + [LogLevel.Debug] = "DEBUG", + [LogLevel.Trace] = "TRACE", + }; + } + + private static readonly LogLevel Level; + private static readonly string LogFile; + private static readonly Dictionary Levels; + + public static void Log(LogLevel level, Exception exception, string message, params object[] args) + { + if (Level > level) + return; + + Log(level, $"{message}{Environment.NewLine}{exception}", args); + } + + public static void Log(LogLevel level, string message, params object[] args) + { + if (Level > level) + return; + + try + { + if (LogFile != null) + { + try + { + using (var stream = File.Open(LogFile, FileMode.Append, FileAccess.Write, FileShare.Read)) + using (var writer = new StreamWriter(stream, new UTF8Encoding(false))) + { + writer.Write($"[{DateTimeOffset.Now:O}] [{Levels[level]}] "); + writer.WriteLine(message, args); + writer.Flush(); + stream.Flush(true); + } + + return; + } + catch + { + // ignore + } + } + + Console.Error.WriteLine($"[{DateTimeOffset.Now:O}] [{Levels[level]}] {message}", args); + } + catch + { + // ignore + } + } + + private static string GetLogFile(string logDirectory) + { + if (logDirectory is null) + return null; + + var process = Process.GetCurrentProcess(); + return Path.Combine(logDirectory, $"Elastic.Apm.Profiler.Managed.Loader_{process.ProcessName}_{process.Id}.log"); + } + + private static LogLevel GetLogLevel(LogLevel defaultLevel) + { + var level = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG"); + if (string.IsNullOrEmpty(level)) + return defaultLevel; + + return Enum.TryParse(level, true, out var parsedLevel) + ? parsedLevel + : defaultLevel; + } + + private static string GetLogDirectory() + { + try + { + var logDirectory = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG_DIR"); + if (string.IsNullOrEmpty(logDirectory)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + var programData = Environment.GetEnvironmentVariable("PROGRAMDATA"); + logDirectory = !string.IsNullOrEmpty(programData) + ? Path.Combine(programData, "elastic", "apm-agent-dotnet", "logs") + : "."; + } + else + logDirectory = "/var/log/elastic/apm-agent-dotnet"; + } + + Directory.CreateDirectory(logDirectory); + return logDirectory; + } + catch + { + return null; + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed.Loader/Startup.NetCore.cs b/src/Elastic.Apm.Profiler.Managed.Loader/Startup.NetCore.cs new file mode 100644 index 000000000..8c7442394 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Loader/Startup.NetCore.cs @@ -0,0 +1,79 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + + +#if !NETFRAMEWORK +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace Elastic.Apm.Profiler.Managed.Loader +{ + public partial class Startup + { + private static AssemblyLoadContext DependencyLoadContext { get; } = new ProfilerAssemblyLoadContext(); + private static string ResolveDirectory() + { + var version = Environment.Version; + // use netcoreapp3.1 for netcoreapp3.1 and later + var framework = version.Major == 3 && version.Minor >= 1 || version.Major >= 5 + ? "netcoreapp3.1" + : "netstandard2.0"; + + var directory = ReadEnvironmentVariable("ELASTIC_APM_PROFILER_HOME") ?? string.Empty; + return Path.Combine(directory, framework); + } + + private static Assembly ResolveDependencies(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + + // Having a non-US locale can cause mscorlib + // to enter the AssemblyResolve event when searching for resources + // in its satellite assemblies. This seems to have been fixed in + // .NET Core in the 2.0 servicing branch, so we should not see this + // occur, but guard against it anyways. If we do see it, exit early + // so we don't cause infinite recursion. + if (string.Equals(assemblyName.Name, "System.Private.CoreLib.resources", StringComparison.OrdinalIgnoreCase) || + string.Equals(assemblyName.Name, "System.Net.Http", StringComparison.OrdinalIgnoreCase)) + return null; + + var path = Path.Combine(Directory, $"{assemblyName.Name}.dll"); + + // Only load the main Elastic.Apm.Profiler.Managed.dll into the default Assembly Load Context. + // If Elastic.Apm or other libraries are provided by the NuGet package, their loads are handled in the following two ways. + // 1) The AssemblyVersion is greater than or equal to the version used by Elastic.Apm.Profiler.Managed, the assembly + // will load successfully and will not invoke this resolve event. + // 2) The AssemblyVersion is lower than the version used by Elastic.Apm.Profiler.Managed, the assembly will fail to load + // and invoke this resolve event. It must be loaded in a separate AssemblyLoadContext since the application will only + // load the originally referenced version + if (File.Exists(path)) + { + if (assemblyName.Name.StartsWith("Elastic.Apm.Profiler.Managed", StringComparison.OrdinalIgnoreCase) + && assemblyName.FullName.IndexOf("PublicKeyToken=ae7400d2c189cf22", StringComparison.OrdinalIgnoreCase) >= 0) + { + Logger.Log(LogLevel.Debug, "Loading {0} assembly using Assembly.LoadFrom", assemblyName); + return Assembly.LoadFrom(path); + } + + Logger.Log(LogLevel.Debug, "Loading {0} assembly using DependencyLoadContext.LoadFromAssemblyPath", assemblyName); + return DependencyLoadContext.LoadFromAssemblyPath(path); + } + + return null; + } + } + + internal class ProfilerAssemblyLoadContext : AssemblyLoadContext + { + protected override Assembly Load(AssemblyName assemblyName) => null; + } +} +#endif diff --git a/src/Elastic.Apm.Profiler.Managed.Loader/Startup.NetFramework.cs b/src/Elastic.Apm.Profiler.Managed.Loader/Startup.NetFramework.cs new file mode 100644 index 000000000..e1ad19e56 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Loader/Startup.NetFramework.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#if NETFRAMEWORK +using System; +using System.IO; +using System.Reflection; + +namespace Elastic.Apm.Profiler.Managed.Loader +{ + public partial class Startup + { + private static string ResolveDirectory() + { + var framework = "net461"; + var directory = ReadEnvironmentVariable("ELASTIC_APM_PROFILER_HOME") ?? string.Empty; + return Path.Combine(directory, framework); + } + + private static Assembly ResolveDependencies(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name).Name; + + // On .NET Framework, having a non-US locale can cause mscorlib + // to enter the AssemblyResolve event when searching for resources + // in its satellite assemblies. Exit early so we don't cause + // infinite recursion. + if (string.Equals(assemblyName, "mscorlib.resources", StringComparison.OrdinalIgnoreCase) || + string.Equals(assemblyName, "System.Net.Http", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var path = Path.Combine(Directory, $"{assemblyName}.dll"); + if (File.Exists(path)) + { + Logger.Log(LogLevel.Debug, "Loading {0} assembly", assemblyName); + return Assembly.LoadFrom(path); + } + + return null; + } + } +} +#endif diff --git a/src/Elastic.Apm.Profiler.Managed.Loader/Startup.cs b/src/Elastic.Apm.Profiler.Managed.Loader/Startup.cs new file mode 100644 index 000000000..9bef60a4f --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed.Loader/Startup.cs @@ -0,0 +1,68 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection; + +namespace Elastic.Apm.Profiler.Managed.Loader +{ + public partial class Startup + { + static Startup() + { + Directory = ResolveDirectory(); + + try + { + AppDomain.CurrentDomain.AssemblyResolve += ResolveDependencies; + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e, "Error registering AssemblyResolve event handler."); + } + + TryLoadManagedAssembly(); + } + + private static void TryLoadManagedAssembly() + { + try + { + var version = Assembly.GetExecutingAssembly().GetName().Version; + var assembly = Assembly.Load($"Elastic.Apm.Profiler.Managed, Version={version}, Culture=neutral, PublicKeyToken=ae7400d2c189cf22"); + if (assembly != null) + { + var type = assembly.GetType("Elastic.Apm.Profiler.Managed.AutoInstrumentation", throwOnError: false); + var method = type?.GetRuntimeMethod("Initialize", parameters: Type.EmptyTypes); + method?.Invoke(obj: null, parameters: null); + } + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e, "Error loading managed assemblies."); + } + } + + internal static string Directory { get; } + + internal static string ReadEnvironmentVariable(string key) + { + try + { + return Environment.GetEnvironmentVariable(key); + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e, "Error reading environment variable."); + } + + return null; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/AutoInstrumentation.cs b/src/Elastic.Apm.Profiler.Managed/AutoInstrumentation.cs new file mode 100644 index 000000000..b787f4855 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/AutoInstrumentation.cs @@ -0,0 +1,55 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading; + +namespace Elastic.Apm.Profiler.Managed +{ + public static class AutoInstrumentation + { + private static int FirstInitialization = 1; + + public static void Initialize() + { + // check if already called + if (Interlocked.Exchange(ref FirstInitialization, 0) != 1) + return; + + try + { + // ReSharper disable once ReplaceWithSingleAssignment.False + var skipInstantiation = false; + +#if NETFRAMEWORK + // if this is a .NET Framework application running in IIS, don't instantiate the agent here, but let the + // ElasticApmModule do so in its Init(). This assumes that the application: + // either + // 1. references Elastic.Apm.AspNetFullFramework and has configured ElasticApmModule in web.config + // or + // 2. is relying on profiler auto instrumentation to register ElasticApmModule + // + // We allow instantiation to happen in ElasticApmModule because in the case point 1, a user may have + // configured a logging adaptor for the agent running in ASP.NET, which would be ignored if the agent + // was instantiated here. + // ReSharper disable once ConvertIfToOrExpression + if (System.Web.Hosting.HostingEnvironment.IsHosted) + skipInstantiation = true; +#endif + // ensure global instance is created if it's not already + if (!skipInstantiation) + _ = Agent.Instance; + } + catch + { + // ignore + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetInvoker.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetInvoker.cs new file mode 100644 index 000000000..41a29f92d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetInvoker.cs @@ -0,0 +1,309 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.CompilerServices; +using Elastic.Apm.Profiler.Managed.CallTarget.Handlers; + +namespace Elastic.Apm.Profiler.Managed.CallTarget +{ + /// + /// CallTarget Invoker + /// + public static class CallTargetInvoker + { + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// Instance value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Instance value + /// First argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Second argument type + /// Instance value + /// First argument value + /// Second argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1, TArg2 arg2) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1, arg2); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Second argument type + /// Third argument type + /// Instance value + /// First argument value + /// Second argument value + /// Third argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1, arg2, arg3); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Second argument type + /// Third argument type + /// Fourth argument type + /// Instance value + /// First argument value + /// Second argument value + /// Third argument value + /// Fourth argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1, arg2, arg3, arg4); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Second argument type + /// Third argument type + /// Fourth argument type + /// Fifth argument type + /// Instance value + /// First argument value + /// Second argument value + /// Third argument value + /// Fourth argument value + /// Fifth argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1, arg2, arg3, arg4, arg5); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Second argument type + /// Third argument type + /// Fourth argument type + /// Fifth argument type + /// Sixth argument type + /// Instance value + /// First argument value + /// Second argument value + /// Third argument value + /// Fourth argument value + /// Fifth argument value + /// Sixth argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1, arg2, arg3, arg4, arg5, arg6); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Second argument type + /// Third argument type + /// Fourth argument type + /// Fifth argument type + /// Sixth argument type + /// Seventh argument type + /// Instance value + /// First argument value + /// Second argument value + /// Third argument value + /// Fourth argument value + /// Fifth argument value + /// Sixth argument value + /// Seventh argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1, arg2, arg3, arg4, arg5, arg6, arg7); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker + /// + /// Integration type + /// Target type + /// First argument type + /// Second argument type + /// Third argument type + /// Fourth argument type + /// Fifth argument type + /// Sixth argument type + /// Seventh argument type + /// Eighth argument type + /// Instance value + /// First argument value + /// Second argument value + /// Third argument value + /// Fourth argument value + /// Fifth argument value + /// Sixth argument value + /// Seventh argument value + /// Eighth argument value + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodHandler.Invoke(instance, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); + + return CallTargetState.GetDefault(); + } + + /// + /// Begin Method Invoker Slow Path + /// + /// Integration type + /// Target type + /// Instance value + /// Object arguments array + /// Call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState BeginMethod(TTarget instance, object[] arguments) + { + if (IntegrationOptions.IsIntegrationEnabled) + return BeginMethodSlowHandler.Invoke(instance, arguments); + + return CallTargetState.GetDefault(); + } + + /// + /// End Method with Void return value invoker + /// + /// Integration type + /// Target type + /// Instance value + /// Exception value + /// CallTarget state + /// CallTarget return structure + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetReturn EndMethod(TTarget instance, Exception exception, CallTargetState state) + { + if (IntegrationOptions.IsIntegrationEnabled) + return EndMethodHandler.Invoke(instance, exception, state); + + return CallTargetReturn.GetDefault(); + } + + /// + /// End Method with Return value invoker + /// + /// Integration type + /// Target type + /// Return type + /// Instance value + /// Return value + /// Exception value + /// CallTarget state + /// CallTarget return structure + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetReturn EndMethod(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + if (IntegrationOptions.IsIntegrationEnabled) + return EndMethodHandler.Invoke(instance, returnValue, exception, state); + + return new CallTargetReturn(returnValue); + } + + /// + /// Log integration exception + /// + /// Integration type + /// Target type + /// Integration exception instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogException(Exception exception) => IntegrationOptions.LogException(exception); + + /// + /// Gets the default value of a type + /// + /// Type to get the default value + /// Default value of T + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T GetDefaultValue() => default; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetInvokerException.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetInvokerException.cs new file mode 100644 index 000000000..7733337e7 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetInvokerException.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.CallTarget +{ + internal class CallTargetInvokerException : Exception + { + public CallTargetInvokerException(Exception innerException) + : base(innerException.Message, innerException) + { + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetReturn.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetReturn.cs new file mode 100644 index 000000000..3a9d3e62a --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetReturn.cs @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Runtime.CompilerServices; + +namespace Elastic.Apm.Profiler.Managed.CallTarget +{ + /// + /// Call target return value + /// + /// Type of the return value + public readonly struct CallTargetReturn + { + private readonly T _returnValue; + + /// + /// Initializes a new instance of the struct. + /// + /// Return value + public CallTargetReturn(T returnValue) => _returnValue = returnValue; + + /// + /// Gets the default call target return value (used by the native side to initialize the locals) + /// + /// Default call target return value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetReturn GetDefault() => default; + + /// + /// Gets the return value + /// + /// Return value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T GetReturnValue() => _returnValue; + + /// + /// ToString override + /// + /// String value + public override string ToString() => $"{typeof(CallTargetReturn).FullName}({_returnValue})"; + } + + /// + /// Call target return value + /// + public readonly struct CallTargetReturn + { + /// + /// Gets the default call target return value (used by the native side to initialize the locals) + /// + /// Default call target return value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetReturn GetDefault() => default; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetState.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetState.cs new file mode 100644 index 000000000..b485bbf81 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/CallTargetState.cs @@ -0,0 +1,103 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.CompilerServices; +using Elastic.Apm.Api; + +namespace Elastic.Apm.Profiler.Managed.CallTarget +{ + /// + /// Call target execution state + /// + public readonly struct CallTargetState + { + private readonly IExecutionSegment _previousSegment; + private readonly IExecutionSegment _segment; + private readonly object _state; + private readonly DateTimeOffset? _startTime; + + /// + /// Initializes a new instance of the struct. + /// + /// Scope instance + public CallTargetState(IExecutionSegment segment) + { + _previousSegment = null; + _segment = segment; + _state = null; + _startTime = null; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Scope instance + /// Object state instance + public CallTargetState(IExecutionSegment segment, object state) + { + _previousSegment = null; + _segment = segment; + _state = state; + _startTime = null; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Scope instance + /// Object state instance + /// The intended start time of the scope, intended for scopes created in the OnMethodEnd handler + public CallTargetState(IExecutionSegment segment, object state, DateTimeOffset? startTime) + { + _previousSegment = null; + _segment = segment; + _state = state; + _startTime = startTime; + } + + internal CallTargetState(IExecutionSegment previousSegment, CallTargetState state) + { + _previousSegment = previousSegment; + _segment = state._segment; + _state = state._state; + _startTime = state._startTime; + } + + /// + /// Gets the CallTarget BeginMethod segment + /// + public IExecutionSegment Segment => _segment; + + /// + /// Gets the CallTarget BeginMethod state + /// + public object State => _state; + + /// + /// Gets the CallTarget state StartTime + /// + public DateTimeOffset? StartTime => _startTime; + + internal IExecutionSegment PreviousSegment => _previousSegment; + + /// + /// Gets the default call target state (used by the native side to initialize the locals) + /// + /// Default call target state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CallTargetState GetDefault() => default; + + /// + /// ToString override + /// + /// String value + public override string ToString() => $"{typeof(CallTargetState).FullName}({_previousSegment}, {_segment}, {_state})"; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler.cs new file mode 100644 index 000000000..c55b9a029 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler.cs @@ -0,0 +1,48 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using Elastic.Apm.Api; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), Array.Empty()); + if (dynMethod != null) + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + _invokeDelegate = instance => CallTargetState.GetDefault(); + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`1.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`1.cs new file mode 100644 index 000000000..95cbde7fd --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`1.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`2.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`2.cs new file mode 100644 index 000000000..919c1beac --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`2.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1), typeof(TArg2) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1, arg2) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1, TArg2 arg2); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1, TArg2 arg2) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1, arg2)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`3.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`3.cs new file mode 100644 index 000000000..66e466a34 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`3.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1, arg2, arg3) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1, arg2, arg3)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`4.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`4.cs new file mode 100644 index 000000000..fc7fb9de7 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`4.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1, arg2, arg3, arg4) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1, arg2, arg3, arg4)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`5.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`5.cs new file mode 100644 index 000000000..398373eae --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`5.cs @@ -0,0 +1,50 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4), typeof(TArg5) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1, arg2, arg3, arg4, arg5) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1, arg2, arg3, arg4, arg5)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`6.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`6.cs new file mode 100644 index 000000000..ef62fa35e --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`6.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4), typeof(TArg5), typeof(TArg6) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1, arg2, arg3, arg4, arg5, arg6) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1, arg2, arg3, arg4, arg5, arg6)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`7.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`7.cs new file mode 100644 index 000000000..32fe95c4a --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`7.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4), typeof(TArg5), typeof(TArg6), typeof(TArg7) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1, arg2, arg3, arg4, arg5, arg6, arg7) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1, arg2, arg3, arg4, arg5, arg6, arg7)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`8.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`8.cs new file mode 100644 index 000000000..1e9e4ef3e --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodHandler`8.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateBeginMethodDelegate(typeof(TIntegration), typeof(TTarget), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4), typeof(TArg5), typeof(TArg6), typeof(TArg7), typeof(TArg8) }); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodSlowHandler.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodSlowHandler.cs new file mode 100644 index 000000000..aef2dd889 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/BeginMethodSlowHandler.cs @@ -0,0 +1,49 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class BeginMethodSlowHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static BeginMethodSlowHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateSlowBeginMethodDelegate(typeof(TIntegration), typeof(TTarget)); + if (dynMethod != null) + { + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + { + _invokeDelegate = (instance, arguments) => CallTargetState.GetDefault(); + } + } + } + + internal delegate CallTargetState InvokeDelegate(TTarget instance, object[] arguments); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetState Invoke(TTarget instance, object[] arguments) => + new CallTargetState(Agent.IsConfigured ? Agent.Tracer.CurrentExecutionSegment() : null, _invokeDelegate(instance, arguments)); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationGenerator.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationGenerator.cs new file mode 100644 index 000000000..28fbec1d4 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationGenerator.cs @@ -0,0 +1,39 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.CompilerServices; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations +{ + internal class ContinuationGenerator + { + public virtual TReturn SetContinuation(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) => returnValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static TReturn ToTReturn(TFrom returnValue) + { +#if NETCOREAPP3_1 || NET5_0 + return Unsafe.As(ref returnValue); +#else + return ContinuationsHelper.Convert(returnValue); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static TTo FromTReturn(TReturn returnValue) + { +#if NETCOREAPP3_1 || NET5_0 + return Unsafe.As(ref returnValue); +#else + return ContinuationsHelper.Convert(returnValue); +#endif + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationsHelper.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationsHelper.cs new file mode 100644 index 000000000..2a8cfa3a2 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ContinuationsHelper.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations +{ + internal static class ContinuationsHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Type GetResultType(Type parentType) + { + var currentType = parentType; + while (currentType != null) + { + var typeArguments = currentType.GenericTypeArguments ?? Type.EmptyTypes; + switch (typeArguments.Length) + { + case 0: + return typeof(object); + case 1: + return typeArguments[0]; + default: + currentType = currentType.BaseType; + break; + } + } + + return typeof(object); + } + +#if NETCOREAPP3_1 || NET5_0 +#else + internal static TTo Convert(TFrom value) => Converter.Convert(value); + + private static class Converter + { + private static readonly ConvertDelegate _converter; + + static Converter() + { + var dMethod = new DynamicMethod($"Converter<{typeof(TFrom).Name},{typeof(TTo).Name}>", typeof(TTo), new[] { typeof(TFrom) }, typeof(ConvertDelegate).Module, true); + var il = dMethod.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ret); + _converter = (ConvertDelegate)dMethod.CreateDelegate(typeof(ConvertDelegate)); + } + + private delegate TTo ConvertDelegate(TFrom value); + + public static TTo Convert(TFrom value) => _converter(value); + } +#endif + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/NoThrowAwaiter.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/NoThrowAwaiter.cs new file mode 100644 index 000000000..118f9d717 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/NoThrowAwaiter.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations +{ + internal struct NoThrowAwaiter : ICriticalNotifyCompletion + { + private readonly Task _task; + private readonly bool _preserveContext; + + public NoThrowAwaiter(Task task, bool preserveContext) + { + _task = task; + _preserveContext = preserveContext; + } + + public bool IsCompleted => _task.IsCompleted; + + public NoThrowAwaiter GetAwaiter() => this; + + public void GetResult() + { + } + + public void OnCompleted(Action continuation) => _task.ConfigureAwait(_preserveContext).GetAwaiter().OnCompleted(continuation); + + public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/TaskContinuationGenerator.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/TaskContinuationGenerator.cs new file mode 100644 index 000000000..6c86c033c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/TaskContinuationGenerator.cs @@ -0,0 +1,101 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations +{ + internal class TaskContinuationGenerator : ContinuationGenerator + { + private static readonly Func _continuation; + private static readonly bool _preserveContext; + + static TaskContinuationGenerator() + { + var result = IntegrationMapper.CreateAsyncEndMethodDelegate(typeof(TIntegration), typeof(TTarget), typeof(object)); + if (result.Method != null) + { + _continuation = (Func)result.Method.CreateDelegate(typeof(Func)); + _preserveContext = result.PreserveContext; + } + } + + public override TReturn SetContinuation(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + if (_continuation == null) + { + return returnValue; + } + + if (exception != null || returnValue == null) + { + _continuation(instance, default, exception, state); + return returnValue; + } + + Task previousTask = FromTReturn(returnValue); + if (previousTask.Status == TaskStatus.RanToCompletion) + { + _continuation(instance, default, null, state); + return returnValue; + } + + return ToTReturn(ContinuationAction(previousTask, instance, state)); + } + + private static async Task ContinuationAction(Task previousTask, TTarget target, CallTargetState state) + { + if (!previousTask.IsCompleted) + { + await new NoThrowAwaiter(previousTask, _preserveContext); + } + + Exception exception = null; + + if (previousTask.Status == TaskStatus.Faulted) + { + exception = previousTask.Exception.GetBaseException(); + } + else if (previousTask.Status == TaskStatus.Canceled) + { + try + { + // The only supported way to extract the cancellation exception is to await the task + await previousTask.ConfigureAwait(_preserveContext); + } + catch (Exception ex) + { + exception = ex; + } + } + + try + { + // * + // Calls the CallTarget integration continuation, exceptions here should never bubble up to the application + // * + _continuation(target, null, exception, state); + } + catch (Exception ex) + { + IntegrationOptions.LogException(ex, "Exception occurred when calling the CallTarget integration continuation."); + } + + // * + // If the original task throws an exception we rethrow it here. + // * + if (exception != null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/TaskContinuationGenerator`1.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/TaskContinuationGenerator`1.cs new file mode 100644 index 000000000..10da60440 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/TaskContinuationGenerator`1.cs @@ -0,0 +1,103 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations +{ + internal class TaskContinuationGenerator : ContinuationGenerator + { + private static readonly Func _continuation; + private static readonly bool _preserveContext; + + static TaskContinuationGenerator() + { + var result = IntegrationMapper.CreateAsyncEndMethodDelegate(typeof(TIntegration), typeof(TTarget), typeof(TResult)); + if (result.Method != null) + { + _continuation = (Func)result.Method.CreateDelegate(typeof(Func)); + _preserveContext = result.PreserveContext; + } + } + + public override TReturn SetContinuation(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + if (_continuation == null) + { + return returnValue; + } + + if (exception != null || returnValue == null) + { + _continuation(instance, default, exception, state); + return returnValue; + } + + var previousTask = FromTReturn>(returnValue); + + if (previousTask.Status == TaskStatus.RanToCompletion) + { + return ToTReturn(Task.FromResult(_continuation(instance, previousTask.Result, default, state))); + } + + return ToTReturn(ContinuationAction(previousTask, instance, state)); + } + + private static async Task ContinuationAction(Task previousTask, TTarget target, CallTargetState state) + { + if (!previousTask.IsCompleted) + await new NoThrowAwaiter(previousTask, _preserveContext); + + TResult taskResult = default; + Exception exception = null; + TResult continuationResult = default; + + if (previousTask.Status == TaskStatus.RanToCompletion) + taskResult = previousTask.Result; + else if (previousTask.Status == TaskStatus.Faulted) + exception = previousTask.Exception.GetBaseException(); + else if (previousTask.Status == TaskStatus.Canceled) + { + try + { + // The only supported way to extract the cancellation exception is to await the task + await previousTask.ConfigureAwait(_preserveContext); + } + catch (Exception ex) + { + exception = ex; + } + } + + try + { + // * + // Calls the CallTarget integration continuation, exceptions here should never bubble up to the application + // * + continuationResult = _continuation(target, taskResult, exception, state); + } + catch (Exception ex) + { + IntegrationOptions.LogException(ex, "Exception occurred when calling the CallTarget integration continuation."); + } + + // * + // If the original task throws an exception we rethrow it here. + // * + if (exception != null) + ExceptionDispatchInfo.Capture(exception).Throw(); + + return continuationResult; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ValueTaskContinuationGenerator.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ValueTaskContinuationGenerator.cs new file mode 100644 index 000000000..412d59a10 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ValueTaskContinuationGenerator.cs @@ -0,0 +1,87 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Threading.Tasks; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations +{ +#if NETCOREAPP3_1 + internal class ValueTaskContinuationGenerator : ContinuationGenerator + { + private static readonly Func _continuation; + private static readonly bool _preserveContext; + + static ValueTaskContinuationGenerator() + { + var result = IntegrationMapper.CreateAsyncEndMethodDelegate(typeof(TIntegration), typeof(TTarget), typeof(object)); + if (result.Method != null) + { + _continuation = (Func)result.Method.CreateDelegate(typeof(Func)); + _preserveContext = result.PreserveContext; + } + } + + public override TReturn SetContinuation(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + if (_continuation is null) + { + return returnValue; + } + + if (exception != null) + { + _continuation(instance, default, exception, state); + return returnValue; + } + + var previousValueTask = FromTReturn(returnValue); + + return ToTReturn(InnerSetValueTaskContinuation(instance, previousValueTask, state)); + + static async ValueTask InnerSetValueTaskContinuation(TTarget instance, ValueTask previousValueTask, CallTargetState state) + { + try + { + await previousValueTask.ConfigureAwait(_preserveContext); + } + catch (Exception ex) + { + try + { + // * + // Calls the CallTarget integration continuation, exceptions here should never bubble up to the application + // * + _continuation(instance, default, ex, state); + } + catch (Exception contEx) + { + IntegrationOptions.LogException(contEx, "Exception occurred when calling the CallTarget integration continuation."); + } + + throw; + } + + try + { + // * + // Calls the CallTarget integration continuation, exceptions here should never bubble up to the application + // * + _continuation(instance, default, default, state); + } + catch (Exception contEx) + { + IntegrationOptions.LogException(contEx, "Exception occurred when calling the CallTarget integration continuation."); + } + } + } + } +#endif +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ValueTaskContinuationGenerator`1.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ValueTaskContinuationGenerator`1.cs new file mode 100644 index 000000000..a0d089041 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/Continuations/ValueTaskContinuationGenerator`1.cs @@ -0,0 +1,91 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Threading.Tasks; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations +{ +#if NETCOREAPP3_1 + internal class ValueTaskContinuationGenerator : ContinuationGenerator + { + private static readonly Func _continuation; + private static readonly bool _preserveContext; + + static ValueTaskContinuationGenerator() + { + var result = IntegrationMapper.CreateAsyncEndMethodDelegate(typeof(TIntegration), typeof(TTarget), typeof(TResult)); + if (result.Method != null) + { + _continuation = (Func)result.Method.CreateDelegate(typeof(Func)); + _preserveContext = result.PreserveContext; + } + } + + public override TReturn SetContinuation(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + if (_continuation is null) + { + return returnValue; + } + + if (exception != null) + { + _continuation(instance, default, exception, state); + return returnValue; + } + + var previousValueTask = FromTReturn>(returnValue); + return ToTReturn(InnerSetValueTaskContinuation(instance, previousValueTask, state)); + + static async ValueTask InnerSetValueTaskContinuation(TTarget instance, ValueTask previousValueTask, CallTargetState state) + { + TResult result = default; + try + { + result = await previousValueTask.ConfigureAwait(_preserveContext); + } + catch (Exception ex) + { + try + { + // * + // Calls the CallTarget integration continuation, exceptions here should never bubble up to the application + // * + _continuation(instance, result, ex, state); + } + catch (Exception contEx) + { + IntegrationOptions.LogException(contEx, "Exception occurred when calling the CallTarget integration continuation."); + } + + throw; + } + + try + { + // * + // Calls the CallTarget integration continuation, exceptions here should never bubble up to the application + // * + return _continuation(instance, result, null, state); + } + catch (Exception contEx) + { + IntegrationOptions.LogException(contEx, "Exception occurred when calling the CallTarget integration continuation."); + } + + return result; + } + } + } +#endif +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/CreateAsyncEndMethodResult.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/CreateAsyncEndMethodResult.cs new file mode 100644 index 000000000..1bfa1a7e5 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/CreateAsyncEndMethodResult.cs @@ -0,0 +1,26 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal readonly struct CreateAsyncEndMethodResult + { + public readonly DynamicMethod Method; + public readonly bool PreserveContext; + + public CreateAsyncEndMethodResult(DynamicMethod method, bool preserveContext) + { + Method = method; + PreserveContext = preserveContext; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/EndMethodHandler.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/EndMethodHandler.cs new file mode 100644 index 000000000..89006eb99 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/EndMethodHandler.cs @@ -0,0 +1,45 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class EndMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate; + + static EndMethodHandler() + { + try + { + var dynMethod = IntegrationMapper.CreateEndMethodDelegate(typeof(TIntegration), typeof(TTarget)); + if (dynMethod != null) + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + finally + { + if (_invokeDelegate is null) + _invokeDelegate = (instance, exception, state) => CallTargetReturn.GetDefault(); + } + } + + internal delegate CallTargetReturn InvokeDelegate(TTarget instance, Exception exception, CallTargetState state); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetReturn Invoke(TTarget instance, Exception exception, CallTargetState state) => + _invokeDelegate(instance, exception, state); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/EndMethodHandler`1.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/EndMethodHandler`1.cs new file mode 100644 index 000000000..b2c36bcf0 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/EndMethodHandler`1.cs @@ -0,0 +1,107 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class EndMethodHandler + { + private static readonly InvokeDelegate _invokeDelegate = null; + private static readonly ContinuationGenerator _continuationGenerator = null; + + static EndMethodHandler() + { + var returnType = typeof(TReturn); + try + { + var dynMethod = IntegrationMapper.CreateEndMethodDelegate(typeof(TIntegration), typeof(TTarget), returnType); + if (dynMethod != null) + _invokeDelegate = (InvokeDelegate)dynMethod.CreateDelegate(typeof(InvokeDelegate)); + } + catch (Exception ex) + { + throw new CallTargetInvokerException(ex); + } + + if (returnType.IsGenericType) + { + var genericReturnType = returnType.GetGenericTypeDefinition(); + if (typeof(Task).IsAssignableFrom(returnType)) + { + // The type is a Task<> + _continuationGenerator = (ContinuationGenerator)Activator.CreateInstance(typeof(TaskContinuationGenerator<,,,>).MakeGenericType(typeof(TIntegration), typeof(TTarget), returnType, ContinuationsHelper.GetResultType(returnType))); + } +#if NETCOREAPP3_1 + else if (genericReturnType == typeof(ValueTask<>)) + { + // The type is a ValueTask<> + _continuationGenerator = (ContinuationGenerator)Activator.CreateInstance(typeof(ValueTaskContinuationGenerator<,,,>).MakeGenericType(typeof(TIntegration), typeof(TTarget), returnType, ContinuationsHelper.GetResultType(returnType))); + } +#endif + } + else + { + if (returnType == typeof(Task)) + { + // The type is a Task + _continuationGenerator = new TaskContinuationGenerator(); + } +#if NETCOREAPP3_1 + else if (returnType == typeof(ValueTask)) + { + // The type is a ValueTask + _continuationGenerator = new ValueTaskContinuationGenerator(); + } +#endif + } + } + + internal delegate CallTargetReturn InvokeDelegate(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CallTargetReturn Invoke(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + if (_continuationGenerator != null) + { + returnValue = _continuationGenerator.SetContinuation(instance, returnValue, exception, state); + + // Restore previous scope if there is a continuation + // This is used to mimic the ExecutionContext copy from the StateMachine + if (Agent.IsConfigured) + { + switch (state.PreviousSegment) + { + case ITransaction transaction: + Agent.Components.TracerInternal.CurrentExecutionSegmentsContainer.CurrentTransaction = transaction; + break; + case ISpan span: + Agent.Components.TracerInternal.CurrentExecutionSegmentsContainer.CurrentSpan = span; + break; + } + } + } + + if (_invokeDelegate != null) + { + var returnWrap = _invokeDelegate(instance, returnValue, exception, state); + returnValue = returnWrap.GetReturnValue(); + } + + return new CallTargetReturn(returnValue); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationMapper.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationMapper.cs new file mode 100644 index 000000000..1a65ce4c5 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationMapper.cs @@ -0,0 +1,671 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal class IntegrationMapper + { + private const string BeginMethodName = "OnMethodBegin"; + private const string EndMethodName = "OnMethodEnd"; + private const string EndAsyncMethodName = "OnAsyncMethodEnd"; + + private static readonly MethodInfo UnwrapReturnValueMethodInfo = typeof(IntegrationMapper).GetMethod(nameof(IntegrationMapper.UnwrapReturnValue), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo ConvertTypeMethodInfo = typeof(IntegrationMapper).GetMethod(nameof(IntegrationMapper.ConvertType), BindingFlags.NonPublic | BindingFlags.Static); + + internal static DynamicMethod CreateBeginMethodDelegate(Type integrationType, Type targetType, Type[] argumentsTypes) + { + /* + * OnMethodBegin signatures with 1 or more parameters with 1 or more generics: + * - CallTargetState OnMethodBegin(TTarget instance); + * - CallTargetState OnMethodBegin(TTarget instance, TArg1 arg1); + * - CallTargetState OnMethodBegin(TTarget instance, TArg1 arg1, TArg2); + * - CallTargetState OnMethodBegin(TTarget instance, TArg1 arg1, TArg2, ...); + * - CallTargetState OnMethodBegin(); + * - CallTargetState OnMethodBegin(TArg1 arg1); + * - CallTargetState OnMethodBegin(TArg1 arg1, TArg2); + * - CallTargetState OnMethodBegin(TArg1 arg1, TArg2, ...); + * + */ + + Logger.Debug($"Creating BeginMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}]"); + var onMethodBeginMethodInfo = integrationType.GetMethod(BeginMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (onMethodBeginMethodInfo is null) + { + Logger.Debug($"'{BeginMethodName}' method was not found in integration type: '{integrationType.FullName}'."); + return null; + } + + if (onMethodBeginMethodInfo.ReturnType != typeof(CallTargetState)) + throw new ArgumentException($"The return type of the method: {BeginMethodName} in type: {integrationType.FullName} is not {nameof(CallTargetState)}"); + + var genericArgumentsTypes = onMethodBeginMethodInfo.GetGenericArguments(); + if (genericArgumentsTypes.Length < 1) + throw new ArgumentException($"The method: {BeginMethodName} in type: {integrationType.FullName} doesn't have the generic type for the instance type."); + + var onMethodBeginParameters = onMethodBeginMethodInfo.GetParameters(); + if (onMethodBeginParameters.Length < argumentsTypes.Length) + throw new ArgumentException($"The method: {BeginMethodName} with {onMethodBeginParameters.Length} parameters in type: {integrationType.FullName} has less parameters than required."); + else if (onMethodBeginParameters.Length > argumentsTypes.Length + 1) + throw new ArgumentException($"The method: {BeginMethodName} with {onMethodBeginParameters.Length} parameters in type: {integrationType.FullName} has more parameters than required."); + else if (onMethodBeginParameters.Length != argumentsTypes.Length && onMethodBeginParameters[0].ParameterType != genericArgumentsTypes[0]) throw new ArgumentException($"The first generic argument for method: {BeginMethodName} in type: {integrationType.FullName} must be the same as the first parameter for the instance value."); + + var callGenericTypes = new List(); + + var mustLoadInstance = onMethodBeginParameters.Length != argumentsTypes.Length; + var instanceGenericType = genericArgumentsTypes[0]; + var instanceGenericConstraint = instanceGenericType.GetGenericParameterConstraints().FirstOrDefault(); + Type instanceProxyType = null; + if (instanceGenericConstraint != null) + { + var result = DuckType.GetOrCreateProxyType(instanceGenericConstraint, targetType); + instanceProxyType = result.ProxyType; + callGenericTypes.Add(instanceProxyType); + } + else + callGenericTypes.Add(targetType); + + var callMethod = new DynamicMethod( + $"{onMethodBeginMethodInfo.DeclaringType.Name}.{onMethodBeginMethodInfo.Name}", + typeof(CallTargetState), + new Type[] { targetType }.Concat(argumentsTypes).ToArray(), + onMethodBeginMethodInfo.Module, + true); + + var ilWriter = callMethod.GetILGenerator(); + + // Load the instance if is needed + if (mustLoadInstance) + { + ilWriter.Emit(OpCodes.Ldarg_0); + + if (instanceGenericConstraint != null) + WriteCreateNewProxyInstance(ilWriter, instanceProxyType, targetType); + } + + // Load arguments + for (var i = mustLoadInstance ? 1 : 0; i < onMethodBeginParameters.Length; i++) + { + var sourceParameterType = argumentsTypes[mustLoadInstance ? i - 1 : i]; + var targetParameterType = onMethodBeginParameters[i].ParameterType; + Type targetParameterTypeConstraint = null; + Type parameterProxyType = null; + + if (targetParameterType.IsGenericParameter) + { + targetParameterType = genericArgumentsTypes[targetParameterType.GenericParameterPosition]; + targetParameterTypeConstraint = targetParameterType.GetGenericParameterConstraints().FirstOrDefault(pType => pType != typeof(IDuckType)); + if (targetParameterTypeConstraint is null) + callGenericTypes.Add(sourceParameterType); + else + { + var result = DuckType.GetOrCreateProxyType(targetParameterTypeConstraint, sourceParameterType); + parameterProxyType = result.ProxyType; + callGenericTypes.Add(parameterProxyType); + } + } + else if (!targetParameterType.IsAssignableFrom(sourceParameterType) && (!(sourceParameterType.IsEnum && targetParameterType.IsEnum))) + throw new InvalidCastException($"The target parameter {targetParameterType} can't be assigned from {sourceParameterType}"); + + WriteLoadArgument(ilWriter, i, mustLoadInstance); + if (parameterProxyType != null) + { + WriteCreateNewProxyInstance(ilWriter, parameterProxyType, sourceParameterType); + } + } + + // Call method + onMethodBeginMethodInfo = onMethodBeginMethodInfo.MakeGenericMethod(callGenericTypes.ToArray()); + ilWriter.EmitCall(OpCodes.Call, onMethodBeginMethodInfo, null); + ilWriter.Emit(OpCodes.Ret); + + Logger.Debug($"Created BeginMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}]"); + return callMethod; + } + + internal static DynamicMethod CreateSlowBeginMethodDelegate(Type integrationType, Type targetType) + { + /* + * OnMethodBegin signatures with 1 or more parameters with 1 or more generics: + * - CallTargetState OnMethodBegin(TTarget instance); + * - CallTargetState OnMethodBegin(TTarget instance, TArg1 arg1); + * - CallTargetState OnMethodBegin(TTarget instance, TArg1 arg1, TArg2); + * - CallTargetState OnMethodBegin(TTarget instance, TArg1 arg1, TArg2, ...); + * - CallTargetState OnMethodBegin(); + * - CallTargetState OnMethodBegin(TArg1 arg1); + * - CallTargetState OnMethodBegin(TArg1 arg1, TArg2); + * - CallTargetState OnMethodBegin(TArg1 arg1, TArg2, ...); + * + */ + + Logger.Debug($"Creating SlowBeginMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}]"); + var onMethodBeginMethodInfo = integrationType.GetMethod(BeginMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (onMethodBeginMethodInfo is null) + { + Logger.Debug($"'{BeginMethodName}' method was not found in integration type: '{integrationType.FullName}'."); + return null; + } + + if (onMethodBeginMethodInfo.ReturnType != typeof(CallTargetState)) + throw new ArgumentException($"The return type of the method: {BeginMethodName} in type: {integrationType.FullName} is not {nameof(CallTargetState)}"); + + var genericArgumentsTypes = onMethodBeginMethodInfo.GetGenericArguments(); + if (genericArgumentsTypes.Length < 1) + throw new ArgumentException($"The method: {BeginMethodName} in type: {integrationType.FullName} doesn't have the generic type for the instance type."); + + var onMethodBeginParameters = onMethodBeginMethodInfo.GetParameters(); + + var callGenericTypes = new List(); + + var mustLoadInstance = onMethodBeginParameters[0].ParameterType.IsGenericParameter && onMethodBeginParameters[0].ParameterType.GenericParameterPosition == 0; + var instanceGenericType = genericArgumentsTypes[0]; + var instanceGenericConstraint = instanceGenericType.GetGenericParameterConstraints().FirstOrDefault(); + Type instanceProxyType = null; + if (instanceGenericConstraint != null) + { + var result = DuckType.GetOrCreateProxyType(instanceGenericConstraint, targetType); + instanceProxyType = result.ProxyType; + callGenericTypes.Add(instanceProxyType); + } + else + callGenericTypes.Add(targetType); + + var callMethod = new DynamicMethod( + $"{onMethodBeginMethodInfo.DeclaringType.Name}.{onMethodBeginMethodInfo.Name}", + typeof(CallTargetState), + new Type[] { targetType, typeof(object[]) }, + onMethodBeginMethodInfo.Module, + true); + + var ilWriter = callMethod.GetILGenerator(); + + // Load the instance if is needed + if (mustLoadInstance) + { + ilWriter.Emit(OpCodes.Ldarg_0); + + if (instanceGenericConstraint != null) + { + WriteCreateNewProxyInstance(ilWriter, instanceProxyType, targetType); + } + } + + // Load arguments + for (var i = mustLoadInstance ? 1 : 0; i < onMethodBeginParameters.Length; i++) + { + var targetParameterType = onMethodBeginParameters[i].ParameterType; + Type targetParameterTypeConstraint = null; + + if (targetParameterType.IsGenericParameter) + { + targetParameterType = genericArgumentsTypes[targetParameterType.GenericParameterPosition]; + + targetParameterTypeConstraint = targetParameterType.GetGenericParameterConstraints().FirstOrDefault(pType => pType != typeof(IDuckType)); + if (targetParameterTypeConstraint is null) + callGenericTypes.Add(typeof(object)); + else + { + targetParameterType = targetParameterTypeConstraint; + callGenericTypes.Add(targetParameterTypeConstraint); + } + } + + ilWriter.Emit(OpCodes.Ldarg_1); + WriteIntValue(ilWriter, i - (mustLoadInstance ? 1 : 0)); + ilWriter.Emit(OpCodes.Ldelem_Ref); + + if (targetParameterTypeConstraint != null) + ilWriter.EmitCall(OpCodes.Call, ConvertTypeMethodInfo.MakeGenericMethod(targetParameterTypeConstraint), null); + else if (targetParameterType.IsValueType) + ilWriter.Emit(OpCodes.Unbox_Any, targetParameterType); + } + + // Call method + onMethodBeginMethodInfo = onMethodBeginMethodInfo.MakeGenericMethod(callGenericTypes.ToArray()); + ilWriter.EmitCall(OpCodes.Call, onMethodBeginMethodInfo, null); + ilWriter.Emit(OpCodes.Ret); + + Logger.Debug($"Created SlowBeginMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}]"); + return callMethod; + } + + internal static DynamicMethod CreateEndMethodDelegate(Type integrationType, Type targetType) + { + /* + * OnMethodEnd signatures with 2 or 3 parameters with 1 generics: + * - CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state); + * - CallTargetReturn OnMethodEnd(Exception exception, CallTargetState state); + * + */ + + Logger.Debug($"Creating EndMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}]"); + var onMethodEndMethodInfo = integrationType.GetMethod(EndMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (onMethodEndMethodInfo is null) + { + Logger.Debug($"'{EndMethodName}' method was not found in integration type: '{integrationType.FullName}'."); + return null; + } + + if (onMethodEndMethodInfo.ReturnType != typeof(CallTargetReturn)) + throw new ArgumentException($"The return type of the method: {EndMethodName} in type: {integrationType.FullName} is not {nameof(CallTargetReturn)}"); + + var genericArgumentsTypes = onMethodEndMethodInfo.GetGenericArguments(); + if (genericArgumentsTypes.Length != 1) + throw new ArgumentException($"The method: {EndMethodName} in type: {integrationType.FullName} must have a single generic type for the instance type."); + + var onMethodEndParameters = onMethodEndMethodInfo.GetParameters(); + if (onMethodEndParameters.Length < 2) + throw new ArgumentException($"The method: {EndMethodName} with {onMethodEndParameters.Length} parameters in type: {integrationType.FullName} has less parameters than required."); + else if (onMethodEndParameters.Length > 3) throw new ArgumentException($"The method: {EndMethodName} with {onMethodEndParameters.Length} parameters in type: {integrationType.FullName} has more parameters than required."); + + if (onMethodEndParameters[onMethodEndParameters.Length - 2].ParameterType != typeof(Exception)) + throw new ArgumentException($"The Exception type parameter of the method: {EndMethodName} in type: {integrationType.FullName} is missing."); + + if (onMethodEndParameters[onMethodEndParameters.Length - 1].ParameterType != typeof(CallTargetState)) + throw new ArgumentException($"The CallTargetState type parameter of the method: {EndMethodName} in type: {integrationType.FullName} is missing."); + + var callGenericTypes = new List(); + + var mustLoadInstance = onMethodEndParameters.Length == 3; + var instanceGenericType = genericArgumentsTypes[0]; + var instanceGenericConstraint = instanceGenericType.GetGenericParameterConstraints().FirstOrDefault(); + Type instanceProxyType = null; + if (instanceGenericConstraint != null) + { + var result = DuckType.GetOrCreateProxyType(instanceGenericConstraint, targetType); + instanceProxyType = result.ProxyType; + callGenericTypes.Add(instanceProxyType); + } + else + callGenericTypes.Add(targetType); + + var callMethod = new DynamicMethod( + $"{onMethodEndMethodInfo.DeclaringType.Name}.{onMethodEndMethodInfo.Name}", + typeof(CallTargetReturn), + new Type[] { targetType, typeof(Exception), typeof(CallTargetState) }, + onMethodEndMethodInfo.Module, + true); + + var ilWriter = callMethod.GetILGenerator(); + + // Load the instance if is needed + if (mustLoadInstance) + { + ilWriter.Emit(OpCodes.Ldarg_0); + + if (instanceGenericConstraint != null) + WriteCreateNewProxyInstance(ilWriter, instanceProxyType, targetType); + } + + // Load the exception + ilWriter.Emit(OpCodes.Ldarg_1); + + // Load the state + ilWriter.Emit(OpCodes.Ldarg_2); + + // Call Method + onMethodEndMethodInfo = onMethodEndMethodInfo.MakeGenericMethod(callGenericTypes.ToArray()); + ilWriter.EmitCall(OpCodes.Call, onMethodEndMethodInfo, null); + + ilWriter.Emit(OpCodes.Ret); + + Logger.Debug($"Created EndMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}]"); + return callMethod; + } + + internal static DynamicMethod CreateEndMethodDelegate(Type integrationType, Type targetType, Type returnType) + { + /* + * OnMethodEnd signatures with 3 or 4 parameters with 1 or 2 generics: + * - CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state); + * - CallTargetReturn OnMethodEnd(TReturn returnValue, Exception exception, CallTargetState state); + * - CallTargetReturn<[Type]> OnMethodEnd([Type] returnValue, Exception exception, CallTargetState state); + * + */ + + Logger.Debug($"Creating EndMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}, ReturnType={returnType.FullName}]"); + var onMethodEndMethodInfo = integrationType.GetMethod(EndMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (onMethodEndMethodInfo is null) + { + Logger.Debug($"'{EndMethodName}' method was not found in integration type: '{integrationType.FullName}'."); + return null; + } + + if (onMethodEndMethodInfo.ReturnType.GetGenericTypeDefinition() != typeof(CallTargetReturn<>)) + throw new ArgumentException($"The return type of the method: {EndMethodName} in type: {integrationType.FullName} is not {nameof(CallTargetReturn)}"); + + var genericArgumentsTypes = onMethodEndMethodInfo.GetGenericArguments(); + if (genericArgumentsTypes.Length < 1 || genericArgumentsTypes.Length > 2) + throw new ArgumentException($"The method: {EndMethodName} in type: {integrationType.FullName} must have the generic type for the instance type."); + + var onMethodEndParameters = onMethodEndMethodInfo.GetParameters(); + if (onMethodEndParameters.Length < 3) + throw new ArgumentException($"The method: {EndMethodName} with {onMethodEndParameters.Length} parameters in type: {integrationType.FullName} has less parameters than required."); + else if (onMethodEndParameters.Length > 4) + throw new ArgumentException($"The method: {EndMethodName} with {onMethodEndParameters.Length} parameters in type: {integrationType.FullName} has more parameters than required."); + + if (onMethodEndParameters[onMethodEndParameters.Length - 2].ParameterType != typeof(Exception)) + throw new ArgumentException($"The Exception type parameter of the method: {EndMethodName} in type: {integrationType.FullName} is missing."); + + if (onMethodEndParameters[onMethodEndParameters.Length - 1].ParameterType != typeof(CallTargetState)) + throw new ArgumentException($"The CallTargetState type parameter of the method: {EndMethodName} in type: {integrationType.FullName} is missing."); + + var callGenericTypes = new List(); + + var mustLoadInstance = onMethodEndParameters.Length == 4; + var instanceGenericType = genericArgumentsTypes[0]; + var instanceGenericConstraint = instanceGenericType.GetGenericParameterConstraints().FirstOrDefault(); + Type instanceProxyType = null; + if (instanceGenericConstraint != null) + { + var result = DuckType.GetOrCreateProxyType(instanceGenericConstraint, targetType); + instanceProxyType = result.ProxyType; + callGenericTypes.Add(instanceProxyType); + } + else + callGenericTypes.Add(targetType); + + var returnParameterIndex = onMethodEndParameters.Length == 4 ? 1 : 0; + var isAGenericReturnValue = onMethodEndParameters[returnParameterIndex].ParameterType.IsGenericParameter; + Type returnValueGenericType = null; + Type returnValueGenericConstraint = null; + Type returnValueProxyType = null; + if (isAGenericReturnValue) + { + returnValueGenericType = genericArgumentsTypes[1]; + returnValueGenericConstraint = returnValueGenericType.GetGenericParameterConstraints().FirstOrDefault(); + if (returnValueGenericConstraint != null) + { + var result = DuckType.GetOrCreateProxyType(returnValueGenericConstraint, returnType); + returnValueProxyType = result.ProxyType; + callGenericTypes.Add(returnValueProxyType); + } + else + { + callGenericTypes.Add(returnType); + } + } + else if (onMethodEndParameters[returnParameterIndex].ParameterType != returnType) + { + throw new ArgumentException($"The ReturnValue type parameter of the method: {EndMethodName} in type: {integrationType.FullName} is invalid. [{onMethodEndParameters[returnParameterIndex].ParameterType} != {returnType}]"); + } + + var callMethod = new DynamicMethod( + $"{onMethodEndMethodInfo.DeclaringType.Name}.{onMethodEndMethodInfo.Name}.{targetType.Name}.{returnType.Name}", + typeof(CallTargetReturn<>).MakeGenericType(returnType), + new Type[] { targetType, returnType, typeof(Exception), typeof(CallTargetState) }, + onMethodEndMethodInfo.Module, + true); + + var ilWriter = callMethod.GetILGenerator(); + + // Load the instance if is needed + if (mustLoadInstance) + { + ilWriter.Emit(OpCodes.Ldarg_0); + + if (instanceGenericConstraint != null) + { + WriteCreateNewProxyInstance(ilWriter, instanceProxyType, targetType); + } + } + + // Load the return value + ilWriter.Emit(OpCodes.Ldarg_1); + if (returnValueProxyType != null) + { + WriteCreateNewProxyInstance(ilWriter, returnValueProxyType, returnType); + } + + // Load the exception + ilWriter.Emit(OpCodes.Ldarg_2); + + // Load the state + ilWriter.Emit(OpCodes.Ldarg_3); + + // Call Method + onMethodEndMethodInfo = onMethodEndMethodInfo.MakeGenericMethod(callGenericTypes.ToArray()); + ilWriter.EmitCall(OpCodes.Call, onMethodEndMethodInfo, null); + + // Unwrap return value proxy + if (returnValueProxyType != null) + { + var unwrapReturnValue = UnwrapReturnValueMethodInfo.MakeGenericMethod(returnValueProxyType, returnType); + ilWriter.EmitCall(OpCodes.Call, unwrapReturnValue, null); + } + + ilWriter.Emit(OpCodes.Ret); + + Logger.Debug($"Created EndMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}, ReturnType={returnType.FullName}]"); + return callMethod; + } + + internal static CreateAsyncEndMethodResult CreateAsyncEndMethodDelegate(Type integrationType, Type targetType, Type returnType) + { + /* + * OnAsyncMethodEnd signatures with 3 or 4 parameters with 1 or 2 generics: + * - TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state); + * - TReturn OnAsyncMethodEnd(TReturn returnValue, Exception exception, CallTargetState state); + * - [Type] OnAsyncMethodEnd([Type] returnValue, Exception exception, CallTargetState state); + * + * In case the continuation is for a Task/ValueTask, the returnValue type will be an object and the value null. + * In case the continuation is for a Task/ValueTask, the returnValue type will be T with the instance value after the task completes. + * + */ + + Logger.Debug($"Creating AsyncEndMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}, ReturnType={returnType.FullName}]"); + var onAsyncMethodEndMethodInfo = integrationType.GetMethod(EndAsyncMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (onAsyncMethodEndMethodInfo is null) + { + Logger.Debug($"'{EndAsyncMethodName}' method was not found in integration type: '{integrationType.FullName}'."); + return default; + } + + if (!onAsyncMethodEndMethodInfo.ReturnType.IsGenericParameter && onAsyncMethodEndMethodInfo.ReturnType != returnType) + { + throw new ArgumentException($"The return type of the method: {EndAsyncMethodName} in type: {integrationType.FullName} is not {returnType}"); + } + + var genericArgumentsTypes = onAsyncMethodEndMethodInfo.GetGenericArguments(); + if (genericArgumentsTypes.Length < 1 || genericArgumentsTypes.Length > 2) + throw new ArgumentException($"The method: {EndAsyncMethodName} in type: {integrationType.FullName} must have the generic type for the instance type."); + + var onAsyncMethodEndParameters = onAsyncMethodEndMethodInfo.GetParameters(); + if (onAsyncMethodEndParameters.Length < 3) + throw new ArgumentException($"The method: {EndAsyncMethodName} with {onAsyncMethodEndParameters.Length} parameters in type: {integrationType.FullName} has less parameters than required."); + else if (onAsyncMethodEndParameters.Length > 4) + throw new ArgumentException($"The method: {EndAsyncMethodName} with {onAsyncMethodEndParameters.Length} parameters in type: {integrationType.FullName} has more parameters than required."); + + if (onAsyncMethodEndParameters[onAsyncMethodEndParameters.Length - 2].ParameterType != typeof(Exception)) + throw new ArgumentException($"The Exception type parameter of the method: {EndAsyncMethodName} in type: {integrationType.FullName} is missing."); + + if (onAsyncMethodEndParameters[onAsyncMethodEndParameters.Length - 1].ParameterType != typeof(CallTargetState)) + throw new ArgumentException($"The CallTargetState type parameter of the method: {EndAsyncMethodName} in type: {integrationType.FullName} is missing."); + + var preserveContext = onAsyncMethodEndMethodInfo.GetCustomAttribute() != null; + + var callGenericTypes = new List(); + + var mustLoadInstance = onAsyncMethodEndParameters.Length == 4; + var instanceGenericType = genericArgumentsTypes[0]; + var instanceGenericConstraint = instanceGenericType.GetGenericParameterConstraints().FirstOrDefault(); + Type instanceProxyType = null; + if (instanceGenericConstraint != null) + { + var result = DuckType.GetOrCreateProxyType(instanceGenericConstraint, targetType); + instanceProxyType = result.ProxyType; + callGenericTypes.Add(instanceProxyType); + } + else + callGenericTypes.Add(targetType); + + var returnParameterIndex = onAsyncMethodEndParameters.Length == 4 ? 1 : 0; + var isAGenericReturnValue = onAsyncMethodEndParameters[returnParameterIndex].ParameterType.IsGenericParameter; + Type returnValueGenericType = null; + Type returnValueGenericConstraint = null; + Type returnValueProxyType = null; + if (isAGenericReturnValue) + { + returnValueGenericType = genericArgumentsTypes[1]; + returnValueGenericConstraint = returnValueGenericType.GetGenericParameterConstraints().FirstOrDefault(); + if (returnValueGenericConstraint != null) + { + var result = DuckType.GetOrCreateProxyType(returnValueGenericConstraint, returnType); + returnValueProxyType = result.ProxyType; + callGenericTypes.Add(returnValueProxyType); + } + else + callGenericTypes.Add(returnType); + } + else if (onAsyncMethodEndParameters[returnParameterIndex].ParameterType != returnType) + throw new ArgumentException($"The ReturnValue type parameter of the method: {EndAsyncMethodName} in type: {integrationType.FullName} is invalid. [{onAsyncMethodEndParameters[returnParameterIndex].ParameterType} != {returnType}]"); + + var callMethod = new DynamicMethod( + $"{onAsyncMethodEndMethodInfo.DeclaringType.Name}.{onAsyncMethodEndMethodInfo.Name}.{targetType.Name}.{returnType.Name}", + returnType, + new Type[] { targetType, returnType, typeof(Exception), typeof(CallTargetState) }, + onAsyncMethodEndMethodInfo.Module, + true); + + var ilWriter = callMethod.GetILGenerator(); + + // Load the instance if is needed + if (mustLoadInstance) + { + ilWriter.Emit(OpCodes.Ldarg_0); + + if (instanceGenericConstraint != null) + WriteCreateNewProxyInstance(ilWriter, instanceProxyType, targetType); + } + + // Load the return value + ilWriter.Emit(OpCodes.Ldarg_1); + if (returnValueProxyType != null) + WriteCreateNewProxyInstance(ilWriter, returnValueProxyType, returnType); + + // Load the exception + ilWriter.Emit(OpCodes.Ldarg_2); + + // Load the state + ilWriter.Emit(OpCodes.Ldarg_3); + + // Call Method + onAsyncMethodEndMethodInfo = onAsyncMethodEndMethodInfo.MakeGenericMethod(callGenericTypes.ToArray()); + ilWriter.EmitCall(OpCodes.Call, onAsyncMethodEndMethodInfo, null); + + // Unwrap return value proxy + if (returnValueProxyType != null) + { + var unwrapReturnValue = UnwrapReturnValueMethodInfo.MakeGenericMethod(returnValueProxyType, returnType); + ilWriter.EmitCall(OpCodes.Call, unwrapReturnValue, null); + } + + ilWriter.Emit(OpCodes.Ret); + + Logger.Debug($"Created AsyncEndMethod Dynamic Method for '{integrationType.FullName}' integration. [Target={targetType.FullName}, ReturnType={returnType.FullName}]"); + return new CreateAsyncEndMethodResult(callMethod, preserveContext); + } + + private static void WriteCreateNewProxyInstance(ILGenerator ilWriter, Type proxyType, Type targetType) + { + var proxyTypeCtor = proxyType.GetConstructors()[0]; + + if (targetType.IsValueType && !proxyTypeCtor.GetParameters()[0].ParameterType.IsValueType) + ilWriter.Emit(OpCodes.Box, targetType); + + ilWriter.Emit(OpCodes.Newobj, proxyTypeCtor); + } + + private static TTo UnwrapReturnValue(TFrom returnValue) + where TFrom : IDuckType => + (TTo)returnValue.Instance; + + private static void WriteIntValue(ILGenerator il, int value) + { + switch (value) + { + case 0: + il.Emit(OpCodes.Ldc_I4_0); + break; + case 1: + il.Emit(OpCodes.Ldc_I4_1); + break; + case 2: + il.Emit(OpCodes.Ldc_I4_2); + break; + case 3: + il.Emit(OpCodes.Ldc_I4_3); + break; + case 4: + il.Emit(OpCodes.Ldc_I4_4); + break; + case 5: + il.Emit(OpCodes.Ldc_I4_5); + break; + case 6: + il.Emit(OpCodes.Ldc_I4_6); + break; + case 7: + il.Emit(OpCodes.Ldc_I4_7); + break; + case 8: + il.Emit(OpCodes.Ldc_I4_8); + break; + default: + il.Emit(OpCodes.Ldc_I4, value); + break; + } + } + + private static void WriteLoadArgument(ILGenerator il, int index, bool isStatic) + { + if (!isStatic) + index += 1; + + switch (index) + { + case 0: + il.Emit(OpCodes.Ldarg_0); + break; + case 1: + il.Emit(OpCodes.Ldarg_1); + break; + case 2: + il.Emit(OpCodes.Ldarg_2); + break; + case 3: + il.Emit(OpCodes.Ldarg_3); + break; + default: + il.Emit(OpCodes.Ldarg_S, index); + break; + } + } + + private static T ConvertType(object value) + { + var conversionType = typeof(T); + if (value is null || conversionType == typeof(object)) + return (T)value; + + var valueType = value.GetType(); + if (valueType == conversionType || conversionType.IsAssignableFrom(valueType)) + return (T)value; + + // Finally we try to duck type + return DuckType.Create(value); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationOptions.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationOptions.cs new file mode 100644 index 000000000..f1a866c88 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/Handlers/IntegrationOptions.cs @@ -0,0 +1,49 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.CompilerServices; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.CallTarget.Handlers +{ + internal static class IntegrationOptions + { + private static volatile bool _disableIntegration; + + internal static bool IsIntegrationEnabled => !_disableIntegration; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void DisableIntegration() => _disableIntegration = true; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void LogException(Exception exception, string message = null) + { + // ReSharper disable twice ExplicitCallerInfoArgument + Logger.Log(LogLevel.Error, exception, message ?? "exception whilst instrumenting integration <{0}, {1}>", + typeof(TIntegration).FullName, + typeof(TTarget).FullName); + + if (exception is DuckTypeException) + { + Logger.Log(LogLevel.Warn, "DuckTypeException has been detected, the integration <{0}, {1}> will be disabled.", + typeof(TIntegration).FullName, + typeof(TTarget).FullName); + _disableIntegration = true; + } + else if (exception is CallTargetInvokerException) + { + Logger.Log(LogLevel.Warn, "CallTargetInvokerException has been detected, the integration <{0}, {1}> will be disabled.", + typeof(TIntegration).FullName, + typeof(TTarget).FullName); + _disableIntegration = true; + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/CallTarget/PreserveContextAttribute.cs b/src/Elastic.Apm.Profiler.Managed/CallTarget/PreserveContextAttribute.cs new file mode 100644 index 000000000..f414f307e --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/CallTarget/PreserveContextAttribute.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.CallTarget +{ + /// + /// Apply on a calltarget async callback to indicate that the method + /// should execute under the current synchronization context/task scheduler. + /// + [AttributeUsage(AttributeTargets.Method)] + internal class PreserveContextAttribute : Attribute + { + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckAttribute.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckAttribute.cs new file mode 100644 index 000000000..76ceec661 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckAttribute.cs @@ -0,0 +1,73 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck kind + /// + public enum DuckKind + { + /// + /// Property + /// + Property, + + /// + /// Field + /// + Field + } + + /// + /// Duck attribute + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = false)] + public class DuckAttribute : Attribute + { + /// + /// Default BindingFlags + /// + public const BindingFlags DefaultFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy; + + /// + /// Gets or sets the underlying type member name + /// + public string Name { get; set; } + + /// + /// Gets or sets duck kind + /// + public DuckKind Kind { get; set; } = DuckKind.Property; + + /// + /// Gets or sets the binding flags + /// + public BindingFlags BindingFlags { get; set; } = DefaultFlags; + + /// + /// Gets or sets the generic parameter type names definition for a generic method call (required when calling generic methods and instance type is non public) + /// + public string[] GenericParameterTypeNames { get; set; } + + /// + /// Gets or sets the parameter type names of the target method (optional / used to disambiguation) + /// + public string[] ParameterTypeNames { get; set; } + + /// + /// Gets or sets the explicit interface type name + /// + public string ExplicitInterfaceTypeName { get; set; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckCopyAttribute.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckCopyAttribute.cs new file mode 100644 index 000000000..2e0f948b7 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckCopyAttribute.cs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck copy struct attribute + /// + [AttributeUsage(AttributeTargets.Struct, AllowMultiple = false)] + public class DuckCopyAttribute : Attribute + { + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckFieldAttribute.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckFieldAttribute.cs new file mode 100644 index 000000000..88302648d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckFieldAttribute.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck attribute where the underlying member is a field + /// + public class DuckFieldAttribute : DuckAttribute + { + /// + /// Initializes a new instance of the class. + /// + public DuckFieldAttribute() => Kind = DuckKind.Field; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckIgnoreAttribute.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckIgnoreAttribute.cs new file mode 100644 index 000000000..18d125b2c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckIgnoreAttribute.cs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Ignores the member when DuckTyping + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = false)] + public class DuckIgnoreAttribute : Attribute + { + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckIncludeAttribute.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckIncludeAttribute.cs new file mode 100644 index 000000000..dce320691 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckIncludeAttribute.cs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Use to include a member that would normally be ignored + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class DuckIncludeAttribute : Attribute + { + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckReverseMethodAttribute.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckReverseMethodAttribute.cs new file mode 100644 index 000000000..c3a78aac0 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckReverseMethodAttribute.cs @@ -0,0 +1,39 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck reverse method attribute + /// + [AttributeUsage(AttributeTargets.Method)] + public class DuckReverseMethodAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DuckReverseMethodAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Methods arguments + public DuckReverseMethodAttribute(params string[] arguments) => Arguments = arguments; + + /// + /// Gets the methods arguments + /// + public string[] Arguments { get; private set; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Fields.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Fields.cs new file mode 100644 index 000000000..c1d7960e0 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Fields.cs @@ -0,0 +1,213 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck Type + /// + public static partial class DuckType + { + private static MethodBuilder GetFieldGetMethod(TypeBuilder proxyTypeBuilder, Type targetType, MemberInfo proxyMember, FieldInfo targetField, + FieldInfo instanceField + ) + { + var proxyMemberName = proxyMember.Name; + var proxyMemberReturnType = proxyMember is PropertyInfo pinfo ? pinfo.PropertyType : + proxyMember is FieldInfo finfo ? finfo.FieldType : typeof(object); + + var proxyMethod = proxyTypeBuilder.DefineMethod( + "get_" + proxyMemberName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Final | MethodAttributes.HideBySig + | MethodAttributes.Virtual, + proxyMemberReturnType, + Type.EmptyTypes); + + var il = new LazyILGenerator(proxyMethod.GetILGenerator()); + var returnType = targetField.FieldType; + + // Load the instance + if (!targetField.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(instanceField.FieldType.IsValueType ? OpCodes.Ldflda : OpCodes.Ldfld, instanceField); + } + + // Load the field value to the stack + if (UseDirectAccessTo(proxyTypeBuilder, targetType) && targetField.IsPublic) + { + // In case is public is pretty simple + il.Emit(targetField.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, targetField); + } + else + { + // If the instance or the field are non public we need to create a Dynamic method to overpass the visibility checks + // we can't access non public types so we have to cast to object type (in the instance object and the return type if is needed). + var dynMethodName = $"_getNonPublicField_{targetField.DeclaringType.Name}_{targetField.Name}"; + returnType = UseDirectAccessTo(proxyTypeBuilder, targetField.FieldType) ? targetField.FieldType : typeof(object); + + // We create the dynamic method + var dynParameters = targetField.IsStatic ? Type.EmptyTypes : new[] { typeof(object) }; + var dynMethod = new DynamicMethod(dynMethodName, returnType, dynParameters, proxyTypeBuilder.Module, true); + + // Emit the dynamic method body + var dynIL = new LazyILGenerator(dynMethod.GetILGenerator()); + + if (!targetField.IsStatic) + { + // Emit the instance load in the dynamic method + dynIL.Emit(OpCodes.Ldarg_0); + if (targetField.DeclaringType != typeof(object)) + { + dynIL.Emit(targetField.DeclaringType!.IsValueType ? OpCodes.Unbox : OpCodes.Castclass, targetField.DeclaringType); + } + } + + // Emit the field and convert before returning (in case of boxing) + dynIL.Emit(targetField.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, targetField); + dynIL.WriteTypeConversion(targetField.FieldType, returnType); + dynIL.Emit(OpCodes.Ret); + dynIL.Flush(); + + // Emit the call to the dynamic method + il.WriteDynamicMethodCall(dynMethod, proxyTypeBuilder); + } + + // Check if the type can be converted or if we need to enable duck chaining + if (DuckType.NeedsDuckChaining(targetField.FieldType, proxyMemberReturnType)) + { + if (UseDirectAccessTo(proxyTypeBuilder, targetField.FieldType) && targetField.FieldType.IsValueType) + { + il.Emit(OpCodes.Box, targetField.FieldType); + } + + // We call DuckType.CreateCache<>.Create() + var getProxyMethodInfo = typeof(DuckType.CreateCache<>) + .MakeGenericType(proxyMemberReturnType) + .GetMethod("Create"); + + il.Emit(OpCodes.Call, getProxyMethodInfo); + } + else if (returnType != proxyMemberReturnType) + { + // If the type is not the expected type we try a conversion. + il.WriteTypeConversion(returnType, proxyMemberReturnType); + } + + il.Emit(OpCodes.Ret); + il.Flush(); + _methodBuilderGetToken.Invoke(proxyMethod, null); + return proxyMethod; + } + + private static MethodBuilder GetFieldSetMethod(TypeBuilder proxyTypeBuilder, Type targetType, MemberInfo proxyMember, FieldInfo targetField, + FieldInfo instanceField + ) + { + var proxyMemberName = proxyMember.Name; + var proxyMemberReturnType = proxyMember is PropertyInfo pinfo ? pinfo.PropertyType : + proxyMember is FieldInfo finfo ? finfo.FieldType : typeof(object); + + var method = proxyTypeBuilder.DefineMethod( + "set_" + proxyMemberName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Final | MethodAttributes.HideBySig + | MethodAttributes.Virtual, + typeof(void), + new[] { proxyMemberReturnType }); + + var il = new LazyILGenerator(method.GetILGenerator()); + var currentValueType = proxyMemberReturnType; + + // Load instance + if (!targetField.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(instanceField.FieldType.IsValueType ? OpCodes.Ldflda : OpCodes.Ldfld, instanceField); + } + + // Check if the type can be converted of if we need to enable duck chaining + if (DuckType.NeedsDuckChaining(targetField.FieldType, proxyMemberReturnType)) + { + // Load the argument and convert it to Duck type + il.Emit(OpCodes.Ldarg_1); + il.WriteTypeConversion(proxyMemberReturnType, typeof(IDuckType)); + + // Call IDuckType.Instance property to get the actual value + il.EmitCall(OpCodes.Callvirt, DuckTypeInstancePropertyInfo.GetMethod, null); + + currentValueType = typeof(object); + } + else + { + // Load the value into the stack + il.Emit(OpCodes.Ldarg_1); + } + + // We set the field value + if (UseDirectAccessTo(proxyTypeBuilder, targetType) && targetField.IsPublic) + { + // If the instance and the field are public then is easy to set. + il.WriteTypeConversion(currentValueType, targetField.FieldType); + + il.Emit(targetField.IsStatic ? OpCodes.Stsfld : OpCodes.Stfld, targetField); + } + else + { + // If the instance or the field are non public we need to create a Dynamic method to overpass the visibility checks + + var dynMethodName = $"_setField_{targetField.DeclaringType.Name}_{targetField.Name}"; + + // Convert the field type for the dynamic method + var dynValueType = UseDirectAccessTo(proxyTypeBuilder, targetField.FieldType) ? targetField.FieldType : typeof(object); + il.WriteTypeConversion(currentValueType, dynValueType); + + // Create dynamic method + var dynParameters = targetField.IsStatic ? new[] { dynValueType } : new[] { typeof(object), dynValueType }; + var dynMethod = new DynamicMethod(dynMethodName, typeof(void), dynParameters, proxyTypeBuilder.Module, true); + + // Write the dynamic method body + var dynIL = new LazyILGenerator(dynMethod.GetILGenerator()); + dynIL.Emit(OpCodes.Ldarg_0); + + if (targetField.IsStatic) + { + dynIL.WriteTypeConversion(dynValueType, targetField.FieldType); + dynIL.Emit(OpCodes.Stsfld, targetField); + } + else + { + if (targetField.DeclaringType != typeof(object)) + { + dynIL.Emit(OpCodes.Castclass, targetField.DeclaringType); + } + + dynIL.Emit(OpCodes.Ldarg_1); + dynIL.WriteTypeConversion(dynValueType, targetField.FieldType); + dynIL.Emit(OpCodes.Stfld, targetField); + } + + dynIL.Emit(OpCodes.Ret); + dynIL.Flush(); + + // Emit the call to the dynamic method + il.WriteDynamicMethodCall(dynMethod, proxyTypeBuilder); + } + + il.Emit(OpCodes.Ret); + il.Flush(); + _methodBuilderGetToken.Invoke(method, null); + return method; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Methods.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Methods.cs new file mode 100644 index 000000000..7a38179d6 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Methods.cs @@ -0,0 +1,728 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck Type + /// + public static partial class DuckType + { + private static List GetMethods(Type baseType) + { + var selectedMethods = new List(GetBaseMethods(baseType)); + // If the base type is an interface we must make sure we implement all methods, including from other interfaces + if (baseType.IsInterface) + { + var implementedInterfaces = baseType.GetInterfaces(); + foreach (var imInterface in implementedInterfaces) + { + if (imInterface == typeof(IDuckType)) + continue; + + foreach (var interfaceMethod in imInterface.GetMethods()) + { + if (interfaceMethod.IsSpecialName) + continue; + + var interfaceMethodName = interfaceMethod.ToString(); + var methodAlreadySelected = false; + foreach (var currentMethod in selectedMethods) + { + if (currentMethod.ToString() == interfaceMethodName) + { + methodAlreadySelected = true; + break; + } + } + + if (!methodAlreadySelected) + { + var prevMethod = baseType.GetMethod(interfaceMethod.Name, DuckAttribute.DefaultFlags, null, interfaceMethod.GetParameters().Select(p => p.ParameterType).ToArray(), null); + if (prevMethod == null || prevMethod.GetCustomAttribute() is null) + selectedMethods.Add(interfaceMethod); + } + } + } + } + + return selectedMethods; + + static IEnumerable GetBaseMethods(Type baseType) + { + foreach (var method in baseType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + // Avoid proxying object methods like ToString(), GetHashCode() + // or the Finalize() that creates problems by keeping alive the object to another collection. + // You can still proxy those methods if they are defined in an interface, or if you add the DuckInclude attribute. + if (method.DeclaringType == typeof(object)) + { + var include = method.GetCustomAttribute(true) is not null; + + if (!include) + continue; + } + + if (method.IsSpecialName || method.IsFinal || method.IsPrivate) + continue; + + if (baseType.IsInterface || method.IsAbstract || method.IsVirtual) + yield return method; + } + } + } + + private static void CreateMethods(TypeBuilder proxyTypeBuilder, Type proxyType, Type targetType, FieldInfo instanceField) + { + var proxyMethodsDefinitions = GetMethods(proxyType); + + var targetMethodsDefinitions = GetMethods(targetType); + + foreach (var method in targetMethodsDefinitions) + { + if (method.GetCustomAttribute(true) is not null) + proxyMethodsDefinitions.Add(method); + } + + foreach (var proxyMethodDefinition in proxyMethodsDefinitions) + { + // Ignore the method marked with `DuckIgnore` attribute + if (proxyMethodDefinition.GetCustomAttribute(true) is not null) + continue; + + // Extract the method parameters types + var proxyMethodDefinitionParameters = proxyMethodDefinition.GetParameters(); + var proxyMethodDefinitionParametersTypes = proxyMethodDefinitionParameters.Select(p => p.ParameterType).ToArray(); + + // We select the target method to call + var targetMethod = SelectTargetMethod(targetType, proxyMethodDefinition, proxyMethodDefinitionParameters, proxyMethodDefinitionParametersTypes); + + // If the target method couldn't be found and the proxy method doesn't have an implementation already (ex: abstract and virtual classes) we throw. + if (targetMethod is null && proxyMethodDefinition.IsVirtual) + DuckTypeTargetMethodNotFoundException.Throw(proxyMethodDefinition); + + // Check if target method is a reverse method + var isReverse = targetMethod.GetCustomAttribute(true) is not null; + + // Gets the proxy method definition generic arguments + var proxyMethodDefinitionGenericArguments = proxyMethodDefinition.GetGenericArguments(); + var proxyMethodDefinitionGenericArgumentsNames = proxyMethodDefinitionGenericArguments.Select(a => a.Name).ToArray(); + + // Checks if the target method is a generic method while the proxy method is non generic (checks if the Duck attribute contains the generic parameters) + var targetMethodGenericArguments = targetMethod.GetGenericArguments(); + if (proxyMethodDefinitionGenericArguments.Length == 0 && targetMethodGenericArguments.Length > 0) + { + var proxyDuckAttribute = proxyMethodDefinition.GetCustomAttribute(); + if (proxyDuckAttribute is null) + DuckTypeTargetMethodNotFoundException.Throw(proxyMethodDefinition); + + if (proxyDuckAttribute.GenericParameterTypeNames?.Length != targetMethodGenericArguments.Length) + DuckTypeTargetMethodNotFoundException.Throw(proxyMethodDefinition); + + targetMethod = targetMethod.MakeGenericMethod(proxyDuckAttribute.GenericParameterTypeNames.Select(name => Type.GetType(name)).ToArray()); + } + + // Gets target method parameters + var targetMethodParameters = targetMethod.GetParameters(); + var targetMethodParametersTypes = targetMethodParameters.Select(p => p.ParameterType).ToArray(); + + // Make sure we have the right methods attributes. + var proxyMethodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig; + + // Create the proxy method implementation + var proxyMethodParametersBuilders = new ParameterBuilder[proxyMethodDefinitionParameters.Length]; + var proxyMethod = proxyTypeBuilder.DefineMethod(proxyMethodDefinition.Name, proxyMethodAttributes, proxyMethodDefinition.ReturnType, proxyMethodDefinitionParametersTypes); + if (proxyMethodDefinitionGenericArgumentsNames.Length > 0) + _ = proxyMethod.DefineGenericParameters(proxyMethodDefinitionGenericArgumentsNames); + + // Define the proxy method implementation parameters for optional parameters with default values + for (var j = 0; j < proxyMethodDefinitionParameters.Length; j++) + { + var pmDefParameter = proxyMethodDefinitionParameters[j]; + var pmImpParameter = proxyMethod.DefineParameter(j, pmDefParameter.Attributes, pmDefParameter.Name); + if (pmDefParameter.HasDefaultValue) + pmImpParameter.SetConstant(pmDefParameter.RawDefaultValue); + + proxyMethodParametersBuilders[j] = pmImpParameter; + } + + var il = new LazyILGenerator(proxyMethod.GetILGenerator()); + var returnType = targetMethod.ReturnType; + List outputAndRefParameters = null; + + // Load the instance if needed + if (!targetMethod.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(instanceField.FieldType.IsValueType ? OpCodes.Ldflda : OpCodes.Ldfld, instanceField); + } + + // Load all the arguments / parameters + var maxParamLength = Math.Max(proxyMethodDefinitionParameters.Length, targetMethodParameters.Length); + for (var idx = 0; idx < maxParamLength; idx++) + { + var proxyParamInfo = idx < proxyMethodDefinitionParameters.Length ? proxyMethodDefinitionParameters[idx] : null; + var targetParamInfo = targetMethodParameters[idx]; + + if (proxyParamInfo is null) + { + // The proxy method is missing parameters, we check if the target parameter is optional + if (!targetParamInfo.IsOptional) + { + // The target method parameter is not optional. + DuckTypeProxyMethodParameterIsMissingException.Throw(proxyMethodDefinition, targetParamInfo); + } + } + else + { + if (proxyParamInfo.IsOut != targetParamInfo.IsOut || proxyParamInfo.IsIn != targetParamInfo.IsIn) + { + // the proxy and target parameters doesn't have the same signature + DuckTypeProxyAndTargetMethodParameterSignatureMismatchException.Throw(proxyMethodDefinition, targetMethod); + } + + var proxyParamType = proxyParamInfo.ParameterType; + var targetParamType = targetParamInfo.ParameterType; + + if (proxyParamType.IsByRef != targetParamType.IsByRef) + { + // the proxy and target parameters doesn't have the same signature + DuckTypeProxyAndTargetMethodParameterSignatureMismatchException.Throw(proxyMethodDefinition, targetMethod); + } + + // We check if we have to handle an output parameter, by ref parameter or a normal parameter + if (proxyParamInfo.IsOut) + { + // If is an output parameter with diferent types we need to handle differently + // by creating a local var first to store the target parameter out value + // and then try to set the output parameter of the proxy method by converting the value (a base class or a duck typing) + if (proxyParamType != targetParamType) + { + var localTargetArg = il.DeclareLocal(targetParamType.GetElementType()); + + // We need to store the output parameter data to set the proxy parameter value after we call the target method + if (outputAndRefParameters is null) + outputAndRefParameters = new List(); + + outputAndRefParameters.Add(new OutputAndRefParameterData(localTargetArg.LocalIndex, targetParamType, idx, proxyParamType)); + + // Load the local var ref (to be used in the target method param as output) + il.Emit(OpCodes.Ldloca_S, localTargetArg.LocalIndex); + } + else + { + il.WriteLoadArgument(idx, false); + } + } + else if (proxyParamType.IsByRef) + { + // If is a ref parameter with diferent types we need to handle differently + // by creating a local var first to store the initial proxy parameter ref value casted to the target parameter type ( this cast may fail at runtime ) + // later pass this local var ref to the target method, and then, modify the proxy parameter ref with the new reference from the target method + // by converting the value (a base class or a duck typing) + if (proxyParamType != targetParamType) + { + var proxyParamTypeElementType = proxyParamType.GetElementType(); + var targetParamTypeElementType = targetParamType.GetElementType(); + + if (!UseDirectAccessTo(proxyTypeBuilder, targetParamTypeElementType)) + { + targetParamType = typeof(object).MakeByRefType(); + targetParamTypeElementType = typeof(object); + } + + var localTargetArg = il.DeclareLocal(targetParamTypeElementType); + + // We need to store the ref parameter data to set the proxy parameter value after we call the target method + if (outputAndRefParameters is null) + outputAndRefParameters = new List(); + + outputAndRefParameters.Add(new OutputAndRefParameterData(localTargetArg.LocalIndex, targetParamType, idx, proxyParamType)); + + // Load the argument (ref) + il.WriteLoadArgument(idx, false); + + // Load the value inside the ref + il.Emit(OpCodes.Ldind_Ref); + + // Check if the type can be converted of if we need to enable duck chaining + if (NeedsDuckChaining(targetParamTypeElementType, proxyParamTypeElementType)) + { + // First we check if the value is null before trying to get the instance value + var lblCallGetInstance = il.DefineLabel(); + var lblAfterGetInstance = il.DefineLabel(); + + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue_S, lblCallGetInstance); + + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Br_S, lblAfterGetInstance); + + // Call IDuckType.Instance property to get the actual value + il.MarkLabel(lblCallGetInstance); + il.Emit(OpCodes.Castclass, typeof(IDuckType)); + il.EmitCall(OpCodes.Callvirt, DuckTypeInstancePropertyInfo.GetMethod, null); + il.MarkLabel(lblAfterGetInstance); + } + + // Cast the value to the target type + il.WriteSafeTypeConversion(proxyParamTypeElementType, targetParamTypeElementType); + + // Store the casted value to the local var + il.WriteStoreLocal(localTargetArg.LocalIndex); + + // Load the local var ref (to be used in the target method param) + il.Emit(OpCodes.Ldloca_S, localTargetArg.LocalIndex); + } + else + il.WriteLoadArgument(idx, false); + } + else if (!isReverse) + { + // Check if the type can be converted of if we need to enable duck chaining + if (NeedsDuckChaining(targetParamType, proxyParamType)) + { + // Load the argument and cast it as Duck type + il.WriteLoadArgument(idx, false); + il.Emit(OpCodes.Castclass, typeof(IDuckType)); + + // Call IDuckType.Instance property to get the actual value + il.EmitCall(OpCodes.Callvirt, DuckTypeInstancePropertyInfo.GetMethod, null); + } + else + il.WriteLoadArgument(idx, false); + + // If the target parameter type is public or if it's by ref we have to actually use the original target type. + targetParamType = UseDirectAccessTo(proxyTypeBuilder, targetParamType) ? targetParamType : typeof(object); + il.WriteSafeTypeConversion(proxyParamType, targetParamType); + + targetMethodParametersTypes[idx] = targetParamType; + } + else + { + if (NeedsDuckChaining(proxyParamType, targetParamType)) + { + // Load the argument (our proxy type) and cast it as Duck type (the original type) + il.WriteLoadArgument(idx, false); + if (UseDirectAccessTo(proxyTypeBuilder, proxyParamType) && proxyParamType.IsValueType) + il.Emit(OpCodes.Box, proxyParamType); + + // We call DuckType.CreateCache<>.Create(object instance) + var getProxyMethodInfo = typeof(CreateCache<>) + .MakeGenericType(targetParamType).GetMethod("Create"); + + il.Emit(OpCodes.Call, getProxyMethodInfo); + } + else + il.WriteLoadArgument(idx, false); + + // If the target parameter type is public or if it's by ref we have to actually use the original target type. + targetParamType = UseDirectAccessTo(proxyTypeBuilder, targetParamType) ? targetParamType : typeof(object); + il.WriteSafeTypeConversion(proxyParamType, targetParamType); + + targetMethodParametersTypes[idx] = targetParamType; + } + } + } + + // Call the target method + if (UseDirectAccessTo(proxyTypeBuilder, targetType)) + { + // If the instance is public we can emit directly without any dynamic method + + // Create generic method call + if (proxyMethodDefinitionGenericArguments.Length > 0) + { + targetMethod = targetMethod.MakeGenericMethod(proxyMethodDefinitionGenericArguments); + } + + // Method call + // A generic method cannot be called using calli (throws System.InvalidOperationException) + if (targetMethod.IsPublic || targetMethod.IsGenericMethod) + { + // We can emit a normal call if we have a public instance with a public target method. + il.EmitCall(targetMethod.IsStatic || targetMethod.DeclaringType.IsValueType ? OpCodes.Call : OpCodes.Callvirt, targetMethod, null); + } + else + { + // In case we have a public instance and a non public target method we can use [Calli] with the function pointer + il.WriteMethodCalli(targetMethod); + } + } + else + { + // A generic method call can't be made from a DynamicMethod + if (proxyMethodDefinitionGenericArguments.Length > 0) + { + DuckTypeProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesException.Throw(proxyMethod); + } + + // If the instance is not public we need to create a Dynamic method to overpass the visibility checks + // we can't access non public types so we have to cast to object type (in the instance object and the return type). + + var dynMethodName = $"_callMethod_{targetMethod.DeclaringType.Name}_{targetMethod.Name}"; + returnType = UseDirectAccessTo(proxyTypeBuilder, targetMethod.ReturnType) && !targetMethod.ReturnType.IsGenericParameter ? targetMethod.ReturnType : typeof(object); + + // We create the dynamic method + var originalTargetParameters = targetMethod.GetParameters().Select(p => p.ParameterType).ToArray(); + var targetParameters = targetMethod.IsStatic ? originalTargetParameters : (new[] { typeof(object) }).Concat(originalTargetParameters).ToArray(); + var dynParameters = targetMethod.IsStatic ? targetMethodParametersTypes : (new[] { typeof(object) }).Concat(targetMethodParametersTypes).ToArray(); + var dynMethod = new DynamicMethod(dynMethodName, returnType, dynParameters, proxyTypeBuilder.Module, true); + + // Emit the dynamic method body + var dynIL = new LazyILGenerator(dynMethod.GetILGenerator()); + + if (!targetMethod.IsStatic) + dynIL.LoadInstanceArgument(typeof(object), targetMethod.DeclaringType); + + for (var idx = targetMethod.IsStatic ? 0 : 1; idx < dynParameters.Length; idx++) + { + dynIL.WriteLoadArgument(idx, true); + dynIL.WriteSafeTypeConversion(dynParameters[idx], targetParameters[idx]); + } + + // Check if we can emit a normal Call/CallVirt to the target method + if (!targetMethod.ContainsGenericParameters) + dynIL.EmitCall(targetMethod.IsStatic || targetMethod.DeclaringType.IsValueType ? OpCodes.Call : OpCodes.Callvirt, targetMethod, null); + else + { + // We can't emit a call to a method with generics from a DynamicMethod + // Instead we emit a Calli with the function pointer. + dynIL.WriteMethodCalli(targetMethod); + } + + dynIL.WriteSafeTypeConversion(targetMethod.ReturnType, returnType); + dynIL.Emit(OpCodes.Ret); + dynIL.Flush(); + + // Emit the call to the dynamic method + il.WriteDynamicMethodCall(dynMethod, proxyTypeBuilder); + } + + // We check if we have output or ref parameters to set in the proxy method + if (outputAndRefParameters != null) + { + foreach (var outOrRefParameter in outputAndRefParameters) + { + var proxyArgumentType = outOrRefParameter.ProxyArgumentType.GetElementType(); + var localType = outOrRefParameter.LocalType.GetElementType(); + + // We load the argument to be set + il.WriteLoadArgument(outOrRefParameter.ProxyArgumentIndex, false); + + // We load the value from the local + il.WriteLoadLocal(outOrRefParameter.LocalIndex); + + // If we detect duck chaining we create a new proxy instance with the output of the original target method + if (NeedsDuckChaining(localType, proxyArgumentType)) + { + if (localType.IsValueType) + il.Emit(OpCodes.Box, localType); + + // We call DuckType.CreateCache<>.Create() + var getProxyMethodInfo = typeof(CreateCache<>) + .MakeGenericType(proxyArgumentType).GetMethod("Create"); + + il.Emit(OpCodes.Call, getProxyMethodInfo); + } + else + il.WriteSafeTypeConversion(localType, proxyArgumentType); + + // We store the value + il.Emit(OpCodes.Stind_Ref); + } + } + + // Check if the target method returns something + if (targetMethod.ReturnType != typeof(void)) + { + // Handle the return value + // Check if the type can be converted or if we need to enable duck chaining + if (NeedsDuckChaining(targetMethod.ReturnType, proxyMethodDefinition.ReturnType)) + { + if (UseDirectAccessTo(proxyTypeBuilder, targetMethod.ReturnType) && targetMethod.ReturnType.IsValueType) + il.Emit(OpCodes.Box, targetMethod.ReturnType); + + // We call DuckType.CreateCache<>.Create() + var getProxyMethodInfo = typeof(CreateCache<>) + .MakeGenericType(proxyMethodDefinition.ReturnType).GetMethod("Create"); + + il.Emit(OpCodes.Call, getProxyMethodInfo); + } + else if (returnType != proxyMethodDefinition.ReturnType) + { + // If the type is not the expected type we try a conversion. + il.WriteSafeTypeConversion(returnType, proxyMethodDefinition.ReturnType); + } + } + + il.Emit(OpCodes.Ret); + il.Flush(); + _methodBuilderGetToken.Invoke(proxyMethod, null); + } + } + + private static MethodInfo SelectTargetMethod(Type targetType, MethodInfo proxyMethod, ParameterInfo[] proxyMethodParameters, Type[] proxyMethodParametersTypes) + { + var proxyMethodDuckAttribute = proxyMethod.GetCustomAttribute(true) ?? new DuckAttribute(); + proxyMethodDuckAttribute.Name ??= proxyMethod.Name; + + MethodInfo targetMethod = null; + + // Check if the duck attribute has the parameter type names to use for selecting the target method, in case of not found an exception is thrown. + if (proxyMethodDuckAttribute.ParameterTypeNames != null) + { + var parameterTypes = proxyMethodDuckAttribute.ParameterTypeNames.Select(pName => Type.GetType(pName, true)).ToArray(); + targetMethod = targetType.GetMethod(proxyMethodDuckAttribute.Name, proxyMethodDuckAttribute.BindingFlags, null, parameterTypes, null); + if (targetMethod is null) + DuckTypeTargetMethodNotFoundException.Throw(proxyMethod); + + return targetMethod; + } + + // If the duck attribute doesn't specify the parameters to use, we do the best effor to find a target method without any ambiguity. + + // First we try with the current proxy parameter types + targetMethod = targetType.GetMethod(proxyMethodDuckAttribute.Name, proxyMethodDuckAttribute.BindingFlags, null, proxyMethodParametersTypes, null); + if (targetMethod != null) + return targetMethod; + + // If the method wasn't found could be because a DuckType interface is being use in the parameters or in the return value. + // Also this can happen if the proxy parameters type uses a base object (ex: System.Object) instead the type. + // In this case we try to find a method that we can match, in case of ambiguity (> 1 method found) we throw an exception. + + var allTargetMethods = targetType.GetMethods(DuckAttribute.DefaultFlags); + foreach (var candidateMethod in allTargetMethods) + { + var name = proxyMethodDuckAttribute.Name; + var useRelaxedNameComparison = false; + + // If there is an explicit interface type name we add it to the name + if (!string.IsNullOrEmpty(proxyMethodDuckAttribute.ExplicitInterfaceTypeName)) + { + var interfaceTypeName = proxyMethodDuckAttribute.ExplicitInterfaceTypeName; + + if (interfaceTypeName == "*") + { + // If a wildcard is use, then we relax the name comparison so it can be an implicit or explicity implementation + useRelaxedNameComparison = true; + } + else + { + // Nested types are separated with a "." on explicit implementation. + interfaceTypeName = interfaceTypeName.Replace("+", "."); + + name = interfaceTypeName + "." + name; + } + } + + // We omit target methods with different names. + if (candidateMethod.Name != name) + { + if (!useRelaxedNameComparison || !candidateMethod.Name.EndsWith("." + name)) + continue; + } + + // Check if the candidate method is a reverse mapped method + var reverseMethodAttribute = candidateMethod.GetCustomAttribute(true); + if (reverseMethodAttribute?.Arguments is not null) + { + var arguments = reverseMethodAttribute.Arguments; + if (arguments.Length != proxyMethodParametersTypes.Length) + continue; + + var match = true; + for (var i = 0; i < arguments.Length; i++) + { + if (arguments[i] != proxyMethodParametersTypes[i].FullName && arguments[i] != proxyMethodParametersTypes[i].Name) + { + match = false; + break; + } + } + + if (match) + return candidateMethod; + } + + var candidateParameters = candidateMethod.GetParameters(); + + // The proxy must have the same or less parameters than the candidate ( less is due to possible optional parameters in the candidate ). + if (proxyMethodParameters.Length > candidateParameters.Length) + { + continue; + } + + // We compare the target method candidate parameter by parameter. + var skip = false; + for (var i = 0; i < proxyMethodParametersTypes.Length; i++) + { + var proxyParam = proxyMethodParameters[i]; + var candidateParam = candidateParameters[i]; + + var proxyParamType = proxyParam.ParameterType; + var candidateParamType = candidateParam.ParameterType; + + // both needs to have the same parameter direction + if (proxyParam.IsOut != candidateParam.IsOut) + { + skip = true; + break; + } + + // Both need to have the same element type or byref type signature. + if (proxyParamType.IsByRef != candidateParamType.IsByRef) + { + skip = true; + break; + } + + // If the parameters are by ref we unwrap them to have the actual type + proxyParamType = proxyParamType.IsByRef ? proxyParamType.GetElementType() : proxyParamType; + candidateParamType = candidateParamType.IsByRef ? candidateParamType.GetElementType() : candidateParamType; + + // We can't compare generic parameters + if (candidateParamType.IsGenericParameter) + continue; + + // If the proxy parameter type is a value type (no ducktyping neither a base class) both types must match + if (proxyParamType.IsValueType && !proxyParamType.IsEnum && proxyParamType != candidateParamType) + { + skip = true; + break; + } + + // If the proxy parameter is a class and not is an abstract class (only interface and abstract class can be used as ducktype base type) + if (proxyParamType.IsClass && !proxyParamType.IsAbstract && proxyParamType != typeof(object)) + { + if (!candidateParamType.IsAssignableFrom(proxyParamType)) + { + // Check if the parameter type contains generic types before skipping + if (!candidateParamType.IsGenericType || !proxyParamType.IsGenericType) + { + skip = true; + break; + } + + // if the string representation of the generic parameter types is not the same we need to analyze the + // GenericTypeArguments array before skipping it + if (candidateParamType.ToString() != proxyParamType.ToString()) + { + if (candidateParamType.GenericTypeArguments.Length != proxyParamType.GenericTypeArguments.Length) + { + skip = true; + break; + } + + for (var paramIndex = 0; paramIndex < candidateParamType.GenericTypeArguments.Length; paramIndex++) + { + var candidateParamTypeGenericType = candidateParamType.GenericTypeArguments[paramIndex]; + var proxyParamTypeGenericType = proxyParamType.GenericTypeArguments[paramIndex]; + + // Both need to have the same element type or byref type signature. + if (proxyParamTypeGenericType.IsByRef != candidateParamTypeGenericType.IsByRef) + { + skip = true; + break; + } + + // If the parameters are by ref we unwrap them to have the actual type + proxyParamTypeGenericType = proxyParamTypeGenericType.IsByRef ? proxyParamTypeGenericType.GetElementType() : proxyParamTypeGenericType; + candidateParamTypeGenericType = candidateParamTypeGenericType.IsByRef ? candidateParamTypeGenericType.GetElementType() : candidateParamTypeGenericType; + + // We can't compare generic parameters + if (candidateParamTypeGenericType.IsGenericParameter) + continue; + + // If the proxy parameter type is a value type (no ducktyping neither a base class) both types must match + if (proxyParamTypeGenericType.IsValueType && !proxyParamTypeGenericType.IsEnum && proxyParamTypeGenericType != candidateParamTypeGenericType) + { + skip = true; + break; + } + + // If the proxy parameter is a class and not is an abstract class (only interface and abstract class can be used as ducktype base type) + if (proxyParamTypeGenericType.IsClass && !proxyParamTypeGenericType.IsAbstract && proxyParamTypeGenericType != typeof(object)) + { + if (!candidateParamTypeGenericType.IsAssignableFrom(proxyParamTypeGenericType)) + { + skip = true; + break; + } + } + } + + if (skip) + { + break; + } + } + } + } + } + + if (skip) + continue; + + // The target method may have optional parameters with default values so we have to skip those + for (var i = proxyMethodParametersTypes.Length; i < candidateParameters.Length; i++) + { + if (!candidateParameters[i].IsOptional) + { + skip = true; + break; + } + } + + if (skip) + continue; + + if (targetMethod is null) + targetMethod = candidateMethod; + else + DuckTypeTargetMethodAmbiguousMatchException.Throw(proxyMethod, targetMethod, candidateMethod); + } + + return targetMethod; + } + + private static void WriteSafeTypeConversion(this LazyILGenerator il, Type actualType, Type expectedType) + { + // If both types are generics, we expect that the generic parameter are the same type (passthrough) + if (actualType.IsGenericParameter && expectedType.IsGenericParameter) + return; + + il.WriteTypeConversion(actualType, expectedType); + } + + private readonly struct OutputAndRefParameterData + { + public readonly Type LocalType; + public readonly Type ProxyArgumentType; + public readonly int LocalIndex; + public readonly int ProxyArgumentIndex; + + public OutputAndRefParameterData(int localIndex, Type localType, int proxyArgumentIndex, Type proxyArgumentType) + { + LocalIndex = localIndex; + LocalType = localType; + ProxyArgumentIndex = proxyArgumentIndex; + ProxyArgumentType = proxyArgumentType; + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Properties.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Properties.cs new file mode 100644 index 000000000..1060de46d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Properties.cs @@ -0,0 +1,330 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck Type + /// + public static partial class DuckType + { + private static MethodBuilder GetPropertyGetMethod(TypeBuilder proxyTypeBuilder, Type targetType, MemberInfo proxyMember, PropertyInfo targetProperty, FieldInfo instanceField) + { + var proxyMemberName = proxyMember.Name; + var proxyMemberReturnType = typeof(object); + var proxyParameterTypes = Type.EmptyTypes; + var targetParametersTypes = GetPropertyGetParametersTypes(proxyTypeBuilder, targetProperty, true).ToArray(); + + if (proxyMember is PropertyInfo proxyProperty) + { + proxyMemberReturnType = proxyProperty.PropertyType; + proxyParameterTypes = GetPropertyGetParametersTypes(proxyTypeBuilder, proxyProperty, true).ToArray(); + if (proxyParameterTypes.Length != targetParametersTypes.Length) + { + DuckTypePropertyArgumentsLengthException.Throw(proxyProperty); + } + } + else if (proxyMember is FieldInfo proxyField) + { + proxyMemberReturnType = proxyField.FieldType; + proxyParameterTypes = Type.EmptyTypes; + if (proxyParameterTypes.Length != targetParametersTypes.Length) + { + DuckTypePropertyArgumentsLengthException.Throw(targetProperty); + } + } + + var proxyMethod = proxyTypeBuilder.DefineMethod( + "get_" + proxyMemberName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.Virtual, + proxyMemberReturnType, + proxyParameterTypes); + + var il = new LazyILGenerator(proxyMethod.GetILGenerator()); + var targetMethod = targetProperty.GetMethod; + var returnType = targetProperty.PropertyType; + + // Load the instance if needed + if (!targetMethod.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(instanceField.FieldType.IsValueType ? OpCodes.Ldflda : OpCodes.Ldfld, instanceField); + } + + // Load the indexer keys to the stack + for (var pIndex = 0; pIndex < proxyParameterTypes.Length; pIndex++) + { + var proxyParamType = proxyParameterTypes[pIndex]; + var targetParamType = targetParametersTypes[pIndex]; + + // Check if the type can be converted of if we need to enable duck chaining + if (NeedsDuckChaining(targetParamType, proxyParamType)) + { + // Load the argument and cast it as Duck type + il.WriteLoadArgument(pIndex, false); + il.Emit(OpCodes.Castclass, typeof(IDuckType)); + + // Call IDuckType.Instance property to get the actual value + il.EmitCall(OpCodes.Callvirt, DuckTypeInstancePropertyInfo.GetMethod, null); + targetParamType = typeof(object); + } + else + { + il.WriteLoadArgument(pIndex, false); + } + + // If the target parameter type is public or if it's by ref we have to actually use the original target type. + targetParamType = UseDirectAccessTo(proxyTypeBuilder, targetParamType) || targetParamType.IsByRef ? targetParamType : typeof(object); + il.WriteTypeConversion(proxyParamType, targetParamType); + + targetParametersTypes[pIndex] = targetParamType; + } + + // Call the getter method + if (UseDirectAccessTo(proxyTypeBuilder, targetType)) + { + // If the instance is public we can emit directly without any dynamic method + + // Method call + if (targetMethod.IsPublic) + { + // We can emit a normal call if we have a public instance with a public property method. + il.EmitCall(targetMethod.IsStatic || instanceField.FieldType.IsValueType ? OpCodes.Call : OpCodes.Callvirt, targetMethod, null); + } + else + { + // In case we have a public instance and a non public property method we can use [Calli] with the function pointer + il.WriteMethodCalli(targetMethod); + } + } + else + { + // If the instance is not public we need to create a Dynamic method to overpass the visibility checks + // we can't access non public types so we have to cast to object type (in the instance object and the return type). + + var dynMethodName = $"_getNonPublicProperty_{targetProperty.DeclaringType.Name}_{targetProperty.Name}"; + returnType = UseDirectAccessTo(proxyTypeBuilder, targetProperty.PropertyType) ? targetProperty.PropertyType : typeof(object); + + // We create the dynamic method + var targetParameters = GetPropertyGetParametersTypes(proxyTypeBuilder, targetProperty, false, !targetMethod.IsStatic).ToArray(); + var dynParameters = targetMethod.IsStatic ? targetParametersTypes : (new[] { typeof(object) }).Concat(targetParametersTypes).ToArray(); + var dynMethod = new DynamicMethod(dynMethodName, returnType, dynParameters, proxyTypeBuilder.Module, true); + + // Emit the dynamic method body + var dynIL = new LazyILGenerator(dynMethod.GetILGenerator()); + + if (!targetMethod.IsStatic) + { + dynIL.LoadInstanceArgument(typeof(object), targetProperty.DeclaringType); + } + + for (var idx = targetMethod.IsStatic ? 0 : 1; idx < dynParameters.Length; idx++) + { + dynIL.WriteLoadArgument(idx, true); + dynIL.WriteTypeConversion(dynParameters[idx], targetParameters[idx]); + } + + dynIL.EmitCall(targetMethod.IsStatic || instanceField.FieldType.IsValueType ? OpCodes.Call : OpCodes.Callvirt, targetMethod, null); + dynIL.WriteTypeConversion(targetProperty.PropertyType, returnType); + dynIL.Emit(OpCodes.Ret); + dynIL.Flush(); + + // Emit the call to the dynamic method + il.WriteDynamicMethodCall(dynMethod, proxyTypeBuilder); + } + + // Handle the return value + // Check if the type can be converted or if we need to enable duck chaining + if (NeedsDuckChaining(targetProperty.PropertyType, proxyMemberReturnType)) + { + if (UseDirectAccessTo(proxyTypeBuilder, targetProperty.PropertyType) && targetProperty.PropertyType.IsValueType) + { + il.Emit(OpCodes.Box, targetProperty.PropertyType); + } + + // We call DuckType.CreateCache<>.Create() + var getProxyMethodInfo = typeof(CreateCache<>) + .MakeGenericType(proxyMemberReturnType).GetMethod("Create"); + + il.Emit(OpCodes.Call, getProxyMethodInfo); + } + else if (returnType != proxyMemberReturnType) + { + // If the type is not the expected type we try a conversion. + il.WriteTypeConversion(returnType, proxyMemberReturnType); + } + + il.Emit(OpCodes.Ret); + il.Flush(); + _methodBuilderGetToken.Invoke(proxyMethod, null); + return proxyMethod; + } + + private static MethodBuilder GetPropertySetMethod(TypeBuilder proxyTypeBuilder, Type targetType, MemberInfo proxyMember, PropertyInfo targetProperty, FieldInfo instanceField) + { + string proxyMemberName = null; + var proxyParameterTypes = Type.EmptyTypes; + var targetParametersTypes = GetPropertySetParametersTypes(proxyTypeBuilder, targetProperty, true).ToArray(); + + if (proxyMember is PropertyInfo proxyProperty) + { + proxyMemberName = proxyProperty.Name; + proxyParameterTypes = GetPropertySetParametersTypes(proxyTypeBuilder, proxyProperty, true).ToArray(); + if (proxyParameterTypes.Length != targetParametersTypes.Length) + { + DuckTypePropertyArgumentsLengthException.Throw(proxyProperty); + } + } + else if (proxyMember is FieldInfo proxyField) + { + proxyMemberName = proxyField.Name; + proxyParameterTypes = new Type[] { proxyField.FieldType }; + if (proxyParameterTypes.Length != targetParametersTypes.Length) + { + DuckTypePropertyArgumentsLengthException.Throw(targetProperty); + } + } + + var proxyMethod = proxyTypeBuilder.DefineMethod( + "set_" + proxyMemberName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.Virtual, + typeof(void), + proxyParameterTypes); + + var il = new LazyILGenerator(proxyMethod.GetILGenerator()); + var targetMethod = targetProperty.SetMethod; + + // Load the instance if needed + if (!targetMethod.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(instanceField.FieldType.IsValueType ? OpCodes.Ldflda : OpCodes.Ldfld, instanceField); + } + + // Load the indexer keys and set value to the stack + for (var pIndex = 0; pIndex < proxyParameterTypes.Length; pIndex++) + { + var proxyParamType = proxyParameterTypes[pIndex]; + var targetParamType = targetParametersTypes[pIndex]; + + // Check if the type can be converted of if we need to enable duck chaining + if (NeedsDuckChaining(targetParamType, proxyParamType)) + { + // Load the argument and cast it as Duck type + il.WriteLoadArgument(pIndex, false); + il.Emit(OpCodes.Castclass, typeof(IDuckType)); + + // Call IDuckType.Instance property to get the actual value + il.EmitCall(OpCodes.Callvirt, DuckTypeInstancePropertyInfo.GetMethod, null); + + targetParamType = typeof(object); + } + else + il.WriteLoadArgument(pIndex, false); + + // If the target parameter type is public or if it's by ref we have to actually use the original target type. + targetParamType = UseDirectAccessTo(proxyTypeBuilder, targetParamType) || targetParamType.IsByRef ? targetParamType : typeof(object); + il.WriteTypeConversion(proxyParamType, targetParamType); + + targetParametersTypes[pIndex] = targetParamType; + } + + // Call the setter method + if (UseDirectAccessTo(proxyTypeBuilder, targetType)) + { + // If the instance is public we can emit directly without any dynamic method + + if (targetMethod.IsPublic) + { + // We can emit a normal call if we have a public instance with a public property method. + il.EmitCall(targetMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt, targetMethod, null); + } + else + { + // In case we have a public instance and a non public property method we can use [Calli] with the function pointer + il.WriteMethodCalli(targetMethod); + } + } + else + { + // If the instance is not public we need to create a Dynamic method to overpass the visibility checks + // we can't access non public types so we have to cast to object type (in the instance object and the return type). + + var dynMethodName = $"_setNonPublicProperty+{targetProperty.DeclaringType.Name}.{targetProperty.Name}"; + + // We create the dynamic method + var targetParameters = GetPropertySetParametersTypes(proxyTypeBuilder, targetProperty, false, !targetMethod.IsStatic).ToArray(); + var dynParameters = targetMethod.IsStatic ? targetParametersTypes : (new[] { typeof(object) }).Concat(targetParametersTypes).ToArray(); + var dynMethod = new DynamicMethod(dynMethodName, typeof(void), dynParameters, proxyTypeBuilder.Module, true); + + // Emit the dynamic method body + var dynIL = new LazyILGenerator(dynMethod.GetILGenerator()); + + if (!targetMethod.IsStatic) + { + dynIL.LoadInstanceArgument(typeof(object), targetProperty.DeclaringType); + } + + for (var idx = targetMethod.IsStatic ? 0 : 1; idx < dynParameters.Length; idx++) + { + dynIL.WriteLoadArgument(idx, true); + dynIL.WriteTypeConversion(dynParameters[idx], targetParameters[idx]); + } + + dynIL.EmitCall(targetMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt, targetMethod, null); + dynIL.Emit(OpCodes.Ret); + dynIL.Flush(); + + // Create and load delegate for the DynamicMethod + il.WriteDynamicMethodCall(dynMethod, proxyTypeBuilder); + } + + il.Emit(OpCodes.Ret); + il.Flush(); + _methodBuilderGetToken.Invoke(proxyMethod, null); + return proxyMethod; + } + + private static IEnumerable GetPropertyGetParametersTypes(TypeBuilder typeBuilder, PropertyInfo property, bool originalTypes, bool isDynamicSignature = false) + { + if (isDynamicSignature) + yield return typeof(object); + + var idxParams = property.GetIndexParameters(); + foreach (var parameter in idxParams) + { + if (originalTypes || UseDirectAccessTo(typeBuilder, parameter.ParameterType)) + yield return parameter.ParameterType; + else + yield return typeof(object); + } + } + + private static IEnumerable GetPropertySetParametersTypes(TypeBuilder typeBuilder, PropertyInfo property, bool originalTypes, bool isDynamicSignature = false) + { + if (isDynamicSignature) + yield return typeof(object); + + foreach (var indexType in GetPropertyGetParametersTypes(typeBuilder, property, originalTypes)) + yield return indexType; + + if (originalTypes || UseDirectAccessTo(typeBuilder, property.PropertyType)) + yield return property.PropertyType; + else + yield return typeof(object); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs new file mode 100644 index 000000000..fe2201741 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Statics.cs @@ -0,0 +1,133 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck Type + /// + public static partial class DuckType + { + /// + /// Gets the Type.GetTypeFromHandle method info + /// + public static readonly MethodInfo GetTypeFromHandleMethodInfo = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)); + + /// + /// Gets the Enum.ToObject method info + /// + public static readonly MethodInfo EnumToObjectMethodInfo = typeof(Enum).GetMethod(nameof(Enum.ToObject), new[] { typeof(Type), typeof(object) }); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly object _locker = new object(); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly ConcurrentDictionary> DuckTypeCache = new ConcurrentDictionary>(); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly PropertyInfo DuckTypeInstancePropertyInfo = typeof(IDuckType).GetProperty(nameof(IDuckType.Instance)); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly MethodInfo _methodBuilderGetToken = typeof(MethodBuilder).GetMethod("GetToken", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly Dictionary ActiveBuilders = new Dictionary(); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static long _assemblyCount = 0; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static long _typeCount = 0; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static ConstructorInfo _ignoresAccessChecksToAttributeCtor = typeof(IgnoresAccessChecksToAttribute).GetConstructor(new Type[] { typeof(string) }); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static Dictionary> _ignoresAccessChecksToAssembliesSetDictionary = new Dictionary>(); + + internal static long AssemblyCount => _assemblyCount; + + internal static long TypeCount => _typeCount; + + /// + /// Gets the ModuleBuilder instance from a target type. (.NET Framework / Non AssemblyLoadContext version) + /// + /// Target type for ducktyping + /// Is visible boolean + /// ModuleBuilder instance + private static ModuleBuilder GetModuleBuilder(Type targetType, bool isVisible) + { + var targetAssembly = targetType.Assembly ?? typeof(DuckType).Assembly; + + if (!isVisible) + { + // If the target type is not visible then we create a new module builder. + // This is the only way to IgnoresAccessChecksToAttribute to work. + // We can't reuse the module builder if the attributes collection changes. + return CreateModuleBuilder($"DuckTypeNotVisibleAssembly.{targetType.Name}", targetAssembly); + } + + if (targetType.IsGenericType) + { + foreach (var type in targetType.GetGenericArguments()) + { + if (type.Assembly != targetAssembly) + { + return CreateModuleBuilder($"DuckTypeGenericTypeAssembly.{targetType.Name}", targetAssembly); + } + } + } + + if (!ActiveBuilders.TryGetValue(targetAssembly, out var moduleBuilder)) + { + moduleBuilder = CreateModuleBuilder($"DuckTypeAssembly.{targetType.Assembly?.GetName().Name}", targetAssembly); + ActiveBuilders.Add(targetAssembly, moduleBuilder); + } + + return moduleBuilder; + + static ModuleBuilder CreateModuleBuilder(string name, Assembly targetAssembly) + { + var assemblyName = new AssemblyName(name + $"_{++_assemblyCount}"); + assemblyName.Version = targetAssembly.GetName().Version; + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + return assemblyBuilder.DefineDynamicModule("MainModule"); + } + } + + /// + /// DynamicMethods delegates cache + /// + /// Proxy delegate type + public static class DelegateCache + where TProxyDelegate : Delegate + { + private static TProxyDelegate _delegate; + + /// + /// Get cached delegate from the DynamicMethod + /// + /// TProxyDelegate instance + public static TProxyDelegate GetDelegate() => _delegate; + + /// + /// Create delegate from a DynamicMethod index + /// + /// Dynamic method index + internal static void FillDelegate(int index) => + _delegate = (TProxyDelegate)ILHelpersExtensions.GetDynamicMethodForIndex(index) + .CreateDelegate(typeof(TProxyDelegate)); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Utilities.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Utilities.cs new file mode 100644 index 000000000..d778f0c61 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.Utilities.cs @@ -0,0 +1,139 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck Type + /// + public static partial class DuckType + { + /// + /// Checks and ensures the arguments for the Create methods + /// + /// Duck type + /// Instance value + /// If the duck type or the instance value is null + private static void EnsureArguments(Type proxyType, object instance) + { + if (proxyType is null) + { + DuckTypeProxyTypeDefinitionIsNull.Throw(); + } + + if (instance is null) + { + DuckTypeTargetObjectInstanceIsNull.Throw(); + } + +#if NET45 + if (!proxyType.IsPublic && !proxyType.IsNestedPublic) + { + DuckTypeTypeIsNotPublicException.Throw(proxyType, nameof(proxyType)); + } +#endif + } + + /// + /// Ensures the visibility access to the type + /// + /// Module builder + /// Type to gain internals visibility + private static void EnsureTypeVisibility(ModuleBuilder builder, Type type) + { + EnsureAssemblyNameVisibility(builder, type.Assembly.GetName().Name); + + if (type.IsGenericType && !type.IsGenericTypeDefinition) + { + foreach (var t in type.GetGenericArguments()) + { + if (!t.IsVisible) + EnsureAssemblyNameVisibility(builder, t.Assembly.GetName().Name); + } + } + + while (type.IsNested) + { + if (!type.IsNestedPublic) + EnsureAssemblyNameVisibility(builder, type.Assembly.GetName().Name); + + // this should be null for non-nested types. + type = type.DeclaringType; + } + + static void EnsureAssemblyNameVisibility(ModuleBuilder builder, string assemblyName) + { + lock (_ignoresAccessChecksToAssembliesSetDictionary) + { + if (!_ignoresAccessChecksToAssembliesSetDictionary.TryGetValue(builder, out var hashSet)) + { + hashSet = new HashSet(); + _ignoresAccessChecksToAssembliesSetDictionary[builder] = hashSet; + } + + if (hashSet.Add(assemblyName)) + { + ((AssemblyBuilder)builder.Assembly).SetCustomAttribute(new CustomAttributeBuilder(_ignoresAccessChecksToAttributeCtor, new object[] { assemblyName })); + } + } + } + } + + private static bool NeedsDuckChaining(Type targetType, Type proxyType) => + // The condition to apply duck chaining is: + // 1. Is a struct with the DuckCopy attribute + // 2. Both types must be differents. + // 3. The proxy type (duck chaining proxy definition type) can't be a struct + // 4. The proxy type can't be a generic parameter (should be a well known type) + // 5. Can't be a base type or an iterface implemented by the targetType type. + // 6. The proxy type can't be a CLR type + proxyType.GetCustomAttribute() != null || + (proxyType != targetType && + !proxyType.IsValueType && + !proxyType.IsGenericParameter && + !proxyType.IsAssignableFrom(targetType) && + proxyType.Module != typeof(string).Module); + + /// + /// Gets if the direct access method should be used or the inderect method (dynamic method) + /// + /// Module builder + /// Target type + /// true for direct method; otherwise, false. + private static bool UseDirectAccessTo(ModuleBuilder builder, Type targetType) + { + if (builder == null) + return targetType.IsPublic || targetType.IsNestedPublic; + + EnsureTypeVisibility(builder, targetType); + return true; + } + + /// + /// Gets if the direct access method should be used or the inderect method (dynamic method) + /// + /// Type builder + /// Target type + /// true for direct method; otherwise, false. + private static bool UseDirectAccessTo(TypeBuilder builder, Type targetType) => UseDirectAccessTo((ModuleBuilder)builder?.Module, targetType); + + /// + /// Gets if the direct access method should be used or the inderect method (dynamic method) + /// + /// Target type + /// true for direct method; otherwise, false. + private static bool UseDirectAccessTo(Type targetType) => UseDirectAccessTo((ModuleBuilder)null, targetType); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.cs new file mode 100644 index 000000000..a3f9c6254 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckType.cs @@ -0,0 +1,733 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Create struct proxy instance delegate + /// + /// Type of struct + /// Object instance + /// Proxy instance + public delegate T CreateProxyInstance(object instance); + + /// + /// Duck Type + /// + public static partial class DuckType + { + /// + /// Create duck type proxy using a base type + /// + /// Instance object + /// Duck type + /// Duck type proxy + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Create(object instance) => CreateCache.Create(instance); + + /// + /// Create duck type proxy using a base type + /// + /// Duck type + /// Instance object + /// Duck Type proxy + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IDuckType Create(Type proxyType, object instance) + { + // Validate arguments + EnsureArguments(proxyType, instance); + + // Create Type + var result = GetOrCreateProxyType(proxyType, instance.GetType()); + + // Create instance + return result.CreateInstance(instance); + } + + /// + /// Gets if a proxy can be created + /// + /// Instance object + /// Duck type + /// true if the proxy can be created; otherwise, false + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanCreate(object instance) => CreateCache.CanCreate(instance); + + /// + /// Gets if a proxy can be created + /// + /// Duck type + /// Instance object + /// true if the proxy can be created; otherwise, false + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanCreate(Type proxyType, object instance) + { + // Validate arguments + EnsureArguments(proxyType, instance); + + // Create Type + var result = GetOrCreateProxyType(proxyType, instance.GetType()); + + // Create instance + return result.CanCreate(); + } + + /// + /// Gets or create a new proxy type for ducktyping + /// + /// ProxyType interface + /// Target type + /// CreateTypeResult instance + public static CreateTypeResult GetOrCreateProxyType(Type proxyType, Type targetType) => + DuckTypeCache.GetOrAdd( + new TypesTuple(proxyType, targetType), + key => new Lazy(() => CreateProxyType(key.ProxyDefinitionType, key.TargetType))) + .Value; + + private static CreateTypeResult CreateProxyType(Type proxyDefinitionType, Type targetType) + { + lock (_locker) + { + try + { + // Define parent type, interface types + Type parentType; + TypeAttributes typeAttributes; + Type[] interfaceTypes; + if (proxyDefinitionType.IsInterface || proxyDefinitionType.IsValueType) + { + // If the proxy type definition is an interface we create an struct proxy + // If the proxy type definition is an struct then we use that struct to copy the values from the target type + parentType = typeof(ValueType); + typeAttributes = TypeAttributes.Public | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.SequentialLayout | TypeAttributes.Sealed | TypeAttributes.Serializable; + if (proxyDefinitionType.IsInterface) + interfaceTypes = new[] { proxyDefinitionType, typeof(IDuckType) }; + else + interfaceTypes = new[] { typeof(IDuckType) }; + } + else + { + // If the proxy type definition is a class then we create a class proxy + parentType = proxyDefinitionType; + typeAttributes = TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout | TypeAttributes.Sealed; + interfaceTypes = new[] { typeof(IDuckType) }; + } + + // Gets the module builder + var moduleBuilder = GetModuleBuilder(targetType, (targetType.IsPublic || targetType.IsNestedPublic) && (proxyDefinitionType.IsPublic || proxyDefinitionType.IsNestedPublic)); + + // Ensure visibility + EnsureTypeVisibility(moduleBuilder, targetType); + EnsureTypeVisibility(moduleBuilder, proxyDefinitionType); + + var assembly = string.Empty; + if (targetType.Assembly != null) + { + // Include target assembly name and public token. + var asmName = targetType.Assembly.GetName(); + assembly = asmName.Name; + var pbToken = asmName.GetPublicKeyToken(); + assembly += "__" + BitConverter.ToString(pbToken).Replace("-", string.Empty); + assembly = assembly.Replace(".", "_").Replace("+", "__"); + } + + // Create a valid type name that can be used as a member of a class. (BenchmarkDotNet fails if is an invalid name) + var proxyTypeName = $"{assembly}.{targetType.FullName.Replace(".", "_").Replace("+", "__")}.{proxyDefinitionType.FullName.Replace(".", "_").Replace("+", "__")}_{++_typeCount}"; + + // Create Type + var proxyTypeBuilder = moduleBuilder.DefineType( + proxyTypeName, + typeAttributes, + parentType, + interfaceTypes); + + // Create IDuckType and IDuckTypeSetter implementations + var instanceField = CreateIDuckTypeImplementation(proxyTypeBuilder, targetType); + + // Define .ctor to store the instance field + var ctorBuilder = proxyTypeBuilder.DefineConstructor( + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + CallingConventions.Standard, + new[] { instanceField.FieldType }); + var ctorIL = ctorBuilder.GetILGenerator(); + ctorIL.Emit(OpCodes.Ldarg_0); + ctorIL.Emit(OpCodes.Ldarg_1); + ctorIL.Emit(OpCodes.Stfld, instanceField); + ctorIL.Emit(OpCodes.Ret); + + if (proxyDefinitionType.IsValueType) + { + // Create Fields and Properties from the struct information + CreatePropertiesFromStruct(proxyTypeBuilder, proxyDefinitionType, targetType, instanceField); + + // Create Type + var proxyType = proxyTypeBuilder.CreateTypeInfo().AsType(); + return new CreateTypeResult(proxyDefinitionType, proxyType, targetType, CreateStructCopyMethod(moduleBuilder, proxyDefinitionType, proxyType, targetType), null); + } + else + { + // Create Fields and Properties + CreateProperties(proxyTypeBuilder, proxyDefinitionType, targetType, instanceField); + + // Create Methods + CreateMethods(proxyTypeBuilder, proxyDefinitionType, targetType, instanceField); + + // Create Type + var proxyType = proxyTypeBuilder.CreateTypeInfo().AsType(); + return new CreateTypeResult(proxyDefinitionType, proxyType, targetType, GetCreateProxyInstanceDelegate(moduleBuilder, proxyDefinitionType, proxyType, targetType), null); + } + } + catch (Exception ex) + { + return new CreateTypeResult(proxyDefinitionType, null, targetType, null, ExceptionDispatchInfo.Capture(ex)); + } + } + } + + private static FieldInfo CreateIDuckTypeImplementation(TypeBuilder proxyTypeBuilder, Type targetType) + { + var instanceType = targetType; + if (!UseDirectAccessTo(proxyTypeBuilder, targetType)) + { + instanceType = typeof(object); + } + + var instanceField = proxyTypeBuilder.DefineField("_currentInstance", instanceType, FieldAttributes.Private | FieldAttributes.InitOnly); + + var propInstance = proxyTypeBuilder.DefineProperty("Instance", PropertyAttributes.None, typeof(object), null); + var getPropInstance = proxyTypeBuilder.DefineMethod( + "get_Instance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot, + typeof(object), + Type.EmptyTypes); + var il = getPropInstance.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, instanceField); + if (instanceType.IsValueType) + { + il.Emit(OpCodes.Box, instanceType); + } + + il.Emit(OpCodes.Ret); + propInstance.SetGetMethod(getPropInstance); + + var propType = proxyTypeBuilder.DefineProperty("Type", PropertyAttributes.None, typeof(Type), null); + var getPropType = proxyTypeBuilder.DefineMethod( + "get_Type", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot, + typeof(Type), + Type.EmptyTypes); + il = getPropType.GetILGenerator(); + il.Emit(OpCodes.Ldtoken, targetType); + il.EmitCall(OpCodes.Call, GetTypeFromHandleMethodInfo, null); + il.Emit(OpCodes.Ret); + propType.SetGetMethod(getPropType); + + return instanceField; + } + + private static List GetProperties(Type proxyDefinitionType) + { + var selectedProperties = new List(proxyDefinitionType.IsInterface ? proxyDefinitionType.GetProperties() : GetBaseProperties(proxyDefinitionType)); + var implementedInterfaces = proxyDefinitionType.GetInterfaces(); + foreach (var imInterface in implementedInterfaces) + { + if (imInterface == typeof(IDuckType)) + { + continue; + } + + var newProps = imInterface.GetProperties().Where(p => selectedProperties.All(i => i.Name != p.Name)); + selectedProperties.AddRange(newProps); + } + + return selectedProperties; + + static IEnumerable GetBaseProperties(Type baseType) + { + foreach (var prop in baseType.GetProperties()) + { + if (prop.CanRead && (prop.GetMethod.IsAbstract || prop.GetMethod.IsVirtual)) + { + yield return prop; + } + else if (prop.CanWrite && (prop.SetMethod.IsAbstract || prop.SetMethod.IsVirtual)) + { + yield return prop; + } + } + } + } + + private static void CreateProperties(TypeBuilder proxyTypeBuilder, Type proxyDefinitionType, Type targetType, FieldInfo instanceField) + { + // Gets all properties to be implemented + var proxyTypeProperties = GetProperties(proxyDefinitionType); + + foreach (var proxyProperty in proxyTypeProperties) + { + // Ignore the properties marked with `DuckIgnore` attribute + if (proxyProperty.GetCustomAttribute(true) is not null) + { + continue; + } + + PropertyBuilder propertyBuilder = null; + + // If the property is abstract or interface we make sure that we have the property defined in the new class + if ((proxyProperty.CanRead && proxyProperty.GetMethod.IsAbstract) || (proxyProperty.CanWrite && proxyProperty.SetMethod.IsAbstract)) + { + propertyBuilder = proxyTypeBuilder.DefineProperty(proxyProperty.Name, PropertyAttributes.None, proxyProperty.PropertyType, null); + } + + var duckAttribute = proxyProperty.GetCustomAttribute(true) ?? new DuckAttribute(); + duckAttribute.Name ??= proxyProperty.Name; + + switch (duckAttribute.Kind) + { + case DuckKind.Property: + PropertyInfo targetProperty = null; + try + { + targetProperty = targetType.GetProperty(duckAttribute.Name, duckAttribute.BindingFlags); + } + catch + { + // This will run only when multiple indexers are defined in a class, that way we can end up with multiple properties with the same name. + // In this case we make sure we select the indexer we want + targetProperty = targetType.GetProperty(duckAttribute.Name, proxyProperty.PropertyType, proxyProperty.GetIndexParameters().Select(i => i.ParameterType).ToArray()); + } + + if (targetProperty is null) + { + break; + } + + propertyBuilder ??= proxyTypeBuilder.DefineProperty(proxyProperty.Name, PropertyAttributes.None, proxyProperty.PropertyType, null); + + if (proxyProperty.CanRead) + { + // Check if the target property can be read + if (!targetProperty.CanRead) + { + DuckTypePropertyCantBeReadException.Throw(targetProperty); + } + + propertyBuilder.SetGetMethod(GetPropertyGetMethod(proxyTypeBuilder, targetType, proxyProperty, targetProperty, instanceField)); + } + + if (proxyProperty.CanWrite) + { + // Check if the target property can be written + if (!targetProperty.CanWrite) + { + DuckTypePropertyCantBeWrittenException.Throw(targetProperty); + } + + // Check if the target property declaring type is an struct (structs modification is not supported) + if (targetProperty.DeclaringType.IsValueType) + { + DuckTypeStructMembersCannotBeChangedException.Throw(targetProperty.DeclaringType); + } + + propertyBuilder.SetSetMethod(GetPropertySetMethod(proxyTypeBuilder, targetType, proxyProperty, targetProperty, instanceField)); + } + + break; + + case DuckKind.Field: + var targetField = targetType.GetField(duckAttribute.Name, duckAttribute.BindingFlags); + if (targetField is null) + { + break; + } + + propertyBuilder ??= proxyTypeBuilder.DefineProperty(proxyProperty.Name, PropertyAttributes.None, proxyProperty.PropertyType, null); + + if (proxyProperty.CanRead) + { + propertyBuilder.SetGetMethod(GetFieldGetMethod(proxyTypeBuilder, targetType, proxyProperty, targetField, instanceField)); + } + + if (proxyProperty.CanWrite) + { + // Check if the target field is marked as InitOnly (readonly) and throw an exception in that case + if ((targetField.Attributes & FieldAttributes.InitOnly) != 0) + { + DuckTypeFieldIsReadonlyException.Throw(targetField); + } + + // Check if the target field declaring type is an struct (structs modification is not supported) + if (targetField.DeclaringType.IsValueType) + { + DuckTypeStructMembersCannotBeChangedException.Throw(targetField.DeclaringType); + } + + propertyBuilder.SetSetMethod(GetFieldSetMethod(proxyTypeBuilder, targetType, proxyProperty, targetField, instanceField)); + } + + break; + } + + if (propertyBuilder is null) + { + continue; + } + + if (proxyProperty.CanRead && propertyBuilder.GetMethod is null) + { + DuckTypePropertyOrFieldNotFoundException.Throw(proxyProperty.Name, duckAttribute.Name); + } + + if (proxyProperty.CanWrite && propertyBuilder.SetMethod is null) + { + DuckTypePropertyOrFieldNotFoundException.Throw(proxyProperty.Name, duckAttribute.Name); + } + } + } + + private static void CreatePropertiesFromStruct(TypeBuilder proxyTypeBuilder, Type proxyDefinitionType, Type targetType, FieldInfo instanceField) + { + // Gets all fields to be copied + foreach (var proxyFieldInfo in proxyDefinitionType.GetFields()) + { + // Skip readonly fields + if ((proxyFieldInfo.Attributes & FieldAttributes.InitOnly) != 0) + { + continue; + } + + // Ignore the fields marked with `DuckIgnore` attribute + if (proxyFieldInfo.GetCustomAttribute(true) is not null) + { + continue; + } + + PropertyBuilder propertyBuilder = null; + + var duckAttribute = proxyFieldInfo.GetCustomAttribute(true) ?? new DuckAttribute(); + duckAttribute.Name ??= proxyFieldInfo.Name; + + switch (duckAttribute.Kind) + { + case DuckKind.Property: + var targetProperty = targetType.GetProperty(duckAttribute.Name, duckAttribute.BindingFlags); + if (targetProperty is null) + { + break; + } + + // Check if the target property can be read + if (!targetProperty.CanRead) + { + DuckTypePropertyCantBeReadException.Throw(targetProperty); + } + + propertyBuilder = proxyTypeBuilder.DefineProperty(proxyFieldInfo.Name, PropertyAttributes.None, proxyFieldInfo.FieldType, null); + propertyBuilder.SetGetMethod(GetPropertyGetMethod(proxyTypeBuilder, targetType, proxyFieldInfo, targetProperty, instanceField)); + break; + + case DuckKind.Field: + var targetField = targetType.GetField(duckAttribute.Name, duckAttribute.BindingFlags); + if (targetField is null) + { + break; + } + + propertyBuilder = proxyTypeBuilder.DefineProperty(proxyFieldInfo.Name, PropertyAttributes.None, proxyFieldInfo.FieldType, null); + propertyBuilder.SetGetMethod(GetFieldGetMethod(proxyTypeBuilder, targetType, proxyFieldInfo, targetField, instanceField)); + break; + } + + if (propertyBuilder is null) + { + DuckTypePropertyOrFieldNotFoundException.Throw(proxyFieldInfo.Name, duckAttribute.Name); + } + } + } + + private static Delegate GetCreateProxyInstanceDelegate(ModuleBuilder moduleBuilder, Type proxyDefinitionType, Type proxyType, Type targetType) + { + var ctor = proxyType.GetConstructors()[0]; + + var createProxyMethod = new DynamicMethod( + $"CreateProxyInstance<{proxyType.Name}>", + proxyDefinitionType, + new[] { typeof(object) }, + typeof(DuckType).Module, + true); + var il = createProxyMethod.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + if (UseDirectAccessTo(moduleBuilder, targetType)) + { + if (targetType.IsValueType) + { + il.Emit(OpCodes.Unbox_Any, targetType); + } + else + { + il.Emit(OpCodes.Castclass, targetType); + } + } + + il.Emit(OpCodes.Newobj, ctor); + + if (proxyType.IsValueType) + { + il.Emit(OpCodes.Box, proxyType); + } + + il.Emit(OpCodes.Ret); + var delegateType = typeof(CreateProxyInstance<>).MakeGenericType(proxyDefinitionType); + return createProxyMethod.CreateDelegate(delegateType); + } + + private static Delegate CreateStructCopyMethod(ModuleBuilder moduleBuilder, Type proxyDefinitionType, Type proxyType, Type targetType) + { + var ctor = proxyType.GetConstructors()[0]; + + var createStructMethod = new DynamicMethod( + $"CreateStructInstance<{proxyType.Name}>", + proxyDefinitionType, + new[] { typeof(object) }, + typeof(Elastic.Apm.Profiler.Managed.DuckTyping.DuckType).Module, + true); + var il = createStructMethod.GetILGenerator(); + + // First we declare the locals + var proxyLocal = il.DeclareLocal(proxyType); + var structLocal = il.DeclareLocal(proxyDefinitionType); + + // We create an instance of the proxy type + il.Emit(OpCodes.Ldloca_S, proxyLocal.LocalIndex); + il.Emit(OpCodes.Ldarg_0); + if (UseDirectAccessTo(moduleBuilder, targetType)) + { + if (targetType.IsValueType) + { + il.Emit(OpCodes.Unbox_Any, targetType); + } + else + { + il.Emit(OpCodes.Castclass, targetType); + } + } + + il.Emit(OpCodes.Call, ctor); + + // Create the destination structure + il.Emit(OpCodes.Ldloca_S, structLocal.LocalIndex); + il.Emit(OpCodes.Initobj, proxyDefinitionType); + + // Start copy properties from the proxy to the structure + foreach (var finfo in proxyDefinitionType.GetFields()) + { + // Skip readonly fields + if ((finfo.Attributes & FieldAttributes.InitOnly) != 0) + { + continue; + } + + // Ignore the fields marked with `DuckIgnore` attribute + if (finfo.GetCustomAttribute(true) is not null) + { + continue; + } + + var prop = proxyType.GetProperty(finfo.Name); + il.Emit(OpCodes.Ldloca_S, structLocal.LocalIndex); + il.Emit(OpCodes.Ldloca_S, proxyLocal.LocalIndex); + il.EmitCall(OpCodes.Call, prop.GetMethod, null); + il.Emit(OpCodes.Stfld, finfo); + } + + // Return + il.WriteLoadLocal(structLocal.LocalIndex); + il.Emit(OpCodes.Ret); + + var delegateType = typeof(CreateProxyInstance<>).MakeGenericType(proxyDefinitionType); + return createStructMethod.CreateDelegate(delegateType); + } + + /// + /// Struct to store the result of creating a proxy type + /// + public readonly struct CreateTypeResult + { + /// + /// Gets if the proxy type creation was successful + /// + public readonly bool Success; + + /// + /// Target type + /// + public readonly Type TargetType; + + private readonly Type _proxyType; + private readonly ExceptionDispatchInfo _exceptionInfo; + private readonly Delegate _activator; + + /// + /// Initializes a new instance of the struct. + /// + /// Proxy type definition + /// Proxy type + /// Target type + /// Proxy activator + /// Exception dispatch info instance + internal CreateTypeResult(Type proxyTypeDefinition, Type proxyType, Type targetType, Delegate activator, ExceptionDispatchInfo exceptionInfo) + { + TargetType = targetType; + _proxyType = proxyType; + _activator = activator; + _exceptionInfo = exceptionInfo; + Success = proxyType != null && exceptionInfo == null; + if (exceptionInfo != null) + { + var methodInfo = typeof(CreateTypeResult).GetMethod(nameof(ThrowOnError), BindingFlags.NonPublic | BindingFlags.Instance); + _activator = methodInfo + .MakeGenericMethod(proxyTypeDefinition) + .CreateDelegate( + typeof(CreateProxyInstance<>).MakeGenericType(proxyTypeDefinition), + this); + } + } + + /// + /// Gets the Proxy type + /// + public Type ProxyType + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + _exceptionInfo?.Throw(); + return _proxyType; + } + } + + /// + /// Create a new proxy instance from a target instance + /// + /// Type of the return value + /// Target instance value + /// Proxy instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T CreateInstance(object instance) => ((CreateProxyInstance)_activator)(instance); + + /// + /// Get if the proxy instance can be created + /// + /// true if the proxy can be created; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanCreate() => _exceptionInfo == null; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal IDuckType CreateInstance(object instance) => (IDuckType)_activator.DynamicInvoke(instance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private T ThrowOnError(object instance) + { + _exceptionInfo.Throw(); + return default; + } + } + + /// + /// Generics Create Cache FastPath + /// + /// Type of proxy definition + public static class CreateCache + { + /// + /// Gets the type of T + /// + public static readonly Type Type = typeof(T); + + /// + /// Gets if the T type is visible + /// + public static readonly bool IsVisible = Type.IsPublic || Type.IsNestedPublic; + + private static CreateTypeResult _fastPath = default; + + /// + /// Gets the proxy type for a target type using the T proxy definition + /// + /// Target type + /// CreateTypeResult instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CreateTypeResult GetProxy(Type targetType) + { + // We set a fast path for the first proxy type for a proxy definition. (It's likely to have a proxy definition just for one target type) + var fastPath = _fastPath; + if (fastPath.TargetType == targetType) + return fastPath; + + var result = GetProxySlow(targetType); + + fastPath = _fastPath; + if (fastPath.TargetType is null) + _fastPath = result; + + return result; + } + + /// + /// Create a new instance of a proxy type for a target instance using the T proxy definition + /// + /// Object instance + /// Proxy instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Create(object instance) + { + if (instance is null) + return default; + + return GetProxy(instance.GetType()).CreateInstance(instance); + } + + /// + /// Get if the proxy instance can be created + /// + /// Object instance + /// true if a proxy can be created; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanCreate(object instance) + { + if (instance is null) + return false; + + return GetProxy(instance.GetType()).CanCreate(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static CreateTypeResult GetProxySlow(Type targetType) + { +#if NET45 + if (!Type.IsValueType && !IsVisible) + { + DuckTypeTypeIsNotPublicException.Throw(Type, nameof(Type)); + } +#endif + return GetOrCreateProxyType(Type, targetType); + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckTypeExceptions.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckTypeExceptions.cs new file mode 100644 index 000000000..e39a8b4a5 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckTypeExceptions.cs @@ -0,0 +1,244 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Diagnostics; +using System.Reflection; + +#pragma warning disable SA1649 // File name must match first type name +#pragma warning disable SA1402 // File may only contain a single class + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// DuckType Exception + /// + public class DuckTypeException : Exception + { + internal DuckTypeException(string message) + : base(message) + { + } + } + + /// + /// DuckType proxy type definition is null + /// + public class DuckTypeProxyTypeDefinitionIsNull : DuckTypeException + { + private DuckTypeProxyTypeDefinitionIsNull() + : base($"The proxy type definition is null.") + { + } + + [DebuggerHidden] + internal static void Throw() => throw new DuckTypeProxyTypeDefinitionIsNull(); + } + + /// + /// DuckType target object instance is null + /// + public class DuckTypeTargetObjectInstanceIsNull : DuckTypeException + { + private DuckTypeTargetObjectInstanceIsNull() + : base($"The target object instance is null.") + { + } + + [DebuggerHidden] + internal static void Throw() => throw new DuckTypeTargetObjectInstanceIsNull(); + } + + /// + /// DuckType invalid type conversion exception + /// + public class DuckTypeInvalidTypeConversionException : DuckTypeException + { + private DuckTypeInvalidTypeConversionException(Type actualType, Type expectedType) + : base($"Invalid type conversion from {actualType.FullName} to {expectedType.FullName}") + { + } + + [DebuggerHidden] + internal static void Throw(Type actualType, Type expectedType) => throw new DuckTypeInvalidTypeConversionException(actualType, expectedType); + } + + /// + /// DuckType property can't be read + /// + public class DuckTypePropertyCantBeReadException : DuckTypeException + { + private DuckTypePropertyCantBeReadException(PropertyInfo property) + : base($"The property '{property.Name}' can't be read, you should remove the getter from the proxy definition base type class or interface.") + { + } + + [DebuggerHidden] + internal static void Throw(PropertyInfo property) => throw new DuckTypePropertyCantBeReadException(property); + } + + /// + /// DuckType property can't be written + /// + public class DuckTypePropertyCantBeWrittenException : DuckTypeException + { + private DuckTypePropertyCantBeWrittenException(PropertyInfo property) + : base($"The property '{property.Name}' can't be written, you should remove the setter from the proxy definition base type class or interface.") + { + } + + [DebuggerHidden] + internal static void Throw(PropertyInfo property) => throw new DuckTypePropertyCantBeWrittenException(property); + } + + /// + /// DuckType property argument doesn't have the same argument length + /// + public class DuckTypePropertyArgumentsLengthException : DuckTypeException + { + private DuckTypePropertyArgumentsLengthException(PropertyInfo property) + : base($"The property '{property.Name}' doesn't have the same number of arguments as the original property.") + { + } + + [DebuggerHidden] + internal static void Throw(PropertyInfo property) => throw new DuckTypePropertyArgumentsLengthException(property); + } + + /// + /// DuckType field is readonly + /// + public class DuckTypeFieldIsReadonlyException : DuckTypeException + { + private DuckTypeFieldIsReadonlyException(FieldInfo field) + : base($"The field '{field.Name}' is marked as readonly, you should remove the setter from the base type class or interface.") + { + } + + [DebuggerHidden] + internal static void Throw(FieldInfo field) => throw new DuckTypeFieldIsReadonlyException(field); + } + + /// + /// DuckType property or field not found + /// + public class DuckTypePropertyOrFieldNotFoundException : DuckTypeException + { + private DuckTypePropertyOrFieldNotFoundException(string name, string duckAttributeName) + : base($"The property or field '{duckAttributeName}' for the proxy property '{name}' was not found in the instance.") + { + } + + [DebuggerHidden] + internal static void Throw(string name, string duckAttributeName) => throw new DuckTypePropertyOrFieldNotFoundException(name, duckAttributeName); + } + + /// + /// DuckType type is not public exception + /// + public class DuckTypeTypeIsNotPublicException : DuckTypeException + { + private DuckTypeTypeIsNotPublicException(Type type, string argumentName) + : base($"The type '{type.FullName}' must be public, argument: '{argumentName}'") + { + } + + [DebuggerHidden] + internal static void Throw(Type type, string argumentName) => throw new DuckTypeTypeIsNotPublicException(type, argumentName); + } + + /// + /// DuckType struct members cannot be changed exception + /// + public class DuckTypeStructMembersCannotBeChangedException : DuckTypeException + { + private DuckTypeStructMembersCannotBeChangedException(Type type) + : base($"Modifying struct members is not supported. [{type.FullName}]") + { + } + + [DebuggerHidden] + internal static void Throw(Type type) => throw new DuckTypeStructMembersCannotBeChangedException(type); + } + + /// + /// DuckType target method can not be found exception + /// + public class DuckTypeTargetMethodNotFoundException : DuckTypeException + { + private DuckTypeTargetMethodNotFoundException(MethodInfo method) + : base($"The target method for the proxy method '{method}' was not found.") + { + } + + [DebuggerHidden] + internal static void Throw(MethodInfo method) => throw new DuckTypeTargetMethodNotFoundException(method); + } + + /// + /// DuckType proxy method parameter is missing exception + /// + public class DuckTypeProxyMethodParameterIsMissingException : DuckTypeException + { + private DuckTypeProxyMethodParameterIsMissingException(MethodInfo proxyMethod, ParameterInfo targetParameterInfo) + : base($"The proxy method '{proxyMethod.Name}' is missing parameter '{targetParameterInfo.Name}' declared in the target method.") + { + } + + [DebuggerHidden] + internal static void Throw(MethodInfo proxyMethod, ParameterInfo targetParameterInfo) => + throw new DuckTypeProxyMethodParameterIsMissingException(proxyMethod, targetParameterInfo); + } + + /// + /// DuckType parameter signature mismatch between proxy and target method + /// + public class DuckTypeProxyAndTargetMethodParameterSignatureMismatchException : DuckTypeException + { + private DuckTypeProxyAndTargetMethodParameterSignatureMismatchException(MethodInfo proxyMethod, MethodInfo targetMethod) + : base($"Parameter signature mismatch between proxy '{proxyMethod}' and target method '{targetMethod}'") + { + } + + [DebuggerHidden] + internal static void Throw(MethodInfo proxyMethod, MethodInfo targetMethod) => + throw new DuckTypeProxyAndTargetMethodParameterSignatureMismatchException(proxyMethod, targetMethod); + } + + /// + /// DuckType proxy methods with generic parameters are not supported in non public instances exception + /// + public class DuckTypeProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesException : DuckTypeException + { + private DuckTypeProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesException(MethodInfo proxyMethod) + : base($"The proxy method with generic parameters '{proxyMethod}' are not supported on non public instances") + { + } + + [DebuggerHidden] + internal static void Throw(MethodInfo proxyMethod) => + throw new DuckTypeProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesException(proxyMethod); + } + + /// + /// DuckType proxy method has an ambiguous match in the target type exception + /// + public class DuckTypeTargetMethodAmbiguousMatchException : DuckTypeException + { + private DuckTypeTargetMethodAmbiguousMatchException(MethodInfo proxyMethod, MethodInfo targetMethod, MethodInfo targetMethod2) + : base($"The proxy method '{proxyMethod}' matches at least two methods in the target type. Method1 = '{targetMethod}' and Method2 = '{targetMethod2}'") + { + } + + [DebuggerHidden] + internal static void Throw(MethodInfo proxyMethod, MethodInfo targetMethod, MethodInfo targetMethod2) => + throw new DuckTypeTargetMethodAmbiguousMatchException(proxyMethod, targetMethod, targetMethod2); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckTypeExtensions.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckTypeExtensions.cs new file mode 100644 index 000000000..d8a15fdf8 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/DuckTypeExtensions.cs @@ -0,0 +1,176 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Runtime.CompilerServices; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck type extensions + /// + public static class DuckTypeExtensions + { + /// + /// Gets the duck type instance for the object implementing a base class or interface T + /// + /// Object instance + /// Target type + /// DuckType instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T DuckCast(this object instance) + => DuckType.Create(instance); + + /// + /// Gets the duck type instance for the object implementing a base class or interface T + /// + /// Object instance + /// Target type + /// DuckType instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object DuckCast(this object instance, Type targetType) + => DuckType.Create(targetType, instance); + + /// + /// Tries to ducktype the object implementing a base class or interface T + /// + /// Target type + /// Object instance + /// Ducktype instance + /// true if the object instance was ducktyped; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryDuckCast(this object instance, out T value) + { + if (instance is null) + DuckTypeTargetObjectInstanceIsNull.Throw(); + + if (DuckType.CreateCache.IsVisible) + { + var proxyResult = DuckType.CreateCache.GetProxy(instance.GetType()); + if (proxyResult.Success) + { + value = proxyResult.CreateInstance(instance); + return true; + } + } + + value = default; + return false; + } + + /// + /// Tries to ducktype the object implementing a base class or interface T + /// + /// Object instance + /// Target type + /// Ducktype instance + /// true if the object instance was ducktyped; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryDuckCast(this object instance, Type targetType, out object value) + { + if (instance is null) + DuckTypeTargetObjectInstanceIsNull.Throw(); + + if (targetType != null && (targetType.IsPublic || targetType.IsNestedPublic)) + { + var proxyResult = DuckType.GetOrCreateProxyType(targetType, instance.GetType()); + if (proxyResult.Success) + { + value = proxyResult.CreateInstance(instance); + return true; + } + } + + value = default; + return false; + } + + /// + /// Gets the duck type instance for the object implementing a base class or interface T + /// + /// Object instance + /// Target type + /// DuckType instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T DuckAs(this object instance) + where T : class + { + if (instance is null) + DuckTypeTargetObjectInstanceIsNull.Throw(); + + if (DuckType.CreateCache.IsVisible) + { + var proxyResult = DuckType.CreateCache.GetProxy(instance.GetType()); + if (proxyResult.Success) + return proxyResult.CreateInstance(instance); + } + + return null; + } + + /// + /// Gets the duck type instance for the object implementing a base class or interface T + /// + /// Object instance + /// Target type + /// DuckType instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object DuckAs(this object instance, Type targetType) + { + if (instance is null) + DuckTypeTargetObjectInstanceIsNull.Throw(); + + if (targetType != null && (targetType.IsPublic || targetType.IsNestedPublic)) + { + var proxyResult = DuckType.GetOrCreateProxyType(targetType, instance.GetType()); + if (proxyResult.Success) + return proxyResult.CreateInstance(instance); + } + + return null; + } + + /// + /// Gets if a proxy can be created + /// + /// Instance object + /// Duck type + /// true if the proxy can be created; otherwise, false + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool DuckIs(this object instance) + { + if (instance is null) + DuckTypeTargetObjectInstanceIsNull.Throw(); + + if (DuckType.CreateCache.IsVisible) + return DuckType.CanCreate(instance); + + return false; + } + + /// + /// Gets if a proxy can be created + /// + /// Instance object + /// Duck type + /// true if the proxy can be created; otherwise, false + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool DuckIs(this object instance, Type targetType) + { + if (instance is null) + DuckTypeTargetObjectInstanceIsNull.Throw(); + + if (targetType != null && (targetType.IsPublic || targetType.IsNestedPublic)) + return DuckType.CanCreate(targetType, instance); + + return false; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/IDuckType.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/IDuckType.cs new file mode 100644 index 000000000..911bf5063 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/IDuckType.cs @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Duck type interface + /// + public interface IDuckType + { + /// + /// Gets instance + /// + object Instance { get; } + + /// + /// Gets instance Type + /// + Type Type { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/ILHelpersExtensions.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/ILHelpersExtensions.cs new file mode 100644 index 000000000..023d411ed --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/ILHelpersExtensions.cs @@ -0,0 +1,392 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + /// + /// Internal IL Helpers + /// + internal static class ILHelpersExtensions + { + private static List _dynamicMethods = new List(); + + internal static DynamicMethod GetDynamicMethodForIndex(int index) + { + lock (_dynamicMethods) + return _dynamicMethods[index]; + } + + internal static void CreateDelegateTypeFor(TypeBuilder proxyType, DynamicMethod dynamicMethod, out Type delType, out MethodInfo invokeMethod) + { + var modBuilder = (ModuleBuilder)proxyType.Module; + var delegateType = modBuilder.DefineType($"{dynamicMethod.Name}Delegate_" + Guid.NewGuid().ToString("N"), TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass, typeof(MulticastDelegate)); + + // Delegate .ctor + var constructorBuilder = delegateType.DefineConstructor(MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(object), typeof(IntPtr) }); + constructorBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed); + + // Define the Invoke method for the delegate + var parameters = dynamicMethod.GetParameters(); + var paramTypes = new Type[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + paramTypes[i] = parameters[i].ParameterType; + } + + var methodBuilder = delegateType.DefineMethod("Invoke", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, dynamicMethod.ReturnType, paramTypes); + for (var i = 0; i < parameters.Length; i++) + { + methodBuilder.DefineParameter(i + 1, parameters[i].Attributes, parameters[i].Name); + } + + methodBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed); + + delType = delegateType.CreateTypeInfo().AsType(); + invokeMethod = delType.GetMethod("Invoke"); + } + + /// + /// Load instance argument + /// + /// LazyILGenerator instance + /// Actual type + /// Expected type + internal static void LoadInstanceArgument(this LazyILGenerator il, Type actualType, Type expectedType) + { + il.Emit(OpCodes.Ldarg_0); + if (actualType == expectedType) + { + return; + } + + if (expectedType.IsValueType) + { + il.DeclareLocal(expectedType); + il.Emit(OpCodes.Unbox_Any, expectedType); + il.Emit(OpCodes.Stloc_0); + il.Emit(OpCodes.Ldloca_S, 0); + } + else + { + il.Emit(OpCodes.Castclass, expectedType); + } + } + + /// + /// Write load arguments + /// + /// LazyILGenerator instance + /// Argument index + /// Define if we need to take into account the instance argument + internal static void WriteLoadArgument(this LazyILGenerator il, int index, bool isStatic) + { + if (!isStatic) + { + index += 1; + } + + switch (index) + { + case 0: + il.Emit(OpCodes.Ldarg_0); + break; + case 1: + il.Emit(OpCodes.Ldarg_1); + break; + case 2: + il.Emit(OpCodes.Ldarg_2); + break; + case 3: + il.Emit(OpCodes.Ldarg_3); + break; + default: + il.Emit(OpCodes.Ldarg_S, index); + break; + } + } + + /// + /// Write load local + /// + /// LazyILGenerator instance + /// Local index + internal static void WriteLoadLocal(this LazyILGenerator il, int index) + { + switch (index) + { + case 0: + il.Emit(OpCodes.Ldloc_0); + break; + case 1: + il.Emit(OpCodes.Ldloc_1); + break; + case 2: + il.Emit(OpCodes.Ldloc_2); + break; + case 3: + il.Emit(OpCodes.Ldloc_3); + break; + default: + il.Emit(OpCodes.Ldloc_S, index); + break; + } + } + + /// + /// Write load local + /// + /// ILGenerator instance + /// Local index + internal static void WriteLoadLocal(this ILGenerator il, int index) + { + switch (index) + { + case 0: + il.Emit(OpCodes.Ldloc_0); + break; + case 1: + il.Emit(OpCodes.Ldloc_1); + break; + case 2: + il.Emit(OpCodes.Ldloc_2); + break; + case 3: + il.Emit(OpCodes.Ldloc_3); + break; + default: + il.Emit(OpCodes.Ldloc_S, index); + break; + } + } + + /// + /// Write store local + /// + /// LazyILGenerator instance + /// Local index + internal static void WriteStoreLocal(this LazyILGenerator il, int index) + { + switch (index) + { + case 0: + il.Emit(OpCodes.Stloc_0); + break; + case 1: + il.Emit(OpCodes.Stloc_1); + break; + case 2: + il.Emit(OpCodes.Stloc_2); + break; + case 3: + il.Emit(OpCodes.Stloc_3); + break; + default: + il.Emit(OpCodes.Stloc_S, index); + break; + } + } + + /// + /// Write constant int value + /// + /// LazyILGenerator instance + /// int value + internal static void WriteInt(this LazyILGenerator il, int value) + { + if (value >= -1 && value <= 8) + { + switch (value) + { + case -1: + il.Emit(OpCodes.Ldc_I4_M1); + break; + case 0: + il.Emit(OpCodes.Ldc_I4_0); + break; + case 1: + il.Emit(OpCodes.Ldc_I4_1); + break; + case 2: + il.Emit(OpCodes.Ldc_I4_2); + break; + case 3: + il.Emit(OpCodes.Ldc_I4_3); + break; + case 4: + il.Emit(OpCodes.Ldc_I4_4); + break; + case 5: + il.Emit(OpCodes.Ldc_I4_5); + break; + case 6: + il.Emit(OpCodes.Ldc_I4_6); + break; + case 7: + il.Emit(OpCodes.Ldc_I4_7); + break; + default: + il.Emit(OpCodes.Ldc_I4_8); + break; + } + } + else if (value >= -128 && value <= 127) + { + il.Emit(OpCodes.Ldc_I4_S, value); + } + else + { + il.Emit(OpCodes.Ldc_I4, value); + } + } + + /// + /// Convert a current type to an expected type + /// + /// LazyILGenerator instance + /// Actual type + /// Expected type + internal static void WriteTypeConversion(this LazyILGenerator il, Type actualType, Type expectedType) + { + var actualUnderlyingType = actualType.IsEnum ? Enum.GetUnderlyingType(actualType) : actualType; + var expectedUnderlyingType = expectedType.IsEnum ? Enum.GetUnderlyingType(expectedType) : expectedType; + + if (actualUnderlyingType == expectedUnderlyingType) + { + return; + } + + if (actualUnderlyingType.IsValueType) + { + if (expectedUnderlyingType.IsValueType) + { + // If both underlying types are value types then both must be of the same type. + DuckTypeInvalidTypeConversionException.Throw(actualType, expectedType); + } + else + { + // An underlying type can be boxed and converted to an object or interface type if the actual type support this + // if not we should throw. + if (expectedUnderlyingType == typeof(object)) + { + // If the expected type is object we just need to box the value + il.Emit(OpCodes.Box, actualType); + } + else if (expectedUnderlyingType.IsAssignableFrom(actualUnderlyingType)) + { + // If the expected type can be assigned from the value type (ex: struct implementing an interface) + il.Emit(OpCodes.Box, actualType); + il.Emit(OpCodes.Castclass, expectedType); + } + else + { + // If the expected type can't be assigned from the actual value type. + // Means if the expected type is an interface the actual type doesn't implement it. + // So no possible conversion or casting can be made here. + DuckTypeInvalidTypeConversionException.Throw(actualType, expectedType); + } + } + } + else + { + if (expectedUnderlyingType.IsValueType) + { + // We only allow conversions from objects or interface type if the actual type support this + // if not we should throw. + if (actualUnderlyingType == typeof(object) || actualUnderlyingType.IsAssignableFrom(expectedUnderlyingType)) + { + // WARNING: The actual type instance can't be detected at this point, we have to check it at runtime. + /* + * In this case we emit something like: + * { + * if (!(value is [expectedType])) { + * throw new InvalidCastException(); + * } + * + * return ([expectedType])value; + * } + */ + var lblIsExpected = il.DefineLabel(); + + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Isinst, expectedType); + il.Emit(OpCodes.Brtrue_S, lblIsExpected); + + il.Emit(OpCodes.Pop); + il.ThrowException(typeof(InvalidCastException)); + + il.MarkLabel(lblIsExpected); + il.Emit(OpCodes.Unbox_Any, expectedType); + } + else + { + DuckTypeInvalidTypeConversionException.Throw(actualType, expectedType); + } + } + else if (expectedUnderlyingType != typeof(object)) + { + il.Emit(OpCodes.Castclass, expectedUnderlyingType); + } + } + } + + /// + /// Write a Call to a method using Calli + /// + /// LazyILGenerator instance + /// Method to get called + internal static void WriteMethodCalli(this LazyILGenerator il, MethodInfo method) + { + il.Emit(OpCodes.Ldc_I8, (long)method.MethodHandle.GetFunctionPointer()); + il.Emit(OpCodes.Conv_I); + il.EmitCalli( + OpCodes.Calli, + method.CallingConvention, + method.ReturnType, + method.GetParameters().Select(p => p.ParameterType).ToArray(), + null); + } + + /// + /// Write a DynamicMethod call by creating and injecting a custom delegate in the proxyType + /// + /// LazyILGenerator instance + /// Dynamic method to get called + /// ProxyType builder + internal static void WriteDynamicMethodCall(this LazyILGenerator il, DynamicMethod dynamicMethod, TypeBuilder proxyType) + { + // We create a custom delegate inside the module builder + CreateDelegateTypeFor(proxyType, dynamicMethod, out var delegateType, out var invokeMethod); + int index; + lock (_dynamicMethods) + { + _dynamicMethods.Add(dynamicMethod); + index = _dynamicMethods.Count - 1; + } + + // We fill the DelegateCache<> for that custom type with the delegate instance + var fillDelegateMethodInfo = typeof(Elastic.Apm.Profiler.Managed.DuckTyping.DuckType.DelegateCache<>).MakeGenericType(delegateType).GetMethod("FillDelegate", BindingFlags.NonPublic | BindingFlags.Static); + fillDelegateMethodInfo.Invoke(null, new object[] { index }); + + // We get the delegate instance and load it in to the stack before the parameters (at the begining of the IL body) + il.SetOffset(0); + il.EmitCall(OpCodes.Call, typeof(DuckType.DelegateCache<>).MakeGenericType(delegateType).GetMethod("GetDelegate"), null); + il.ResetOffset(); + + // We emit the call to the delegate invoke method. + il.EmitCall(OpCodes.Callvirt, invokeMethod, null); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/IgnoresAccessChecksToAttribute.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/IgnoresAccessChecksToAttribute.cs new file mode 100644 index 000000000..d24462c8c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/IgnoresAccessChecksToAttribute.cs @@ -0,0 +1,31 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// This attribute is recognized by the CLR and allow us to disable visibility checks for certain assemblies (only from 4.6+) + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class IgnoresAccessChecksToAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// Assembly name + public IgnoresAccessChecksToAttribute(string assemblyName) => AssemblyName = assemblyName; + + /// + /// Gets the assembly name + /// + public string AssemblyName { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/LazyILGenerator.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/LazyILGenerator.cs new file mode 100644 index 000000000..ff361c551 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/LazyILGenerator.cs @@ -0,0 +1,329 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + internal class LazyILGenerator + { + private ILGenerator _generator; + private List> _instructions; + private int _offset; + + public LazyILGenerator(ILGenerator generator) + { + _generator = generator; + _instructions = new List>(16); + } + + public int Offset => _offset; + + public int Count => _instructions.Count; + + public void SetOffset(int value) + { + if (value > _instructions.Count) + _offset = _instructions.Count; + else + _offset = value; + } + + public void ResetOffset() => _offset = _instructions.Count; + + public void BeginScope() + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.BeginScope()); + else + _instructions.Insert(_offset, il => il.BeginScope()); + + _offset++; + } + + public LocalBuilder DeclareLocal(Type localType, bool pinned) => _generator.DeclareLocal(localType, pinned); + + public LocalBuilder DeclareLocal(Type localType) => _generator.DeclareLocal(localType); + + public Label DefineLabel() => _generator.DefineLabel(); + + public void Emit(OpCode opcode, string str) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, str)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, str)); + + _offset++; + } + + public void Emit(OpCode opcode, FieldInfo field) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, field)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, field)); + + _offset++; + } + + public void Emit(OpCode opcode, Label[] labels) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, labels)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, labels)); + + _offset++; + } + + public void Emit(OpCode opcode, Label label) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, label)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, label)); + + _offset++; + } + + public void Emit(OpCode opcode, LocalBuilder local) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, local)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, local)); + + _offset++; + } + + public void Emit(OpCode opcode, float arg) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, arg)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, arg)); + + _offset++; + } + + public void Emit(OpCode opcode, byte arg) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, arg)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, arg)); + + _offset++; + } + + public void Emit(OpCode opcode, sbyte arg) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, arg)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, arg)); + + _offset++; + } + + public void Emit(OpCode opcode, short arg) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, arg)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, arg)); + + _offset++; + } + + public void Emit(OpCode opcode, double arg) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, arg)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, arg)); + + _offset++; + } + + public void Emit(OpCode opcode, MethodInfo meth) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, meth)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, meth)); + + _offset++; + } + + public void Emit(OpCode opcode, int arg) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, arg)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, arg)); + + _offset++; + } + + public void Emit(OpCode opcode) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode)); + else + _instructions.Insert(_offset, il => il.Emit(opcode)); + + _offset++; + } + + public void Emit(OpCode opcode, long arg) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, arg)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, arg)); + + _offset++; + } + + public void Emit(OpCode opcode, Type cls) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, cls)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, cls)); + + _offset++; + } + + public void Emit(OpCode opcode, SignatureHelper signature) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, signature)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, signature)); + + _offset++; + } + + public void Emit(OpCode opcode, ConstructorInfo con) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.Emit(opcode, con)); + else + _instructions.Insert(_offset, il => il.Emit(opcode, con)); + + _offset++; + } + + public void EmitCall(OpCode opcode, MethodInfo methodInfo, Type[] optionalParameterTypes) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.EmitCall(opcode, methodInfo, optionalParameterTypes)); + else + _instructions.Insert(_offset, il => il.EmitCall(opcode, methodInfo, optionalParameterTypes)); + + _offset++; + } + + public void EmitCalli(OpCode opcode, CallingConventions callingConvention, Type returnType, Type[] parameterTypes, Type[] optionalParameterTypes) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.EmitCalli(opcode, callingConvention, returnType, parameterTypes, optionalParameterTypes)); + else + _instructions.Insert(_offset, il => il.EmitCalli(opcode, callingConvention, returnType, parameterTypes, optionalParameterTypes)); + + _offset++; + } + + public void EmitWriteLine(string value) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.EmitWriteLine(value)); + else + _instructions.Insert(_offset, il => il.EmitWriteLine(value)); + + _offset++; + } + + public void EmitWriteLine(FieldInfo fld) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.EmitWriteLine(fld)); + else + _instructions.Insert(_offset, il => il.EmitWriteLine(fld)); + + _offset++; + } + + public void EmitWriteLine(LocalBuilder localBuilder) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.EmitWriteLine(localBuilder)); + else + _instructions.Insert(_offset, il => il.EmitWriteLine(localBuilder)); + + _offset++; + } + + public void EndScope() + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.EndScope()); + else + _instructions.Insert(_offset, il => il.EndScope()); + + _offset++; + } + + public void MarkLabel(Label loc) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.MarkLabel(loc)); + else + _instructions.Insert(_offset, il => il.MarkLabel(loc)); + + _offset++; + } + + public void ThrowException(Type excType) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.ThrowException(excType)); + else + _instructions.Insert(_offset, il => il.ThrowException(excType)); + + _offset++; + } + + public void UsingNamespace(string usingNamespace) + { + if (_offset == _instructions.Count) + _instructions.Add(il => il.UsingNamespace(usingNamespace)); + else + _instructions.Insert(_offset, il => il.UsingNamespace(usingNamespace)); + + _offset++; + } + + public void Flush() + { + foreach (var instr in _instructions) + instr(_generator); + + _instructions.Clear(); + _offset = 0; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/README.md b/src/Elastic.Apm.Profiler.Managed/DuckTyping/README.md new file mode 100644 index 000000000..e0be6bff5 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/README.md @@ -0,0 +1,775 @@ +# Datadog.Trace.DuckTyping + +The duck typing library allows us to get and set data from fields and properties and call methods from an object without having the type definition at compile time. This is done by creating a proxy type to an object instance using a proxy definition at runtime. + +The goal of the library is to have an unify code to access unknown types as fastest and with the minimum allocations as possible. + +### Example +Given the following scenario, where we want to access the data from an anonymous class instance in another method, a code example to do that would be: + +```csharp +public class Program +{ + public static void Main() + { + // We create an anonymous object + var anonymousObject = new { Name = ".NET Core", Version = "3.1" }; + + Process(anonymousObject); + } + + public static void Process(object obj) + { + // First, we create a proxy instance using IDuckAnonymous type + // as a proxy definition and the obj instance as the target. + // Now the proxyInstance implements IDuckAnonymous type and all + // getters to access the anonymous object internals were generated + // automatically for us. + + // We use the `As` extension method to call the duck typing proxy creator. + var proxyInstance = obj.As(); + + // Here we can access the internal properties + Console.WriteLine($"Name: {proxyInstance.Name}"); + Console.WriteLine($"Version: {proxyInstance.Version}"); + } + + public interface IDuckAnonymous + { + string Name { get; } + string Version { get; } + } +} +``` + +For this particular case the generated proxy type by the Duck Type library will look like this: + +```csharp +public readonly struct IDuckAnonymous___<>f__AnonymousType0`2[System.String,System.String] : IDuckAnonymous, IDuckType +{ + private readonly <>f__AnonymousType0`2[System.String,System.String] _currentInstance; + + // *** IDuckType implementation + public object Instance => _currentInstance; + public Type Type => typeof(<>f__AnonymousType0`2[System.String,System.String]); + + // *** IDuckAnonymous implementation + public string Name => _currentInstance.Name; + public string Version => _currentInstance.Version; +} +``` + +## Proxy types +Depending on the proxy definition type, the library will create different proxy types as shown in the following table: + +| Proxy Definition Type | Resulting Proxy Type | +|----------------------------|--------------------------------------------------| +| Interface | Struct implementing the interface | +| Abstract class | Class inherits and overriding the abstract class | +| Class with virtual members | Class inherits and overriding the virtual class | + +Also, all resulting proxy types implements the `IDuckType` interface to expose the target instance and type. The interface has the following definition: + +```csharp +public interface IDuckType +{ + /// + /// Gets instance + /// + object Instance { get; } + + /// + /// Gets instance Type + /// + Type Type { get; } +} +``` + +## Controlling bindings + +To control the bindings between the proxy definition and the target type, we can make use of the `DuckAttribute` attribute type defined as: + +```csharp +/// +/// Duck attribute +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false)] +public class DuckAttribute : Attribute +{ + /// + /// Gets or sets the underlying type member name + /// + public string Name { get; set; } + + /// + /// Gets or sets duck kind + /// + public DuckKind Kind { get; set; } = DuckKind.Property; + + /// + /// Gets or sets the generic parameter type names definition for a generic method call (required when calling generic methods and instance type is non public) + /// + public string[] GenericParameterTypeNames { get; set; } + + /// + /// Gets or sets the parameter type names of the target method (optional / used to disambiguation) + /// + public string[] ParameterTypeNames { get; set; } +} + +/// +/// Duck attribute where the underlying member is a field +/// +public class DuckFieldAttribute : DuckAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public DuckFieldAttribute() + { + Kind = DuckKind.Field; + } +} + +/// +/// Duck kind +/// +public enum DuckKind +{ + /// + /// Property + /// + Property, + + /// + /// Field + /// + Field +} +``` + +This attribute can be used in `Properties` or `Methods` in the proxy definition and is used by the library to select the members we want to access. + +### Example + +The following example shows multiple uses of the `DuckAttribute`: + +```csharp +public interface IMyProxy +{ + // *** + // *** Field binding + // *** + + [DuckField(Name = "_sampleStaticField")] + string MyStaticField { get; } + + [DuckField(Name = "_normalField")] + int NormalFieldWithGetterAndSetter { get; set; } + + + + // *** + // *** Property binding (by default the Kind is Property so we can ignore it) + // *** + + [Duck(Name = "Instance")] + string MyStaticInstanceProperty { get; } + + // If we don´t use the duck attribute it will try to find a property with the same name in the target type + string MyProperty { get; set; } + + // If the original property returns a type that we don´t have at compile time, + // we can pass a proxy definition and that original value will be wrapped with the proxy + // as well (DuckType chaining). + IObscureObject CustomData { get; } + + // Indexers are supported as well + string this[string index] { get; set; } + + + + // *** + // *** Method binding + // *** + + // support overloads of a method + void Add(string name, int obj); + void Add(string name, string obj = "none"); + + // In order to resolve an ambiguity of a call we can specify the parameters we want to match. + [Duck(ParameterTypeNames = new string[] { "System.String", "MyNamespace.MyType, MyAssembly" })] + void Add(string name, object obj); + + // We can also use the name to map to another method name. + [Duck(Name = "InternalGetReference")] + void GetReference(ref int value); + + // The Proxy definition can support generics only if the instance type + // is *Public* + Tuple Wrap(T1 a, T2 b); + + // If we need to call a generic method from a non public type + // we can use the DuckAttribute as well: + [Duck(Name = "Wrap", GenericParameterTypeNames = new[] { "System.Int32", "System.String" })] + Tuple WrapIntString(int a, string b); + + + // If the original call returns or uses a parameter that we don´t have at compile time, + // we can pass a proxy definition and that original value will be wrapped with the proxy + // as well (DuckType chaining). + bool TryGetPrivateObscure(out IObscureObject obj); +} +``` + + +## Accessor modifiers (AM) + +In order to support all accessor modifiers for: instance types, parameters and return value types, the Duck Type library applies some `tricks` to avoid the visibility checks. This is done automatically when the library is creating the proxy type. In summary the following logic is applied depending on each case: + +| Target Type AM | Target Member Type | Target Member AM | Access Method | +|------------------------------|--------------------|------------------------------|-----------------------------------------------| +| Public | Field | Public | Direct | +| Public | Field | Private, Protected, Internal | through DynamicMethod | +| Public | Property | Public | Direct | +| Public | Property | Private, Protected, Internal | Direct using function pointers (Calli opcode) | +| Public | Method | Public | Direct | +| Public | Method | Private, Protected, Internal | Direct using function pointers (Calli opcode) | +| Private, Protected, Internal | Field | Public | through DynamicMethod delegate | +| Private, Protected, Internal | Field | Private, Protected, Internal | through DynamicMethod delegate | +| Private, Protected, Internal | Property | Public | through DynamicMethod delegate | +| Private, Protected, Internal | Property | Private, Protected, Internal | through DynamicMethod delegate | +| Private, Protected, Internal | Method | Public | through DynamicMethod delegate | +| Private, Protected, Internal | Method | Private, Protected, Internal | through DynamicMethod delegate | + +## Generics methods + +Calling generics methods are supported by the library, but due the use of `DynamicMethod`in some scenarios we can´t define it as a normal generic method (Please note if we know that the target type is Public, then we can declare a normal generic method). + +For those scenarios were we can´t use the generic definition, the alternative is to specify the generic parameters to be match in the duck attribute of the proxy definition. + + +### Example with public type + +Consider this public target type: + +```csharp +public class PublicType +{ + public Tuple Wrap(T1 a, T2 b) + { + return Tuple.Create(default, default); + } +} +``` + +Because the type is public we can define a Generic proxy method: + +```csharp +public interface IMyProxy +{ + Tuple Wrap(T1 a, T2 b); +} +``` + +### Example with non public type + +Consider this private target type: + +```csharp +private class PrivateType +{ + public Tuple Wrap(T1 a, T2 b) + { + return Tuple.Create(default, default); + } +} +``` + +Because the type is non public we have to define each generic arguments we want to use in different calls. + +```csharp +public interface IMyProxy +{ + // We have to define all kind of call we want to make to the target instance + + [Duck(Name = "Wrap", GenericParameterTypeNames = new[] { "System.Int32", "System.String" })] + Tuple WrapIntString(int a, string b); + + [Duck(Name = "Wrap", GenericParameterTypeNames = new[] { "System.Int32", "System.Int32" })] + Tuple WrapIntInt(int a, int b); +} +``` + +## Duck chaining + +Duck chaining enables the possibility to interact with properties or methods returning or using non public type parameters to be wrapped with a new duck type proxy, so we can access the internals of those objects. + +### Example + +Consider the following types: + +```csharp +public class MyHandler +{ + public string Name { get; set; } + + internal MyHandlerConfiguration Configuration { get; } +} + +internal class MyHandlerConfiguration +{ + public int MaxConnections { get; set; } +} +``` + +We can write the following proxy definitions to get access to all the data: + +```csharp +public interface IProxyMyHandler +{ + string Name { get; set; } + + IProxyMyHandlerConfiguration Configuration { get; } +} +public interface IProxyMyHandlerConfiguration +{ + int MaxConnections { get; set; } +} +``` + +In this example the non public instance of `MyHandlerConfiguration` when calling the `Configuration` property is going to be wrapped with a `IProxyMyHandlerConfiguration` instance automatically. That allow us to access the internal data of that non public type. + +## Benchmarks + +Several benchmark tests were run for multiple cases to keep track of the time execution and heap allocations of the library, these are the results: + +### Fields Getter and Setter + +The `proxy` column indicates the target type access modifier. + +Tests with `blank` proxy are the direct access of the value through an interface without creating any ducktype proxy. + +``` ini +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.508 (2004/?/20H1) +Intel Core i7-1068NG7 CPU 2.30GHz, 1 CPU, 2 logical and 2 physical cores +.NET Core SDK=3.1.402 + [Host] : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT + Job-LJAVIR : .NET Framework 4.8 (4.8.4220.0), X64 RyuJIT + Job-OHOUFK : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT +``` +| Method | Runtime | Categories | proxy | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|------------------------ |-------------- |-------------- |--------- |---------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| +| GetPublicStaticField | .NET 4.7.2 | Static Getter | Internal | 3.207 ns | 0.0623 ns | 0.0553 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicStaticField | .NET Core 3.1 | Static Getter | Internal | 4.052 ns | 0.0748 ns | 0.0700 ns | 1.27 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalStaticField | .NET 4.7.2 | Static Getter | Internal | 3.233 ns | 0.0774 ns | 0.0724 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalStaticField | .NET Core 3.1 | Static Getter | Internal | 3.987 ns | 0.0723 ns | 0.0677 ns | 1.23 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedStaticField | .NET 4.7.2 | Static Getter | Internal | 3.229 ns | 0.0607 ns | 0.0567 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedStaticField | .NET Core 3.1 | Static Getter | Internal | 4.004 ns | 0.1010 ns | 0.0895 ns | 1.24 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateStaticField | .NET 4.7.2 | Static Getter | Internal | 3.209 ns | 0.0288 ns | 0.0255 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateStaticField | .NET Core 3.1 | Static Getter | Internal | 4.274 ns | 0.0539 ns | 0.0421 ns | 1.33 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicStaticField | .NET 4.7.2 | Static Setter | Internal | 2.941 ns | 0.0507 ns | 0.0423 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicStaticField | .NET Core 3.1 | Static Setter | Internal | 3.966 ns | 0.0788 ns | 0.0737 ns | 1.34 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalStaticField | .NET 4.7.2 | Static Setter | Internal | 2.952 ns | 0.0671 ns | 0.0627 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalStaticField | .NET Core 3.1 | Static Setter | Internal | 3.973 ns | 0.0804 ns | 0.0752 ns | 1.35 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedStaticField | .NET 4.7.2 | Static Setter | Internal | 2.880 ns | 0.0472 ns | 0.0394 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedStaticField | .NET Core 3.1 | Static Setter | Internal | 3.872 ns | 0.0359 ns | 0.0300 ns | 1.34 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateStaticField | .NET 4.7.2 | Static Setter | Internal | 2.945 ns | 0.0625 ns | 0.0585 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateStaticField | .NET Core 3.1 | Static Setter | Internal | 3.966 ns | 0.0846 ns | 0.0791 ns | 1.35 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicField | .NET 4.7.2 | Getter | Internal | 3.405 ns | 0.0599 ns | 0.0531 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicField | .NET Core 3.1 | Getter | Internal | 3.229 ns | 0.0571 ns | 0.0477 ns | 0.95 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalField | .NET 4.7.2 | Getter | Internal | 3.400 ns | 0.0465 ns | 0.0389 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalField | .NET Core 3.1 | Getter | Internal | 3.549 ns | 0.0657 ns | 0.0615 ns | 1.05 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedField | .NET 4.7.2 | Getter | Internal | 3.384 ns | 0.0459 ns | 0.0383 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedField | .NET Core 3.1 | Getter | Internal | 3.278 ns | 0.0503 ns | 0.0392 ns | 0.97 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateField | .NET 4.7.2 | Getter | Internal | 3.425 ns | 0.0545 ns | 0.0510 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateField | .NET Core 3.1 | Getter | Internal | 3.282 ns | 0.0761 ns | 0.0675 ns | 0.96 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicField | .NET 4.7.2 | Setter | Internal | 3.392 ns | 0.0649 ns | 0.0575 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicField | .NET Core 3.1 | Setter | Internal | 3.164 ns | 0.0509 ns | 0.0451 ns | 0.93 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalField | .NET 4.7.2 | Setter | Internal | 3.422 ns | 0.0727 ns | 0.0680 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalField | .NET Core 3.1 | Setter | Internal | 3.166 ns | 0.0430 ns | 0.0359 ns | 0.92 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedField | .NET 4.7.2 | Setter | Internal | 3.384 ns | 0.0392 ns | 0.0306 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedField | .NET Core 3.1 | Setter | Internal | 3.180 ns | 0.0520 ns | 0.0486 ns | 0.94 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateField | .NET 4.7.2 | Setter | Internal | 3.392 ns | 0.0548 ns | 0.0485 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateField | .NET Core 3.1 | Setter | Internal | 3.155 ns | 0.0300 ns | 0.0234 ns | 0.93 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicStaticField | .NET 4.7.2 | Static Getter | Private | 3.163 ns | 0.0390 ns | 0.0326 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicStaticField | .NET Core 3.1 | Static Getter | Private | 4.310 ns | 0.0717 ns | 0.0671 ns | 1.36 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalStaticField | .NET 4.7.2 | Static Getter | Private | 3.220 ns | 0.0737 ns | 0.0689 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalStaticField | .NET Core 3.1 | Static Getter | Private | 3.999 ns | 0.0327 ns | 0.0255 ns | 1.24 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedStaticField | .NET 4.7.2 | Static Getter | Private | 3.198 ns | 0.0458 ns | 0.0383 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedStaticField | .NET Core 3.1 | Static Getter | Private | 4.082 ns | 0.0989 ns | 0.0925 ns | 1.27 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateStaticField | .NET 4.7.2 | Static Getter | Private | 3.185 ns | 0.0336 ns | 0.0280 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateStaticField | .NET Core 3.1 | Static Getter | Private | 3.993 ns | 0.0543 ns | 0.0453 ns | 1.25 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicStaticField | .NET 4.7.2 | Static Setter | Private | 2.928 ns | 0.0462 ns | 0.0386 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicStaticField | .NET Core 3.1 | Static Setter | Private | 3.951 ns | 0.0857 ns | 0.0760 ns | 1.35 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalStaticField | .NET 4.7.2 | Static Setter | Private | 3.006 ns | 0.0861 ns | 0.0805 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalStaticField | .NET Core 3.1 | Static Setter | Private | 3.986 ns | 0.0890 ns | 0.0833 ns | 1.33 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedStaticField | .NET 4.7.2 | Static Setter | Private | 2.916 ns | 0.0256 ns | 0.0214 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedStaticField | .NET Core 3.1 | Static Setter | Private | 3.937 ns | 0.0509 ns | 0.0425 ns | 1.35 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateStaticField | .NET 4.7.2 | Static Setter | Private | 3.003 ns | 0.0684 ns | 0.0639 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateStaticField | .NET Core 3.1 | Static Setter | Private | 3.951 ns | 0.0954 ns | 0.0846 ns | 1.32 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicField | .NET 4.7.2 | Getter | Private | 3.408 ns | 0.0499 ns | 0.0417 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicField | .NET Core 3.1 | Getter | Private | 3.243 ns | 0.0506 ns | 0.0448 ns | 0.95 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalField | .NET 4.7.2 | Getter | Private | 3.419 ns | 0.0360 ns | 0.0301 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalField | .NET Core 3.1 | Getter | Private | 3.276 ns | 0.0638 ns | 0.0565 ns | 0.96 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedField | .NET 4.7.2 | Getter | Private | 3.428 ns | 0.0688 ns | 0.0644 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedField | .NET Core 3.1 | Getter | Private | 3.260 ns | 0.0414 ns | 0.0367 ns | 0.95 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateField | .NET 4.7.2 | Getter | Private | 3.533 ns | 0.0972 ns | 0.1264 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateField | .NET Core 3.1 | Getter | Private | 3.275 ns | 0.0576 ns | 0.0511 ns | 0.92 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicField | .NET 4.7.2 | Setter | Private | 3.437 ns | 0.0919 ns | 0.0903 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicField | .NET Core 3.1 | Setter | Private | 3.151 ns | 0.0520 ns | 0.0461 ns | 0.92 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalField | .NET 4.7.2 | Setter | Private | 3.384 ns | 0.0533 ns | 0.0445 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalField | .NET Core 3.1 | Setter | Private | 3.156 ns | 0.0327 ns | 0.0273 ns | 0.93 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedField | .NET 4.7.2 | Setter | Private | 3.368 ns | 0.0396 ns | 0.0331 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedField | .NET Core 3.1 | Setter | Private | 2.953 ns | 0.0264 ns | 0.0221 ns | 0.88 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateField | .NET 4.7.2 | Setter | Private | 3.353 ns | 0.0471 ns | 0.0393 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateField | .NET Core 3.1 | Setter | Private | 3.160 ns | 0.0592 ns | 0.0525 ns | 0.94 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicStaticField | .NET 4.7.2 | Static Getter | Public | 2.433 ns | 0.0616 ns | 0.0576 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicStaticField | .NET Core 3.1 | Static Getter | Public | 2.191 ns | 0.0497 ns | 0.0465 ns | 0.90 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalStaticField | .NET 4.7.2 | Static Getter | Public | 3.189 ns | 0.0460 ns | 0.0384 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalStaticField | .NET Core 3.1 | Static Getter | Public | 3.964 ns | 0.0426 ns | 0.0332 ns | 1.24 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedStaticField | .NET 4.7.2 | Static Getter | Public | 3.201 ns | 0.0625 ns | 0.0522 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedStaticField | .NET Core 3.1 | Static Getter | Public | 3.998 ns | 0.0359 ns | 0.0300 ns | 1.25 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateStaticField | .NET 4.7.2 | Static Getter | Public | 3.220 ns | 0.0675 ns | 0.0631 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateStaticField | .NET Core 3.1 | Static Getter | Public | 4.068 ns | 0.0766 ns | 0.0717 ns | 1.26 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicStaticField | .NET 4.7.2 | Static Setter | Public | 2.175 ns | 0.0506 ns | 0.0474 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicStaticField | .NET Core 3.1 | Static Setter | Public | 2.088 ns | 0.0281 ns | 0.0235 ns | 0.96 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalStaticField | .NET 4.7.2 | Static Setter | Public | 2.972 ns | 0.0674 ns | 0.0598 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalStaticField | .NET Core 3.1 | Static Setter | Public | 3.966 ns | 0.0569 ns | 0.0504 ns | 1.34 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedStaticField | .NET 4.7.2 | Static Setter | Public | 2.936 ns | 0.0451 ns | 0.0376 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedStaticField | .NET Core 3.1 | Static Setter | Public | 3.923 ns | 0.0517 ns | 0.0432 ns | 1.34 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateStaticField | .NET 4.7.2 | Static Setter | Public | 2.957 ns | 0.0740 ns | 0.0656 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateStaticField | .NET Core 3.1 | Static Setter | Public | 3.933 ns | 0.0221 ns | 0.0172 ns | 1.33 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicField | .NET 4.7.2 | Getter | Public | 2.326 ns | 0.0442 ns | 0.0369 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicField | .NET Core 3.1 | Getter | Public | 2.421 ns | 0.0646 ns | 0.0573 ns | 1.04 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalField | .NET 4.7.2 | Getter | Public | 3.418 ns | 0.0579 ns | 0.0483 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalField | .NET Core 3.1 | Getter | Public | 3.251 ns | 0.0490 ns | 0.0409 ns | 0.95 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedField | .NET 4.7.2 | Getter | Public | 3.424 ns | 0.0471 ns | 0.0393 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedField | .NET Core 3.1 | Getter | Public | 3.232 ns | 0.0316 ns | 0.0264 ns | 0.94 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateField | .NET 4.7.2 | Getter | Public | 3.384 ns | 0.0300 ns | 0.0251 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateField | .NET Core 3.1 | Getter | Public | 3.338 ns | 0.0897 ns | 0.0839 ns | 0.98 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicField | .NET 4.7.2 | Setter | Public | 2.078 ns | 0.0375 ns | 0.0333 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicField | .NET Core 3.1 | Setter | Public | 2.396 ns | 0.0510 ns | 0.0477 ns | 1.15 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalField | .NET 4.7.2 | Setter | Public | 3.402 ns | 0.0424 ns | 0.0354 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalField | .NET Core 3.1 | Setter | Public | 3.146 ns | 0.0248 ns | 0.0207 ns | 0.92 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedField | .NET 4.7.2 | Setter | Public | 3.368 ns | 0.0443 ns | 0.0370 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedField | .NET Core 3.1 | Setter | Public | 3.167 ns | 0.0242 ns | 0.0189 ns | 0.94 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateField | .NET 4.7.2 | Setter | Public | 3.392 ns | 0.0563 ns | 0.0499 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateField | .NET Core 3.1 | Setter | Public | 3.171 ns | 0.0663 ns | 0.0621 ns | 0.94 | 0.02 | - | - | - | - | + +### Properties Getter and Setter + +The `proxy` column indicates the target type access modifier. + +Tests with `blank` proxy are the direct access of the value through an interface without creating any ducktype proxy. + +``` ini +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.508 (2004/?/20H1) +Intel Core i7-1068NG7 CPU 2.30GHz, 1 CPU, 2 logical and 2 physical cores +.NET Core SDK=3.1.402 + [Host] : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT + Job-QIOTKF : .NET Framework 4.8 (4.8.4220.0), X64 RyuJIT + Job-FEKUVA : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT +``` +| Method | Runtime | Categories | proxy | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|--------------------------- |-------------- |--------------- |--------- |---------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| +| GetPublicProperty | .NET 4.7.2 | Getter | | 1.046 ns | 0.0457 ns | 0.0449 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicProperty | .NET Core 3.1 | Getter | | 1.300 ns | 0.0329 ns | 0.0292 ns | 1.24 | 0.05 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicProperty | .NET 4.7.2 | Setter | | 1.283 ns | 0.0225 ns | 0.0188 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicProperty | .NET Core 3.1 | Setter | | 1.048 ns | 0.0262 ns | 0.0232 ns | 0.82 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetIndexerProperty | .NET 4.7.2 | Indexer Getter | | 1.027 ns | 0.0271 ns | 0.0240 ns | 1.00 | 0.00 | - | - | - | - | +| GetIndexerProperty | .NET Core 3.1 | Indexer Getter | | 1.014 ns | 0.0435 ns | 0.0407 ns | 0.99 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetIndexerProperty | .NET 4.7.2 | Indexer Setter | | 1.303 ns | 0.0366 ns | 0.0324 ns | 1.00 | 0.00 | - | - | - | - | +| SetIndexerProperty | .NET Core 3.1 | Indexer Setter | | 1.315 ns | 0.0383 ns | 0.0358 ns | 1.01 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicStaticProperty | .NET 4.7.2 | Static Getter | Internal | 2.849 ns | 0.0529 ns | 0.0442 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicStaticProperty | .NET Core 3.1 | Static Getter | Internal | 4.029 ns | 0.0897 ns | 0.0839 ns | 1.42 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalStaticProperty | .NET 4.7.2 | Static Getter | Internal | 2.888 ns | 0.0606 ns | 0.0567 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalStaticProperty | .NET Core 3.1 | Static Getter | Internal | 4.263 ns | 0.0799 ns | 0.0708 ns | 1.48 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedStaticProperty | .NET 4.7.2 | Static Getter | Internal | 2.855 ns | 0.0372 ns | 0.0311 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedStaticProperty | .NET Core 3.1 | Static Getter | Internal | 4.277 ns | 0.0655 ns | 0.0612 ns | 1.50 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateStaticProperty | .NET 4.7.2 | Static Getter | Internal | 2.911 ns | 0.0726 ns | 0.0679 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateStaticProperty | .NET Core 3.1 | Static Getter | Internal | 4.030 ns | 0.0721 ns | 0.0675 ns | 1.39 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicStaticProperty | .NET 4.7.2 | Static Setter | Internal | 3.221 ns | 0.0757 ns | 0.0632 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicStaticProperty | .NET Core 3.1 | Static Setter | Internal | 3.955 ns | 0.0575 ns | 0.0481 ns | 1.23 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalStaticProperty | .NET 4.7.2 | Static Setter | Internal | 3.174 ns | 0.0334 ns | 0.0279 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalStaticProperty | .NET Core 3.1 | Static Setter | Internal | 4.212 ns | 0.0608 ns | 0.0507 ns | 1.33 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedStaticProperty | .NET 4.7.2 | Static Setter | Internal | 3.247 ns | 0.0846 ns | 0.1100 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedStaticProperty | .NET Core 3.1 | Static Setter | Internal | 4.190 ns | 0.0312 ns | 0.0243 ns | 1.27 | 0.05 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateStaticProperty | .NET 4.7.2 | Static Setter | Internal | 3.205 ns | 0.0714 ns | 0.0668 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateStaticProperty | .NET Core 3.1 | Static Setter | Internal | 4.249 ns | 0.0814 ns | 0.0721 ns | 1.32 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicProperty | .NET 4.7.2 | Getter | Internal | 3.241 ns | 0.0758 ns | 0.0672 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicProperty | .NET Core 3.1 | Getter | Internal | 3.257 ns | 0.0730 ns | 0.0683 ns | 1.01 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalProperty | .NET 4.7.2 | Getter | Internal | 3.239 ns | 0.0467 ns | 0.0390 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalProperty | .NET Core 3.1 | Getter | Internal | 3.256 ns | 0.0570 ns | 0.0476 ns | 1.01 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedProperty | .NET 4.7.2 | Getter | Internal | 3.290 ns | 0.0504 ns | 0.0447 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedProperty | .NET Core 3.1 | Getter | Internal | 3.350 ns | 0.0911 ns | 0.0935 ns | 1.02 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateProperty | .NET 4.7.2 | Getter | Internal | 3.259 ns | 0.0609 ns | 0.0540 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateProperty | .NET Core 3.1 | Getter | Internal | 3.243 ns | 0.0814 ns | 0.0722 ns | 1.00 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicProperty | .NET 4.7.2 | Setter | Internal | 3.192 ns | 0.0749 ns | 0.0664 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicProperty | .NET Core 3.1 | Setter | Internal | 2.912 ns | 0.0481 ns | 0.0401 ns | 0.91 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalProperty | .NET 4.7.2 | Setter | Internal | 3.211 ns | 0.0482 ns | 0.0427 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalProperty | .NET Core 3.1 | Setter | Internal | 2.914 ns | 0.0401 ns | 0.0335 ns | 0.91 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedProperty | .NET 4.7.2 | Setter | Internal | 3.176 ns | 0.0477 ns | 0.0399 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedProperty | .NET Core 3.1 | Setter | Internal | 2.890 ns | 0.0454 ns | 0.0379 ns | 0.91 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateProperty | .NET 4.7.2 | Setter | Internal | 3.170 ns | 0.0466 ns | 0.0413 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateProperty | .NET Core 3.1 | Setter | Internal | 2.948 ns | 0.0720 ns | 0.0674 ns | 0.93 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetIndexerProperty | .NET 4.7.2 | Indexer Getter | Internal | 3.274 ns | 0.0724 ns | 0.0642 ns | 1.00 | 0.00 | - | - | - | - | +| GetIndexerProperty | .NET Core 3.1 | Indexer Getter | Internal | 3.282 ns | 0.0818 ns | 0.0765 ns | 1.00 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| SetIndexerProperty | .NET 4.7.2 | Indexer Setter | Internal | 3.087 ns | 0.0686 ns | 0.0674 ns | 1.00 | 0.00 | - | - | - | - | +| SetIndexerProperty | .NET Core 3.1 | Indexer Setter | Internal | 3.184 ns | 0.0323 ns | 0.0252 ns | 1.03 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicStaticProperty | .NET 4.7.2 | Static Getter | Private | 2.876 ns | 0.0561 ns | 0.0497 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicStaticProperty | .NET Core 3.1 | Static Getter | Private | 4.013 ns | 0.0914 ns | 0.0810 ns | 1.40 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalStaticProperty | .NET 4.7.2 | Static Getter | Private | 2.851 ns | 0.0339 ns | 0.0283 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalStaticProperty | .NET Core 3.1 | Static Getter | Private | 4.019 ns | 0.0669 ns | 0.0559 ns | 1.41 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedStaticProperty | .NET 4.7.2 | Static Getter | Private | 2.838 ns | 0.0373 ns | 0.0311 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedStaticProperty | .NET Core 3.1 | Static Getter | Private | 4.035 ns | 0.0962 ns | 0.0900 ns | 1.42 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateStaticProperty | .NET 4.7.2 | Static Getter | Private | 2.918 ns | 0.0825 ns | 0.0731 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateStaticProperty | .NET Core 3.1 | Static Getter | Private | 4.134 ns | 0.0657 ns | 0.0614 ns | 1.42 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicStaticProperty | .NET 4.7.2 | Static Setter | Private | 3.219 ns | 0.0472 ns | 0.0418 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicStaticProperty | .NET Core 3.1 | Static Setter | Private | 3.989 ns | 0.0664 ns | 0.0588 ns | 1.24 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalStaticProperty | .NET 4.7.2 | Static Setter | Private | 3.271 ns | 0.0901 ns | 0.0843 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalStaticProperty | .NET Core 3.1 | Static Setter | Private | 4.304 ns | 0.0783 ns | 0.0694 ns | 1.32 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedStaticProperty | .NET 4.7.2 | Static Setter | Private | 3.241 ns | 0.0724 ns | 0.0677 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedStaticProperty | .NET Core 3.1 | Static Setter | Private | 4.180 ns | 0.0391 ns | 0.0305 ns | 1.29 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateStaticProperty | .NET 4.7.2 | Static Setter | Private | 3.161 ns | 0.0371 ns | 0.0310 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateStaticProperty | .NET Core 3.1 | Static Setter | Private | 4.186 ns | 0.0527 ns | 0.0412 ns | 1.33 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicProperty | .NET 4.7.2 | Getter | Private | 3.245 ns | 0.0466 ns | 0.0389 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicProperty | .NET Core 3.1 | Getter | Private | 3.320 ns | 0.0889 ns | 0.0988 ns | 1.03 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalProperty | .NET 4.7.2 | Getter | Private | 3.237 ns | 0.0475 ns | 0.0371 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalProperty | .NET Core 3.1 | Getter | Private | 3.301 ns | 0.0874 ns | 0.0972 ns | 1.01 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedProperty | .NET 4.7.2 | Getter | Private | 3.255 ns | 0.0309 ns | 0.0241 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedProperty | .NET Core 3.1 | Getter | Private | 3.295 ns | 0.0436 ns | 0.0340 ns | 1.01 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateProperty | .NET 4.7.2 | Getter | Private | 3.280 ns | 0.0656 ns | 0.0614 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateProperty | .NET Core 3.1 | Getter | Private | 3.309 ns | 0.0864 ns | 0.0766 ns | 1.01 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicProperty | .NET 4.7.2 | Setter | Private | 3.205 ns | 0.0679 ns | 0.0635 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicProperty | .NET Core 3.1 | Setter | Private | 2.959 ns | 0.0682 ns | 0.0605 ns | 0.93 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalProperty | .NET 4.7.2 | Setter | Private | 3.184 ns | 0.0479 ns | 0.0400 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalProperty | .NET Core 3.1 | Setter | Private | 2.943 ns | 0.0632 ns | 0.0592 ns | 0.92 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedProperty | .NET 4.7.2 | Setter | Private | 3.247 ns | 0.0927 ns | 0.1206 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedProperty | .NET Core 3.1 | Setter | Private | 2.934 ns | 0.0608 ns | 0.0539 ns | 0.89 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateProperty | .NET 4.7.2 | Setter | Private | 3.182 ns | 0.0711 ns | 0.0594 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateProperty | .NET Core 3.1 | Setter | Private | 2.938 ns | 0.0562 ns | 0.0499 ns | 0.92 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetIndexerProperty | .NET 4.7.2 | Indexer Getter | Private | 3.334 ns | 0.0826 ns | 0.0772 ns | 1.00 | 0.00 | - | - | - | - | +| GetIndexerProperty | .NET Core 3.1 | Indexer Getter | Private | 3.523 ns | 0.0732 ns | 0.0611 ns | 1.05 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetIndexerProperty | .NET 4.7.2 | Indexer Setter | Private | 3.099 ns | 0.0479 ns | 0.0374 ns | 1.00 | 0.00 | - | - | - | - | +| SetIndexerProperty | .NET Core 3.1 | Indexer Setter | Private | 3.167 ns | 0.0535 ns | 0.0447 ns | 1.02 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicStaticProperty | .NET 4.7.2 | Static Getter | Public | 2.085 ns | 0.0345 ns | 0.0288 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicStaticProperty | .NET Core 3.1 | Static Getter | Public | 2.159 ns | 0.0326 ns | 0.0255 ns | 1.03 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalStaticProperty | .NET 4.7.2 | Static Getter | Public | 2.969 ns | 0.0492 ns | 0.0384 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalStaticProperty | .NET Core 3.1 | Static Getter | Public | 3.197 ns | 0.0484 ns | 0.0404 ns | 1.08 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedStaticProperty | .NET 4.7.2 | Static Getter | Public | 2.935 ns | 0.0434 ns | 0.0363 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedStaticProperty | .NET Core 3.1 | Static Getter | Public | 3.234 ns | 0.0757 ns | 0.0632 ns | 1.10 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateStaticProperty | .NET 4.7.2 | Static Getter | Public | 3.003 ns | 0.0723 ns | 0.0676 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateStaticProperty | .NET Core 3.1 | Static Getter | Public | 3.024 ns | 0.0561 ns | 0.0525 ns | 1.01 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicStaticProperty | .NET 4.7.2 | Static Setter | Public | 2.443 ns | 0.0405 ns | 0.0359 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicStaticProperty | .NET Core 3.1 | Static Setter | Public | 2.073 ns | 0.0400 ns | 0.0334 ns | 0.85 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalStaticProperty | .NET 4.7.2 | Static Setter | Public | 3.109 ns | 0.0536 ns | 0.0448 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalStaticProperty | .NET Core 3.1 | Static Setter | Public | 3.174 ns | 0.0548 ns | 0.0486 ns | 1.02 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedStaticProperty | .NET 4.7.2 | Static Setter | Public | 3.120 ns | 0.0602 ns | 0.0503 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedStaticProperty | .NET Core 3.1 | Static Setter | Public | 3.194 ns | 0.0629 ns | 0.0557 ns | 1.02 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateStaticProperty | .NET 4.7.2 | Static Setter | Public | 3.161 ns | 0.0777 ns | 0.0727 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateStaticProperty | .NET Core 3.1 | Static Setter | Public | 3.194 ns | 0.0502 ns | 0.0419 ns | 1.01 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPublicProperty | .NET 4.7.2 | Getter | Public | 2.214 ns | 0.0432 ns | 0.0383 ns | 1.00 | 0.00 | - | - | - | - | +| GetPublicProperty | .NET Core 3.1 | Getter | Public | 2.215 ns | 0.0705 ns | 0.0692 ns | 1.00 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| GetInternalProperty | .NET 4.7.2 | Getter | Public | 2.869 ns | 0.0425 ns | 0.0355 ns | 1.00 | 0.00 | - | - | - | - | +| GetInternalProperty | .NET Core 3.1 | Getter | Public | 3.079 ns | 0.0644 ns | 0.0602 ns | 1.07 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetProtectedProperty | .NET 4.7.2 | Getter | Public | 2.876 ns | 0.0450 ns | 0.0376 ns | 1.00 | 0.00 | - | - | - | - | +| GetProtectedProperty | .NET Core 3.1 | Getter | Public | 3.033 ns | 0.0757 ns | 0.0671 ns | 1.06 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| GetPrivateProperty | .NET 4.7.2 | Getter | Public | 2.902 ns | 0.0455 ns | 0.0380 ns | 1.00 | 0.00 | - | - | - | - | +| GetPrivateProperty | .NET Core 3.1 | Getter | Public | 3.011 ns | 0.0552 ns | 0.0461 ns | 1.04 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPublicProperty | .NET 4.7.2 | Setter | Public | 2.377 ns | 0.0558 ns | 0.0522 ns | 1.00 | 0.00 | - | - | - | - | +| SetPublicProperty | .NET Core 3.1 | Setter | Public | 2.102 ns | 0.0543 ns | 0.0508 ns | 0.88 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetInternalProperty | .NET 4.7.2 | Setter | Public | 3.138 ns | 0.0822 ns | 0.0808 ns | 1.00 | 0.00 | - | - | - | - | +| SetInternalProperty | .NET Core 3.1 | Setter | Public | 2.898 ns | 0.0652 ns | 0.0578 ns | 0.92 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| SetProtectedProperty | .NET 4.7.2 | Setter | Public | 3.142 ns | 0.0820 ns | 0.0727 ns | 1.00 | 0.00 | - | - | - | - | +| SetProtectedProperty | .NET Core 3.1 | Setter | Public | 2.857 ns | 0.0366 ns | 0.0305 ns | 0.91 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| SetPrivateProperty | .NET 4.7.2 | Setter | Public | 3.145 ns | 0.0640 ns | 0.0567 ns | 1.00 | 0.00 | - | - | - | - | +| SetPrivateProperty | .NET Core 3.1 | Setter | Public | 2.865 ns | 0.0398 ns | 0.0332 ns | 0.91 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| GetIndexerProperty | .NET 4.7.2 | Indexer Getter | Public | 2.214 ns | 0.0256 ns | 0.0200 ns | 1.00 | 0.00 | - | - | - | - | +| GetIndexerProperty | .NET Core 3.1 | Indexer Getter | Public | 2.229 ns | 0.0704 ns | 0.0811 ns | 1.01 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| SetIndexerProperty | .NET 4.7.2 | Indexer Setter | Public | 2.316 ns | 0.0323 ns | 0.0270 ns | 1.00 | 0.00 | - | - | - | - | +| SetIndexerProperty | .NET Core 3.1 | Indexer Setter | Public | 2.360 ns | 0.0341 ns | 0.0302 ns | 1.02 | 0.01 | - | - | - | - | + +### Method Calls + +The `proxy` column indicates the target type access modifier. + +Tests with `blank` proxy are the direct access of the value through an interface without creating any ducktype proxy. + +``` ini +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.508 (2004/?/20H1) +Intel Core i7-1068NG7 CPU 2.30GHz, 1 CPU, 2 logical and 2 physical cores +.NET Core SDK=3.1.402 + [Host] : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT + Job-CGLGGF : .NET Framework 4.8 (4.8.4220.0), X64 RyuJIT + Job-GEDFFR : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT +``` +| Method | Runtime | Categories | proxy | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------------------------- |-------------- |----------------- |--------- |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| +| PublicVoidMethod | .NET 4.7.2 | Void Method | | 1.3457 ns | 0.0441 ns | 0.0391 ns | 1.00 | 0.00 | - | - | - | - | +| PublicVoidMethod | .NET Core 3.1 | Void Method | | 1.3195 ns | 0.0300 ns | 0.0266 ns | 0.98 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicMethod | .NET 4.7.2 | Method | | 0.7032 ns | 0.0647 ns | 0.0605 ns | 1.00 | 0.00 | - | - | - | - | +| PublicMethod | .NET Core 3.1 | Method | | 1.0446 ns | 0.0595 ns | 0.0557 ns | 1.50 | 0.16 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicOutParameterMethod | .NET 4.7.2 | Out-Param Method | | 2.2154 ns | 0.0305 ns | 0.0271 ns | 1.00 | 0.00 | - | - | - | - | +| PublicOutParameterMethod | .NET Core 3.1 | Out-Param Method | | 2.2246 ns | 0.0649 ns | 0.0607 ns | 1.01 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicVoidMethod | .NET 4.7.2 | Void Method | Internal | 3.3373 ns | 0.0401 ns | 0.0335 ns | 1.00 | 0.00 | - | - | - | - | +| PublicVoidMethod | .NET Core 3.1 | Void Method | Internal | 3.4379 ns | 0.0713 ns | 0.0632 ns | 1.03 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateVoidMethod | .NET 4.7.2 | Void Method | Internal | 3.3397 ns | 0.0408 ns | 0.0341 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateVoidMethod | .NET Core 3.1 | Void Method | Internal | 3.4176 ns | 0.0515 ns | 0.0456 ns | 1.02 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicMethod | .NET 4.7.2 | Method | Internal | 3.1409 ns | 0.0730 ns | 0.0647 ns | 1.00 | 0.00 | - | - | - | - | +| PublicMethod | .NET Core 3.1 | Method | Internal | 3.7283 ns | 0.0737 ns | 0.0689 ns | 1.19 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateMethod | .NET 4.7.2 | Method | Internal | 3.1578 ns | 0.1190 ns | 0.1113 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateMethod | .NET Core 3.1 | Method | Internal | 3.3137 ns | 0.1062 ns | 0.0942 ns | 1.05 | 0.05 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicOutParameterMethod | .NET 4.7.2 | Out-Param Method | Internal | 4.2402 ns | 0.0436 ns | 0.0340 ns | 1.00 | 0.00 | - | - | - | - | +| PublicOutParameterMethod | .NET Core 3.1 | Out-Param Method | Internal | 4.5497 ns | 0.1064 ns | 0.0995 ns | 1.07 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateOutParameterMethod | .NET 4.7.2 | Out-Param Method | Internal | 4.2640 ns | 0.0368 ns | 0.0307 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateOutParameterMethod | .NET Core 3.1 | Out-Param Method | Internal | 4.5834 ns | 0.0937 ns | 0.1003 ns | 1.07 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicVoidMethod | .NET 4.7.2 | Void Method | Private | 3.3801 ns | 0.0650 ns | 0.0576 ns | 1.00 | 0.00 | - | - | - | - | +| PublicVoidMethod | .NET Core 3.1 | Void Method | Private | 3.4223 ns | 0.0696 ns | 0.0651 ns | 1.01 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateVoidMethod | .NET 4.7.2 | Void Method | Private | 3.3691 ns | 0.0569 ns | 0.0532 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateVoidMethod | .NET Core 3.1 | Void Method | Private | 3.3862 ns | 0.0657 ns | 0.0549 ns | 1.00 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicMethod | .NET 4.7.2 | Method | Private | 3.0958 ns | 0.0645 ns | 0.0538 ns | 1.00 | 0.00 | - | - | - | - | +| PublicMethod | .NET Core 3.1 | Method | Private | 3.6469 ns | 0.0410 ns | 0.0320 ns | 1.18 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateMethod | .NET 4.7.2 | Method | Private | 3.1414 ns | 0.0910 ns | 0.0852 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateMethod | .NET Core 3.1 | Method | Private | 3.3168 ns | 0.0972 ns | 0.0910 ns | 1.06 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicOutParameterMethod | .NET 4.7.2 | Out-Param Method | Private | 4.2712 ns | 0.0519 ns | 0.0485 ns | 1.00 | 0.00 | - | - | - | - | +| PublicOutParameterMethod | .NET Core 3.1 | Out-Param Method | Private | 4.4966 ns | 0.0507 ns | 0.0423 ns | 1.05 | 0.01 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateOutParameterMethod | .NET 4.7.2 | Out-Param Method | Private | 4.2237 ns | 0.0299 ns | 0.0234 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateOutParameterMethod | .NET Core 3.1 | Out-Param Method | Private | 4.5464 ns | 0.0818 ns | 0.0765 ns | 1.08 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicVoidMethod | .NET 4.7.2 | Void Method | Public | 2.3150 ns | 0.0717 ns | 0.0767 ns | 1.00 | 0.00 | - | - | - | - | +| PublicVoidMethod | .NET Core 3.1 | Void Method | Public | 2.4284 ns | 0.0395 ns | 0.0330 ns | 1.04 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateVoidMethod | .NET 4.7.2 | Void Method | Public | 2.9836 ns | 0.0834 ns | 0.0780 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateVoidMethod | .NET Core 3.1 | Void Method | Public | 3.1669 ns | 0.0851 ns | 0.0796 ns | 1.06 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicMethod | .NET 4.7.2 | Method | Public | 1.7100 ns | 0.0664 ns | 0.0555 ns | 1.00 | 0.00 | - | - | - | - | +| PublicMethod | .NET Core 3.1 | Method | Public | 2.1847 ns | 0.0759 ns | 0.0710 ns | 1.27 | 0.06 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateMethod | .NET 4.7.2 | Method | Public | 2.7196 ns | 0.1125 ns | 0.1053 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateMethod | .NET Core 3.1 | Method | Public | 2.4933 ns | 0.0783 ns | 0.0694 ns | 0.91 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | | +| PublicOutParameterMethod | .NET 4.7.2 | Out-Param Method | Public | 2.9139 ns | 0.0538 ns | 0.0420 ns | 1.00 | 0.00 | - | - | - | - | +| PublicOutParameterMethod | .NET Core 3.1 | Out-Param Method | Public | 3.1691 ns | 0.0546 ns | 0.0456 ns | 1.09 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | | +| PrivateOutParameterMethod | .NET 4.7.2 | Out-Param Method | Public | 3.7001 ns | 0.0380 ns | 0.0318 ns | 1.00 | 0.00 | - | - | - | - | +| PrivateOutParameterMethod | .NET Core 3.1 | Out-Param Method | Public | 3.9932 ns | 0.0553 ns | 0.0490 ns | 1.08 | 0.01 | - | - | - | - | + diff --git a/src/Elastic.Apm.Profiler.Managed/DuckTyping/TypesTuple.cs b/src/Elastic.Apm.Profiler.Managed/DuckTyping/TypesTuple.cs new file mode 100644 index 000000000..879749377 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/DuckTyping/TypesTuple.cs @@ -0,0 +1,68 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.DuckTyping +{ + internal readonly struct TypesTuple : IEquatable + { + /// + /// The proxy definition type + /// + public readonly Type ProxyDefinitionType; + + /// + /// The target type + /// + public readonly Type TargetType; + + /// + /// Initializes a new instance of the struct. + /// + /// The proxy definition type + /// The target type + public TypesTuple(Type proxyDefinitionType, Type targetType) + { + ProxyDefinitionType = proxyDefinitionType; + TargetType = targetType; + } + + /// + /// Gets the struct hashcode + /// + /// Hashcode + public override int GetHashCode() + { + unchecked + { + var hash = (int)2166136261; + hash = (hash ^ ProxyDefinitionType.GetHashCode()) * 16777619; + hash = (hash ^ TargetType.GetHashCode()) * 16777619; + return hash; + } + } + + /// + /// Gets if the struct is equal to other object or struct + /// + /// Object to compare + /// True if both are equals; otherwise, false. + public override bool Equals(object obj) => + obj is TypesTuple vTuple && + ProxyDefinitionType == vTuple.ProxyDefinitionType && + TargetType == vTuple.TargetType; + + /// + public bool Equals(TypesTuple other) => + ProxyDefinitionType == other.ProxyDefinitionType && + TargetType == other.TargetType; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj b/src/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj new file mode 100644 index 000000000..d5d627396 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Elastic.Apm.Profiler.Managed.csproj @@ -0,0 +1,28 @@ + + + + net461;netstandard2.0;netcoreapp3.1 + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/AdoNetTypeNames.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/AdoNetTypeNames.cs new file mode 100644 index 000000000..8569ed81d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/AdoNetTypeNames.cs @@ -0,0 +1,201 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Apm.Profiler.Managed.Core; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + internal class InstrumentMySqlAttribute : InstrumentAttribute + { + public InstrumentMySqlAttribute() + { + Assembly = "MySql.Data"; + Type = "MySql.Data.MySqlClient.MySqlCommand"; + MinimumVersion = "6.7.0"; + MaximumVersion = "8.*.*"; + Group = "MySqlCommand"; + } + } + + internal class InstrumentNpgsqlAttribute : InstrumentAttribute + { + public InstrumentNpgsqlAttribute() + { + Assembly = "Npgsql"; + Type = "Npgsql.NpgsqlCommand"; + MinimumVersion = "4.0.0"; + MaximumVersion = "5.*.*"; + Group = "NpgsqlCommand"; + } + } + + internal class InstrumentOracleManagedDataAccessAttribute : InstrumentAttribute + { + public InstrumentOracleManagedDataAccessAttribute() + { + Assembly = "Oracle.ManagedDataAccess"; + Type = "Oracle.ManagedDataAccess.Client.OracleCommand"; + MinimumVersion = "4.122.0"; + MaximumVersion = "4.122.*"; + Group = "OracleCommand"; + } + } + + internal class InstrumentOracleManagedDataAccessCoreAttribute : InstrumentAttribute + { + public InstrumentOracleManagedDataAccessCoreAttribute() + { + Assembly = "Oracle.ManagedDataAccess"; + Type = "Oracle.ManagedDataAccess.Client.OracleCommand"; + MinimumVersion = "2.0.0"; + MaximumVersion = "2.*.*"; + Group = "OracleCommand"; + } + } + + internal class InstrumentMicrosoftDataSqliteAttribute : InstrumentAttribute + { + public InstrumentMicrosoftDataSqliteAttribute() + { + Assembly = "Microsoft.Data.Sqlite"; + Type = "Microsoft.Data.Sqlite.SqliteCommand"; + MinimumVersion = "2.0.0"; + MaximumVersion = "5.*.*"; + Group = "SqliteCommand"; + } + } + + internal class InstrumentSystemDataSqliteAttribute : InstrumentAttribute + { + public InstrumentSystemDataSqliteAttribute() + { + Assembly = "System.Data.SQLite"; + Type = "System.Data.SQLite.SQLiteCommand"; + MinimumVersion = "1.0.0"; + MaximumVersion = "2.*.*"; + Group = "SqliteCommand"; + } + } + + internal class InstrumentSystemDataSqlAttribute : InstrumentAttribute + { + public InstrumentSystemDataSqlAttribute() + { + Assembly = "System.Data"; + Type = "System.Data.SqlClient.SqlCommand"; + MinimumVersion = "4.0.0"; + MaximumVersion = "4.*.*"; + Group = "SqlCommand"; + } + } + + internal class InstrumentSystemDataSqlClientAttribute : InstrumentAttribute + { + public InstrumentSystemDataSqlClientAttribute() + { + Assembly = "System.Data.SqlClient"; + Type = "System.Data.SqlClient.SqlCommand"; + MinimumVersion = "4.0.0"; + MaximumVersion = "4.*.*"; + Group = "SqlCommand"; + } + } + + internal class InstrumentMicrosoftDataSqlClientAttribute : InstrumentAttribute + { + public InstrumentMicrosoftDataSqlClientAttribute() + { + Assembly = "Microsoft.Data.SqlClient"; + Type = "Microsoft.Data.SqlClient.SqlCommand"; + MinimumVersion = "1.0.0"; + MaximumVersion = "2.*.*"; + Group = "SqlCommand"; + } + } + + internal class InstrumentSystemDataAttribute : InstrumentAttribute + { + public InstrumentSystemDataAttribute() + { + Assembly = "System.Data"; + Type = "System.Data.Common.DbCommand"; + MinimumVersion = "4.0.0"; + MaximumVersion = "4.*.*"; + Group = "AdoNet"; + } + } + + internal class InstrumentSystemDataCommonAttribute : InstrumentAttribute + { + public InstrumentSystemDataCommonAttribute() + { + Assembly = "System.Data.Common"; + Type = "System.Data.Common.DbCommand"; + MinimumVersion = "4.0.0"; + MaximumVersion = "5.*.*"; + Group = "AdoNet"; + } + } + + internal class AdoNetTypeNames + { + public const string CommandBehavior = "System.Data.CommandBehavior"; + public const string DbDataReader = "System.Data.Common.DbDataReader"; + public const string TaskDbDataReader = "System.Threading.Tasks.Task`1"; + public const string TaskInt32 = "System.Threading.Tasks.Task`1"; + public const string TaskObject = "System.Threading.Tasks.Task`1"; + + public const string ExecuteNonQuery = nameof(ExecuteNonQuery); + public const string ExecuteNonQueryAsync = nameof(ExecuteNonQueryAsync); + public const string ExecuteScalar = nameof(ExecuteScalar); + public const string ExecuteScalarAsync = nameof(ExecuteScalarAsync); + public const string ExecuteReader = nameof(ExecuteReader); + public const string ExecuteReaderAsync = nameof(ExecuteReaderAsync); + public const string ExecuteDbDataReader = nameof(ExecuteDbDataReader); + public const string ExecuteDbDataReaderAsync = nameof(ExecuteDbDataReaderAsync); + + internal static class MySql + { + public const string DataReader = "MySql.Data.MySqlClient.MySqlDataReader"; + public const string TaskDataReader = "System.Threading.Tasks.Task`1"; + } + + internal static class Npgsql + { + public const string DataReader = "Npgsql.NpgsqlDataReader"; + public const string TaskDataReader = "System.Threading.Tasks.Task`1"; + } + + internal static class OracleManagedDataAccess + { + public const string DataReader = "Oracle.ManagedDataAccess.Client.OracleDataReader"; + public const string TaskDataReader = "System.Threading.Tasks.Task`1"; + } + + internal static class MicrosoftDataSqlite + { + public const string DataReader = "Microsoft.Data.Sqlite.SqliteDataReader"; + public const string TaskDataReader = "System.Threading.Tasks.Task`1"; + } + + internal static class SystemDataSqlite + { + public const string DataReader = "System.Data.SQLite.SQLiteDataReader"; + public const string TaskDataReader = "System.Threading.Tasks.Task`1"; + } + + internal static class SystemDataSqlServer + { + public const string DataReader = "System.Data.SqlClient.SqlDataReader"; + public const string TaskDataReader = "System.Threading.Tasks.Task`1"; + } + + internal static class MicrosoftDataSqlServer + { + public const string DataReader = "Microsoft.Data.SqlClient.SqlDataReader"; + public const string TaskDataReader = "System.Threading.Tasks.Task`1"; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryAsyncIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryAsyncIntegration.cs new file mode 100644 index 000000000..f2f0f3fd9 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryAsyncIntegration.cs @@ -0,0 +1,66 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using System.Threading; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// Task[int] [Command].ExecuteNonQueryAsync(CancellationToken) + /// + [InstrumentMySqlAttribute(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentNpgsql(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccess(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlite(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSql(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlClient(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemData(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataCommon(Method = ExecuteNonQueryAsync, ReturnType = TaskInt32, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + public class CommandExecuteNonQueryAsyncIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// CancellationToken value + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, CancellationToken cancellationToken) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnAsyncMethodEnd callback + /// + /// Type of the target + /// Type of the return value, in an async scenario will be T of Task of T + /// Instance value, aka `this` of the instrumented method. + /// Return value instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return returnValue; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryIntegration.cs new file mode 100644 index 000000000..e57b14aae --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryIntegration.cs @@ -0,0 +1,62 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// int [Command].ExecuteNonQuery() + /// + [InstrumentMySqlAttribute(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentNpgsql(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentOracleManagedDataAccess(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentMicrosoftDataSqlite(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentSystemDataSqlite(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentSystemDataSql(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentSystemDataSqlClient(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32)] + public class CommandExecuteNonQueryIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Type of the return value + /// Instance value, aka `this` of the instrumented method. + /// Task of HttpResponse message instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return new CallTargetReturn(returnValue); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryWithBehaviorIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryWithBehaviorIntegration.cs new file mode 100644 index 000000000..b45c001c9 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteNonQueryWithBehaviorIntegration.cs @@ -0,0 +1,64 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// int [Command].ExecuteNonQuery(CommandBehavior) + /// + [InstrumentMySqlAttribute(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentNpgsql(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccess(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSqlite(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSql(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSqlClient(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteNonQuery, ReturnType = ClrTypeNames.Int32, ParameterTypes = new[] { AdoNetTypeNames.CommandBehavior })] + public class CommandExecuteNonQueryWithBehaviorIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Command Behavior type + /// Instance value, aka `this` of the instrumented method. + /// Command behavior + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TBehavior commandBehavior) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Type of the return value + /// Instance value, aka `this` of the instrumented method. + /// Task of HttpResponse message instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return new CallTargetReturn(returnValue); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderAsyncIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderAsyncIntegration.cs new file mode 100644 index 000000000..b0cd42fae --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderAsyncIntegration.cs @@ -0,0 +1,64 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using System.Threading; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// Task[*DataReader] [Command].ExecuteReaderAsync(CancellationToken) + /// + [InstrumentMySqlAttribute(Method = ExecuteReaderAsync, ReturnType = MySql.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentNpgsql(Method = ExecuteReaderAsync, ReturnType = Npgsql.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccess(Method = ExecuteReaderAsync, ReturnType = OracleManagedDataAccess.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteReaderAsync, ReturnType = OracleManagedDataAccess.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteReaderAsync, ReturnType = MicrosoftDataSqlite.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlite(Method = ExecuteReaderAsync, ReturnType = SystemDataSqlite.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSql(Method = ExecuteReaderAsync, ReturnType = SystemDataSqlServer.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlClient(Method = ExecuteReaderAsync, ReturnType = SystemDataSqlServer.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteReaderAsync, ReturnType = MicrosoftDataSqlServer.TaskDataReader, ParameterTypes = new[] { ClrTypeNames.CancellationToken })] + public class CommandExecuteReaderAsyncIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// CancellationToken value + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, CancellationToken cancellationToken) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnAsyncMethodEnd callback + /// + /// Type of the target + /// Type of the return value, in an async scenario will be T of Task of T + /// Instance value, aka `this` of the instrumented method. + /// Return value instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return returnValue; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderIntegration.cs new file mode 100644 index 000000000..11fe89ff6 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderIntegration.cs @@ -0,0 +1,64 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using Elastic.Apm.Api; +using Elastic.Apm.Model; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Reflection; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// [*]DataReader [Command].ExecuteReader() + /// + [InstrumentMySqlAttribute(Method = ExecuteReader, ReturnType = MySql.DataReader)] + [InstrumentNpgsql(Method = ExecuteReader, ReturnType = Npgsql.DataReader)] + [InstrumentOracleManagedDataAccess(Method = ExecuteReader, ReturnType = OracleManagedDataAccess.DataReader)] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteReader, ReturnType = OracleManagedDataAccess.DataReader)] + [InstrumentMicrosoftDataSqlite(Method = ExecuteReader, ReturnType = MicrosoftDataSqlite.DataReader)] + [InstrumentSystemDataSqlite(Method = ExecuteReader, ReturnType = SystemDataSqlite.DataReader)] + + [InstrumentSystemDataSql(Method = ExecuteReader, ReturnType = SystemDataSqlServer.DataReader)] + [InstrumentSystemDataSqlClient(Method = ExecuteReader, ReturnType = SystemDataSqlServer.DataReader)] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteReader, ReturnType = MicrosoftDataSqlServer.DataReader)] + public class CommandExecuteReaderIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Type of the return value + /// Instance value, aka `this` of the instrumented method. + /// Task of HttpResponse message instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return new CallTargetReturn(returnValue); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderWithBehaviorAsyncIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderWithBehaviorAsyncIntegration.cs new file mode 100644 index 000000000..a62f87aa4 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderWithBehaviorAsyncIntegration.cs @@ -0,0 +1,79 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using System.Threading; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// Task[*DataReader] [Command].ExecuteReaderAsync(CommandBehavior, CancellationToken) + /// Task[DbDataReader] [Command].ExecuteDbDataReaderAsync(CommandBehavior, CancellationToken) + /// + [InstrumentMySqlAttribute(Method = ExecuteReaderAsync, ReturnType = MySql.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentNpgsql(Method = ExecuteReaderAsync, ReturnType = Npgsql.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccess(Method = ExecuteReaderAsync, ReturnType = OracleManagedDataAccess.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteReaderAsync, ReturnType = OracleManagedDataAccess.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteReaderAsync, ReturnType = MicrosoftDataSqlite.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlite(Method = ExecuteReaderAsync, ReturnType = SystemDataSqlite.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSql(Method = ExecuteReaderAsync, ReturnType = SystemDataSqlServer.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlClient(Method = ExecuteReaderAsync, ReturnType = SystemDataSqlServer.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteReaderAsync, ReturnType = MicrosoftDataSqlServer.TaskDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + + [InstrumentMySqlAttribute(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentNpgsql(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccess(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSql(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlClient(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + + [InstrumentSystemData(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + [InstrumentSystemDataCommon(Method = ExecuteDbDataReaderAsync, ReturnType = TaskDbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior, ClrTypeNames.CancellationToken })] + public class CommandExecuteReaderWithBehaviorAsyncIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Command Behavior type + /// Instance value, aka `this` of the instrumented method. + /// Command behavior + /// CancellationToken value + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TBehavior commandBehavior, CancellationToken cancellationToken) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnAsyncMethodEnd callback + /// + /// Type of the target + /// Type of the return value, in an async scenario will be T of Task of T + /// Instance value, aka `this` of the instrumented method. + /// Return value instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return returnValue; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderWithBehaviorIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderWithBehaviorIntegration.cs new file mode 100644 index 000000000..54cae6b3c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteReaderWithBehaviorIntegration.cs @@ -0,0 +1,74 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using Elastic.Apm.Api; +using Elastic.Apm.Model; +using Elastic.Apm.Profiler.Managed.CallTarget; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// [*]DataReader [Command].ExecuteReader(CommandBehavior) + /// [*]DataReader [Command].ExecuteDbDataReader(CommandBehavior) + /// + [InstrumentMySqlAttribute(Method = ExecuteReader, ReturnType = MySql.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentNpgsql(Method = ExecuteReader, ReturnType = Npgsql.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccess(Method = ExecuteReader, ReturnType = OracleManagedDataAccess.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteReader, ReturnType = OracleManagedDataAccess.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteReader, ReturnType = MicrosoftDataSqlite.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSqlite(Method = ExecuteReader, ReturnType = SystemDataSqlite.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSql(Method = ExecuteReader, ReturnType = SystemDataSqlServer.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSqlClient(Method = ExecuteReader, ReturnType = SystemDataSqlServer.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteReader, ReturnType = MicrosoftDataSqlServer.DataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + + [InstrumentMySqlAttribute(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentNpgsql(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccess(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSql(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSqlClient(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteDbDataReader, ReturnType = DbDataReader, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + public class CommandExecuteReaderWithBehaviorIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Command Behavior type + /// Instance value, aka `this` of the instrumented method. + /// Command behavior + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TBehavior commandBehavior) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Type of the return value + /// Instance value, aka `this` of the instrumented method. + /// Task of HttpResponse message instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return new CallTargetReturn(returnValue); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarAsyncIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarAsyncIntegration.cs new file mode 100644 index 000000000..7b24ce7c2 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarAsyncIntegration.cs @@ -0,0 +1,67 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using System.Threading; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// Task[object] [Command].ExecuteScalarAsync(CancellationToken) + /// + [InstrumentMySqlAttribute(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentNpgsql(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccess(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlite(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSql(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataSqlClient(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + + [InstrumentSystemData(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + [InstrumentSystemDataCommon(Method = ExecuteScalarAsync, ReturnType = TaskObject, ParameterTypes = new [] { ClrTypeNames.CancellationToken })] + public class CommandExecuteScalarAsyncIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// CancellationToken value + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, CancellationToken cancellationToken) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnAsyncMethodEnd callback + /// + /// Type of the target + /// Type of the return value, in an async scenario will be T of Task of T + /// Instance value, aka `this` of the instrumented method. + /// Return value instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return returnValue; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarIntegration.cs new file mode 100644 index 000000000..386cc35ed --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarIntegration.cs @@ -0,0 +1,62 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// object [Command].ExecuteScalar() + /// + [InstrumentMySqlAttribute(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentNpgsql(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentOracleManagedDataAccess(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentMicrosoftDataSqlite(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentSystemDataSqlite(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentSystemDataSql(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentSystemDataSqlClient(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object)] + public class CommandExecuteScalarIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Type of the return value + /// Instance value, aka `this` of the instrumented method. + /// Task of HttpResponse message instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return new CallTargetReturn(returnValue); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarWithBehaviorIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarWithBehaviorIntegration.cs new file mode 100644 index 000000000..8d51d3aa0 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/CommandExecuteScalarWithBehaviorIntegration.cs @@ -0,0 +1,64 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Data; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using static Elastic.Apm.Profiler.Managed.Integrations.AdoNet.AdoNetTypeNames; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + /// + /// CallTarget instrumentation for: + /// object [Command].ExecuteScalar(CommandBehavior) + /// + [InstrumentMySqlAttribute(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentNpgsql(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccess(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentOracleManagedDataAccessCore(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlite(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSqlite(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSql(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentSystemDataSqlClient(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + [InstrumentMicrosoftDataSqlClient(Method = ExecuteScalar, ReturnType = ClrTypeNames.Object, ParameterTypes = new [] { AdoNetTypeNames.CommandBehavior })] + public class CommandExecuteScalarWithBehaviorIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Command Behavior type + /// Instance value, aka `this` of the instrumented method. + /// Command behavior + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TBehavior commandBehavior) + { + var command = (IDbCommand)instance; + return new CallTargetState(DbSpanFactory.CreateSpan(Agent.Instance, command), command); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Type of the return value + /// Instance value, aka `this` of the instrumented method. + /// Task of HttpResponse message instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) + { + DbSpanFactory.EndSpan(Agent.Instance, (IDbCommand)instance, (ISpan)state.Segment, exception); + return new CallTargetReturn(returnValue); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/DbSpanFactory.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/DbSpanFactory.cs new file mode 100644 index 000000000..544faec6d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AdoNet/DbSpanFactory.cs @@ -0,0 +1,84 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Data; +using Elastic.Apm.Api; +using Elastic.Apm.Helpers; +using Elastic.Apm.Model; +using static Elastic.Apm.Model.InstrumentationFlag; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AdoNet +{ + internal static class DbSpanFactory + { + private static readonly Type _type; + private static readonly InstrumentationFlag _instrumentationFlag; + + static DbSpanFactory() + { + _type = typeof(T); + _instrumentationFlag = GetInstrumentationFlag(_type.FullName); + } + + internal static ISpan CreateSpan(ApmAgent agent, IDbCommand command) + { + if (agent.Tracer.CurrentTransaction is null) + return null; + + // if the current execution segment is + // 1. already for this instrumentation or instrumentation is AdoNet (System.Data.Common.DbCommand) and the type is "db" + // and + // 2. for the same command text + // skip creating another db span for it, to prevent instrumenting delegated methods. + if (agent.GetCurrentExecutionSegment() is Span span && + (span.InstrumentationFlag.HasFlag(_instrumentationFlag) || + span.Type == ApiConstants.TypeDb && + (_instrumentationFlag == InstrumentationFlag.AdoNet || span.InstrumentationFlag == InstrumentationFlag.AdoNet)) && + span.Name == DbSpanCommon.GetDbSpanName(command)) + return null; + + return agent.TracerInternal.DbSpanCommon.StartSpan(agent, command, _instrumentationFlag); + } + + internal static void EndSpan(ApmAgent agent, IDbCommand command, ISpan span, Exception exception) + { + if (span != null) + { + var outcome = Outcome.Success; + if (exception != null) + { + span.CaptureException(exception); + outcome = Outcome.Failure; + } + + agent.TracerInternal.DbSpanCommon.EndSpan(span, command, outcome); + } + } + + private static InstrumentationFlag GetInstrumentationFlag(string typeName) + { + switch (typeName) + { + case { } str when str.ContainsOrdinalIgnoreCase("Sqlite"): + return Sqlite; + case { } str when str.ContainsOrdinalIgnoreCase("MySQL"): + return MySql; + case { } when typeName.ContainsOrdinalIgnoreCase("Oracle"): + return Oracle; + case { } when typeName.ContainsOrdinalIgnoreCase("Postgre"): + case { } when typeName.ContainsOrdinalIgnoreCase("NpgSql"): + return Postgres; + case { } when typeName.ContainsOrdinalIgnoreCase("Microsoft"): + case { } when typeName.ContainsOrdinalIgnoreCase("System.Data.SqlClient"): + return SqlClient; + case { } when typeName.ContainsOrdinalIgnoreCase("System.Data.Common"): + return InstrumentationFlag.AdoNet; + default: + return None; + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/AspNet/ElasticApmModuleIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/AspNet/ElasticApmModuleIntegration.cs new file mode 100644 index 000000000..d78ed6115 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/AspNet/ElasticApmModuleIntegration.cs @@ -0,0 +1,76 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + + +using System.Threading; +#if NETFRAMEWORK +using System.Web; +using Elastic.Apm.AspNetFullFramework; +#endif +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; + +namespace Elastic.Apm.Profiler.Managed.Integrations.AspNet +{ + /// + /// System.Web.Compilation.BuildManager.InvokePreStartInitMethodsCore calltarget instrumentation + /// + [Instrument( + Assembly = "System.Web", + Type = "System.Web.Compilation.BuildManager", + Method = "InvokePreStartInitMethodsCore", + ReturnType = ClrTypeNames.Void, + ParameterTypes = new[] { "System.Collections.Generic.ICollection`1[System.Reflection.MethodInfo]", "System.Func`1[System.IDisposable]" }, + MinimumVersion = "4.0.0", + MaximumVersion = "4.*.*", + Group = "AspNet")] + public class ElasticApmModuleIntegration + { + /// + /// Indicates whether we're initializing the HttpModule for the first time + /// + private static int FirstInitialization = 1; + + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Type of the collection + /// Type of the + /// Instance value, aka `this` of the instrumented method. This method is static so this parameter will always be null + /// The methods to be invoked + /// The function to set the environment culture + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TCollection methods, TFunc setHostingEnvironmentCultures) + { + if (Interlocked.Exchange(ref FirstInitialization, 0) != 1) + { + // The HttpModule was already registered + return CallTargetState.GetDefault(); + } + + try + { +// directive applied just to here and to .NET framework specific using directives, to allow +// the integrations file generator to pick this integration up, irrespective of version. +#if NETFRAMEWORK + HttpApplication.RegisterModule(typeof(ElasticApmModule)); +#endif + } + catch + { + // Unable to dynamically register module + // Not sure if we can technically log yet or not, so do nothing + } + + return CallTargetState.GetDefault(); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/CachedMessageHeadersHelper.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/CachedMessageHeadersHelper.cs new file mode 100644 index 000000000..7e8386ce3 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/CachedMessageHeadersHelper.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Reflection.Emit; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + internal static class CachedMessageHeadersHelper + { + private static readonly Func _activator; + + static CachedMessageHeadersHelper() + { + var headersType = typeof(TMarkerType).Assembly.GetType("Confluent.Kafka.Headers"); + + var ctor = headersType.GetConstructor(System.Type.EmptyTypes); + + var createHeadersMethod = new DynamicMethod( + $"KafkaCachedMessageHeadersHelpers", + headersType, + null, + typeof(DuckType).Module, + true); + + var il = createHeadersMethod.GetILGenerator(); + il.Emit(OpCodes.Newobj, ctor); + il.Emit(OpCodes.Ret); + + _activator = (Func)createHeadersMethod.CreateDelegate(typeof(Func)); + } + + /// + /// Creates a Confluent.Kafka.Headers object and assigns it to an `IMessage` proxy + /// + /// A proxy for the new Headers object + public static IHeaders CreateHeaders() + { + var headers = _activator(); + return headers.DuckCast(); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IConsumeException.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IConsumeException.cs new file mode 100644 index 000000000..a43712867 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IConsumeException.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// ConsumeException interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IConsumeException + { + /// + /// Gets the consume result associated with the consume request + /// + public IConsumeResult ConsumerRecord { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IConsumeResult.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IConsumeResult.cs new file mode 100644 index 000000000..77deb0592 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IConsumeResult.cs @@ -0,0 +1,43 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// ConsumeResult for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IConsumeResult + { + /// + /// Gets the topic + /// + public string Topic { get; } + + /// + /// Gets the partition + /// + public Partition Partition { get; } + + /// + /// Gets the offset + /// + public Offset Offset { get; } + + /// + /// Gets the Kafka record + /// + public IMessage Message { get; } + + /// + /// Gets a value indicating whether gets whether the message is a partition EOF + /// + // ReSharper disable once InconsistentNaming + public bool IsPartitionEOF { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IDeliveryReport.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IDeliveryReport.cs new file mode 100644 index 000000000..d71554459 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IDeliveryReport.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// DeliveryReport interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IDeliveryReport : IDeliveryResult + { + /// + /// Gets the Error associated with the delivery report + /// + public IError Error { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IDeliveryResult.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IDeliveryResult.cs new file mode 100644 index 000000000..ca2e4b099 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IDeliveryResult.cs @@ -0,0 +1,32 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// DeliveryResult interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IDeliveryResult + { + /// + /// Gets the Kafka partition. + /// + public Partition Partition { get; } + + /// + /// Gets the Kafka offset + /// + public Offset Offset { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IError.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IError.cs new file mode 100644 index 000000000..40be80a7c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IError.cs @@ -0,0 +1,33 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Error interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IError + { + /// + /// Gets a value indicating whether the error is really an error + /// + public bool IsError { get; } + + /// + /// Gets the string representation of the error + /// + /// The string representation of the error + public string ToString(); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IHeaders.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IHeaders.cs new file mode 100644 index 000000000..b4d08233e --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IHeaders.cs @@ -0,0 +1,52 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Headers interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IHeaders + { + /// + /// Adds a header to the collection + /// + /// The header's key value + /// The value of the header. May be null. Format strings as UTF8 + public void Add(string key, byte[] val); + + /// + /// Removes all headers for the given key. + /// + /// The key to remove all headers for + public void Remove(string key); + + /// + /// Try to get the value of the latest header with the specified key. + /// + /// + /// The key to get the associated value of. + /// + /// + /// The value of the latest element in the collection with the + /// specified key, if a header with that key was present in the + /// collection. + /// + /// + /// true if the a value with the specified key was present in + /// the collection, false otherwise. + /// + public bool TryGetLastBytes(string key, out byte[] lastHeader); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IMessage.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IMessage.cs new file mode 100644 index 000000000..f2eed2357 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IMessage.cs @@ -0,0 +1,37 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Message interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IMessage + { + /// + /// Gets the value of the message + /// + public object Value { get; } + + /// + /// Gets the timestamp that the message was produced + /// + public ITimestamp Timestamp { get; } + + /// + /// Gets or sets the headers for the record + /// + public IHeaders Headers { get; set; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IProduceException.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IProduceException.cs new file mode 100644 index 000000000..f6f675951 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/IProduceException.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// ProduceException interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IProduceException + { + /// + /// Gets the delivery result associated with the produce request + /// + public IDeliveryResult DeliveryResult { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITimestamp.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITimestamp.cs new file mode 100644 index 000000000..af1825927 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITimestamp.cs @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Timestamp struct for duck-typing + /// Requires boxing, but necessary as we need to duck-type too + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface ITimestamp + { + /// + /// Gets the timestamp type + /// + public int Type { get; } + + /// + /// Gets the UTC DateTime for the timestamp + /// + public DateTime UtcDateTime { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITopicPartition.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITopicPartition.cs new file mode 100644 index 000000000..a89e06f6d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITopicPartition.cs @@ -0,0 +1,32 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// TopicPartition interface for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface ITopicPartition + { + /// + /// Gets the Kafka topic name. + /// + public string Topic { get; } + + /// + /// Gets the Kafka partition. + /// + public Partition Partition { get; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITypedDeliveryHandlerShimAction.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITypedDeliveryHandlerShimAction.cs new file mode 100644 index 000000000..48b4a8454 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/ITypedDeliveryHandlerShimAction.cs @@ -0,0 +1,29 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// TypedDeliveryHandlerShim_Action for duck-typing + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface ITypedDeliveryHandlerShimAction + { + /// + /// Sets the delivery report handler + /// + [DuckField] + public object Handler { set; } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConstants.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConstants.cs new file mode 100644 index 000000000..879965465 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConstants.cs @@ -0,0 +1,25 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + internal static class KafkaConstants + { + internal const string ConsumeOperationName = "kafka.consume"; + internal const string ProduceOperationName = "kafka.produce"; + internal const string TopicPartitionTypeName = "Confluent.Kafka.TopicPartition"; + internal const string MessageTypeName = "Confluent.Kafka.Message`2[!0,!1]"; + internal const string ConsumeResultTypeName = "Confluent.Kafka.ConsumeResult`2[!0,!1]"; + internal const string ActionOfDeliveryReportTypeName = "System.Action`1[Confluent.Kafka.DeliveryReport`2[!0,!1]]"; + internal const string TaskDeliveryReportTypeName = "System.Threading.Tasks.Task`1[Confluent.Kafka.DeliveryReport`2[!0,!1]]"; + internal const string Subtype = "kafka"; + internal const string IntegrationName = "Kafka"; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerCloseIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerCloseIntegration.cs new file mode 100644 index 000000000..5aec5dcd5 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerCloseIntegration.cs @@ -0,0 +1,59 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Confluent.Kafka Consumer.Consume calltarget instrumentation + /// + [Instrument( + Assembly = "Confluent.Kafka", + Type = "Confluent.Kafka.Consumer`2", + Method = "Close", + ReturnType = ClrTypeNames.Void, + ParameterTypes = new string[0], + MinimumVersion = "1.4.0", + MaximumVersion = "1.*.*", + Group = "Kafka")] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class KafkaConsumerCloseIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance) + { + // If we are already in a consumer scope, close it. + KafkaHelper.CloseConsumerTransaction(Agent.Instance); + return CallTargetState.GetDefault(); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state) => + CallTargetReturn.GetDefault(); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerConsumeIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerConsumeIntegration.cs new file mode 100644 index 000000000..8d5d5be7d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerConsumeIntegration.cs @@ -0,0 +1,101 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Confluent.Kafka Consumer.Consume calltarget instrumentation + /// + [Instrument( + Assembly = "Confluent.Kafka", + Type = "Confluent.Kafka.Consumer`2", + Method = "Consume", + ReturnType = KafkaConstants.ConsumeResultTypeName, + ParameterTypes = new[] { ClrTypeNames.Int32 }, + MinimumVersion = "1.4.0", + MaximumVersion = "1.*.*", + Group = KafkaConstants.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class KafkaConsumerConsumeIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// The maximum period of time the call may block. + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, int millisecondsTimeout) + { + // If we are already in a consumer scope, close it, and start a new one on method exit. + KafkaHelper.CloseConsumerTransaction(Agent.Instance); + return CallTargetState.GetDefault(); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Type of the response + /// Instance value, aka `this` of the instrumented method. + /// Response instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, TResponse response, Exception exception, CallTargetState state) + where TResponse : IConsumeResult, IDuckType + { + IConsumeResult consumeResult = response.Instance is not null + ? response + : null; + + if (exception is not null && exception.TryDuckCast(out var consumeException)) + consumeResult = consumeException.ConsumerRecord; + + if (consumeResult is not null) + { + var outcome = Outcome.Success; + + // This creates the transaction and either disposes it immediately + // or disposes it on the next call to Consumer.Consume() + var transaction = KafkaHelper.CreateConsumerTransaction( + Agent.Instance, + consumeResult.Topic, + consumeResult.Partition, + consumeResult.Offset, + consumeResult.Message); + + if (exception is not null) + { + outcome = Outcome.Failure; + transaction.CaptureException(exception); + } + + transaction.Outcome = outcome; + + // if (!Agent.Instance.ConfigurationReader.KafkaCreateConsumerScopeEnabled) + // { + // // Close and dispose the transaction immediately + // transaction.End(); + // } + } + + return new CallTargetReturn(response); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerDisposeIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerDisposeIntegration.cs new file mode 100644 index 000000000..b8931505d --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerDisposeIntegration.cs @@ -0,0 +1,59 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Confluent.Kafka Consumer.Consume calltarget instrumentation + /// + [Instrument( + Assembly = "Confluent.Kafka", + Type = "Confluent.Kafka.Consumer`2", + Method = "Dispose", + ReturnType = ClrTypeNames.Void, + ParameterTypes = new string[0], + MinimumVersion = "1.4.0", + MaximumVersion = "1.*.*", + Group = KafkaConstants.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class KafkaConsumerDisposeIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance) + { + // If we are already in a consumer scope, close it. + KafkaHelper.CloseConsumerTransaction(Agent.Instance); + return CallTargetState.GetDefault(); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state) => + CallTargetReturn.GetDefault(); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerUnsubscribeIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerUnsubscribeIntegration.cs new file mode 100644 index 000000000..636b95c6f --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaConsumerUnsubscribeIntegration.cs @@ -0,0 +1,58 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Confluent.Kafka Consumer.Consume calltarget instrumentation + /// + [Instrument( + Assembly = "Confluent.Kafka", + Type= "Confluent.Kafka.Consumer`2", + Method = "Unsubscribe", + ReturnType = ClrTypeNames.Void, + ParameterTypes = new string[0], + MinimumVersion = "1.4.0", + MaximumVersion = "1.*.*", + Group = KafkaConstants.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class KafkaConsumerUnsubscribeIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance) + { + // If we are already in a consumer scope, close it. + KafkaHelper.CloseConsumerTransaction(Agent.Instance); + return CallTargetState.GetDefault(); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state) => + CallTargetReturn.GetDefault(); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaHeadersCollectionAdapter.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaHeadersCollectionAdapter.cs new file mode 100644 index 000000000..8e5ea3372 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaHeadersCollectionAdapter.cs @@ -0,0 +1,91 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// A collection of headers. + /// + internal interface IHeadersCollection + { + /// + /// Returns all header values for a specified header stored in the collection. + /// + /// The specified header to return values for. + /// Zero or more header strings. + IEnumerable GetValues(string name); + + /// + /// Sets the value of an entry in the collection, replacing any previous values. + /// + /// The header to add to the collection. + /// The content of the header. + void Set(string name, string value); + + /// + /// Adds the specified header and its value into the collection. + /// + /// The header to add to the collection. + /// The content of the header. + void Add(string name, string value); + + /// + /// Removes the specified header from the collection. + /// + /// The name of the header to remove from the collection. + void Remove(string name); + } + + internal struct KafkaHeadersCollectionAdapter : IHeadersCollection + { + private readonly IHeaders _headers; + private readonly IApmLogger _logger; + + public KafkaHeadersCollectionAdapter(IHeaders headers, IApmLogger logger) + { + _headers = headers; + _logger = logger.Scoped("KafkaHeadersCollectionAdapter"); + } + + public IEnumerable GetValues(string name) + { + // This only returns the _last_ bytes. Accessing other values is more expensive and should generally be unnecessary + if (_headers.TryGetLastBytes(name, out var bytes)) + { + try + { + return new[] { Encoding.UTF8.GetString(bytes) }; + } + catch (Exception ex) + { + _logger.Info()?.LogException(ex, "Could not deserialize Kafka header {headerName}", name); + } + } + + return Enumerable.Empty(); + } + + public void Set(string name, string value) + { + Remove(name); + Add(name, value); + } + + public void Add(string name, string value) => _headers.Add(name, Encoding.UTF8.GetBytes(value)); + + public void Remove(string name) => _headers.Remove(name); + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaHelper.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaHelper.cs new file mode 100644 index 000000000..3a076647b --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaHelper.cs @@ -0,0 +1,169 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Linq; +using Elastic.Apm.Api; +using Elastic.Apm.DistributedTracing; +using Elastic.Apm.Helpers; +using Elastic.Apm.Logging; +using Elastic.Apm.Model; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + internal static class KafkaHelper + { + private static bool HeadersInjectionEnabled = true; + + internal static ISpan CreateProducerSpan(IApmAgent agent, ITopicPartition topicPartition, bool isTombstone, bool finishOnClose) + { + ISpan span = null; + + try + { + // no current transaction, don't create a span + var currentTransaction = agent.Tracer.CurrentTransaction; + if (currentTransaction is null) + return null; + + var spanName = string.IsNullOrEmpty(topicPartition?.Topic) + ? "Kafka SEND" + : $"Kafka SEND to {topicPartition.Topic}"; + + span = agent.GetCurrentExecutionSegment().StartSpan( + spanName, + ApiConstants.TypeMessaging, + KafkaConstants.Subtype); + + if (topicPartition?.Partition is not null && !topicPartition.Partition.IsSpecial) + span.SetLabel("partition", topicPartition.Partition.ToString()); + + if (isTombstone) + span.SetLabel("tombstone", "true"); + } + catch (Exception ex) + { + agent.Logger.Error()?.LogException(ex, "Error creating or populating kafka span."); + } + + return span; + } + + internal static ITransaction CreateConsumerTransaction( + IApmAgent agent, + string topic, + Partition? partition, + Offset? offset, + IMessage message) + { + ITransaction transaction = null; + + try + { + var currentTransaction = agent.Tracer.CurrentTransaction; + if (currentTransaction is not null) + return null; + + DistributedTracingData distributedTracingData = null; + if (message?.Headers != null) + { + var headers = new KafkaHeadersCollectionAdapter(message.Headers, agent.Logger); + + try + { + var traceParent = string.Join(",", headers.GetValues(TraceContext.TraceParentHeaderName)); + var traceState = headers.GetValues(TraceContext.TraceStateHeaderName).FirstOrDefault(); + distributedTracingData = TraceContext.TryExtractTracingData(traceParent, traceState); + } + catch (Exception ex) + { + agent.Logger.Error()?.LogException(ex, "Error extracting propagated headers from Kafka message"); + } + } + + var name = string.IsNullOrEmpty(topic) + ? "Kafka RECEIVE" + : $"Kafka RECEIVE from {topic}"; + + transaction = agent.Tracer.StartTransaction(name, ApiConstants.TypeMessaging, distributedTracingData); + + if (partition is not null) + transaction.SetLabel("partition", partition.ToString()); + + if (offset is not null) + transaction.SetLabel("offset", offset.ToString()); + + if (transaction is Transaction realTransaction && message is not null && message.Timestamp.Type != 0) + { + var consumeTime = TimeUtils.ToDateTime(realTransaction.Timestamp); + var produceTime = message.Timestamp.UtcDateTime; + + // TODO: set transaction.Context.Message to Math.Max(0, (consumeTime - produceTime).TotalMilliseconds + } + + if (message is not null && message.Value is null) + transaction.SetLabel("tombstone", "true"); + } + catch (Exception ex) + { + agent.Logger.Error()?.LogException(ex, "Error creating or populating transaction."); + } + + return transaction; + } + + internal static void CloseConsumerTransaction(IApmAgent agent) + { + try + { + var transaction = agent.Tracer.CurrentTransaction; + if (transaction is null || !transaction.Name.StartsWith("Kafka RECEIVE")) + return; + + transaction.End(); + } + catch (Exception ex) + { + agent.Logger.Error()?.LogException(ex, "Error closing Kafka consumer transaction"); + } + } + + /// + /// Try to inject the prop + /// + /// The agent + /// The outgoing distributed tracing data to propagate + /// The duck-typed Kafka Message object + /// The TopicPartition type (used optimisation purposes) + /// The type of the duck-type proxy + internal static void TryInjectHeaders(IApmAgent agent, IExecutionSegment segment, TMessage message) + where TMessage : IMessage + { + if (!HeadersInjectionEnabled) + return; + + try + { + message.Headers ??= CachedMessageHeadersHelper.CreateHeaders(); + var adapter = new KafkaHeadersCollectionAdapter(message.Headers, agent.Logger); + var distributedTracingData = segment.OutgoingDistributedTracingData; + + adapter.Set(TraceContext.TraceParentHeaderName, distributedTracingData.SerializeToString()); + adapter.Set(TraceContext.TraceStateHeaderName, distributedTracingData.TraceState.ToTextHeader()); + } + catch (Exception ex) + { + // don't keep trying if we run into problems + HeadersInjectionEnabled = false; + agent.Logger.Warning()?.LogException(ex, "There was a problem injecting headers into the Kafka record. Disabling headers injection"); + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceAsyncIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceAsyncIntegration.cs new file mode 100644 index 000000000..fb41a0f6c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceAsyncIntegration.cs @@ -0,0 +1,110 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using System.Threading; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Confluent.Kafka Producer.Produce calltarget instrumentation + /// + [Instrument( + Assembly = "Confluent.Kafka", + Type = "Confluent.Kafka.Producer`2", + Method= "ProduceAsync", + ReturnType = KafkaConstants.TaskDeliveryReportTypeName, + ParameterTypes = new[] { KafkaConstants.TopicPartitionTypeName, KafkaConstants.MessageTypeName, ClrTypeNames.CancellationToken }, + MinimumVersion = "1.4.0", + MaximumVersion = "1.*.*", + Group = KafkaConstants.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class KafkaProduceAsyncIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Type of the TopicPartition + /// Type of the message + /// Instance value, aka `this` of the instrumented method. + /// TopicPartition instance + /// Message instance + /// CancellationToken instance + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TTopicPartition topicPartition, TMessage message, CancellationToken cancellationToken) + where TMessage : IMessage + { + var agent = Agent.Instance; + + var span = KafkaHelper.CreateProducerSpan( + agent, + topicPartition.DuckCast(), + isTombstone: message.Value is null, + finishOnClose: true); + + if (span is not null) + { + KafkaHelper.TryInjectHeaders(agent, span, message); + return new CallTargetState(span); + } + + return CallTargetState.GetDefault(); + } + + /// + /// OnAsyncMethodEnd callback + /// + /// Type of the target + /// Type of the response, in an async scenario will be T of Task of T + /// Instance value, aka `this` of the instrumented method. + /// Response instance + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static TResponse OnAsyncMethodEnd(TTarget instance, TResponse response, Exception exception, CallTargetState state) + where TResponse : IDeliveryResult + { + var span = state.Segment; + if (span is not null) + { + var outcome = Outcome.Success; + IDeliveryResult deliveryResult = null; + if (exception is not null) + { + outcome = Outcome.Failure; + span.CaptureException(exception); + + var produceException = exception.DuckAs(); + if (produceException is not null) + deliveryResult = produceException.DeliveryResult; + } + else if (response is not null) + deliveryResult = response; + + if (deliveryResult is not null) + { + span.SetLabel("partition", deliveryResult.Partition.ToString()); + span.SetLabel("offset", deliveryResult.Offset.ToString()); + } + + span.Outcome = outcome; + span.End(); + } + return response; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceSyncDeliveryHandlerIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceSyncDeliveryHandlerIntegration.cs new file mode 100644 index 000000000..5c4c2f0d3 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceSyncDeliveryHandlerIntegration.cs @@ -0,0 +1,193 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using System.Reflection; +using Elastic.Apm.Api; +using Elastic.Apm.Logging; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Confluent.Kafka Producer.TypedDeliveryHandlerShim_Action.HandleDeliveryReport calltarget instrumentation + /// + [Instrument( + Assembly = "Confluent.Kafka", + Type = "Confluent.Kafka.Producer`2+TypedDeliveryHandlerShim_Action", + Method = ".ctor", + ReturnType = ClrTypeNames.Void, + ParameterTypes = new[] { ClrTypeNames.String, "!0", "!1", KafkaConstants.ActionOfDeliveryReportTypeName }, + MinimumVersion = "1.4.0", + MaximumVersion = "1.*.*", + Group = KafkaConstants.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class KafkaProduceSyncDeliveryHandlerIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Type of the message key + /// Type of the message value + /// Type of the delivery report + /// Instance value, aka `this` of the instrumented method. + /// The topic to which the message was sent + /// The message key value + /// The message value + /// The delivery handler instance + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, string topic, TKey key, TValue value, TActionOfDeliveryReport handler) + { + if (handler is null) + { + // Handled in KafkaProduceSyncIntegration + return CallTargetState.GetDefault(); + } + + try + { + var agent = Agent.Instance; + + // The current span should be started in KafkaProduceSyncIntegration.OnMethodBegin + // The OnMethodBegin and OnMethodEnd of this integration happens between KafkaProduceSyncIntegration.OnMethodBegin + // and KafkaProduceSyncIntegration.OnMethodEnd, so the consumer span is active for the duration of this integration + var span = agent.Tracer.CurrentSpan; + if (span is null) + { + agent.Logger.Error()?.Log("Unexpected null span for Kafka Producer with delivery handler"); + return CallTargetState.GetDefault(); + } + + var newAction = CachedWrapperDelegate.CreateWrapper(handler, span); + + Action updateHandlerAction = inst => inst.Handler = newAction; + + // store the call to update the handler variable as state + // so we update it at the _end_ of the constructor + return new CallTargetState(span, updateHandlerAction); + } + catch (Exception ex) + { + Agent.Instance.Logger.Error()?.LogException(ex, "Error creating wrapped delegate for delivery report"); + return CallTargetState.GetDefault(); + } + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state) + { + if (state.State is Action updateHandlerAction + && instance.TryDuckCast(out var shim)) + { + var agent = Agent.Instance; + + try + { + updateHandlerAction.Invoke(shim); + } + catch (Exception ex) + { + agent.Logger.Error()?.LogException(ex, "There was an error updating the delivery report handler"); + // Not ideal to close the span here immediately, but as we can't trace the result, + // we don't really have a choice + state.Segment?.End(); + } + } + + return CallTargetReturn.GetDefault(); + } + + /// + /// Helper method used by to create a delegate + /// + /// The original delivery report handler + /// A that can be manipulated when the action is invoked + /// Type of the delivery report + /// The wrapped action + public static Action WrapAction(Action originalHandler, ISpan span) => + value => + { + var outcome = Outcome.Success; + + if (value.TryDuckCast(out var report)) + { + var isError = report?.Error is not null && report.Error.IsError; + if (isError) + { + // Set the error tags manually, as we don't have an exception + stack trace here + // Should we create a stack trace manually? + var ex = new Exception(report.Error.ToString()); + span.CaptureException(ex); + outcome = Outcome.Failure; + } + + if (report?.Partition is not null) + span.SetLabel("partition", report.Partition.ToString()); + + // Won't have offset if is error + if (!isError && report?.Offset is not null) + span.SetLabel("offset", report.Offset.ToString()); + } + + // call previous delegate + try + { + originalHandler(value); + } + finally + { + span.Outcome = outcome; + span.End(); + } + }; + + /// + /// Helper class for creating a that wraps an , + /// + /// Makes the assumption that TActionDelegate is an + internal static class CachedWrapperDelegate + { + private static readonly CreateWrapperDelegate _createWrapper; + + static CachedWrapperDelegate() + { + // This type makes the following assumption: TActionDelegate = Action ! + + // Get the Action WrapHelper.WrapAction(Action value) methodinfo + var wrapActionMethod = typeof(KafkaProduceSyncDeliveryHandlerIntegration) + .GetMethod(nameof(WrapAction), BindingFlags.Public | BindingFlags.Static); + + // Create the generic method using the inner generic types of TActionDelegate => TParam + wrapActionMethod = wrapActionMethod.MakeGenericMethod(typeof(TActionDelegate).GetGenericArguments()); + + // With Action WrapHelper.WrapAction(Action value) method info we create a delegate + _createWrapper = (CreateWrapperDelegate)wrapActionMethod.CreateDelegate(typeof(CreateWrapperDelegate)); + } + + private delegate TActionDelegate CreateWrapperDelegate(TActionDelegate value, ISpan span); + + public static TActionDelegate CreateWrapper(TActionDelegate value, ISpan span) => + // we call the delegate passing the instance of the previous delegate + _createWrapper(value, span); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceSyncIntegration.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceSyncIntegration.cs new file mode 100644 index 000000000..c39ebcab2 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/KafkaProduceSyncIntegration.cs @@ -0,0 +1,97 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using Elastic.Apm.Api; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.Core; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Confluent.Kafka Producer.Produce calltarget instrumentation + /// + [Instrument( + Assembly = "Confluent.Kafka", + Type = "Confluent.Kafka.Producer`2", + Method = "Produce", + ReturnType = ClrTypeNames.Void, + ParameterTypes = new[] { KafkaConstants.TopicPartitionTypeName, KafkaConstants.MessageTypeName, KafkaConstants.ActionOfDeliveryReportTypeName }, + MinimumVersion = "1.4.0", + MaximumVersion = "1.*.*", + Group = KafkaConstants.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class KafkaProduceSyncIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// Type of the TopicPartition + /// Type of the message + /// Type of the delivery handler action + /// Instance value, aka `this` of the instrumented method. + /// TopicPartition instance + /// Message instance + /// Delivery Handler instance + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TTopicPartition topicPartition, TMessage message, TDeliveryHandler deliveryHandler) + where TMessage : IMessage + { + var agent = Agent.Instance; + + // manually doing duck cast here so we have access to the _original_ TopicPartition type + // as a generic parameter, for injecting headers + var span = KafkaHelper.CreateProducerSpan( + agent, + topicPartition.DuckCast(), + isTombstone: message.Value is null, + finishOnClose: deliveryHandler is null); + + if (span is not null) + { + KafkaHelper.TryInjectHeaders(agent, span, message); + return new CallTargetState(span); + } + + return CallTargetState.GetDefault(); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state) + { + var span = state.Segment; + if (span is not null) + { + var outcome = Outcome.Success; + if (exception is not null) + { + outcome = Outcome.Failure; + span.CaptureException(exception); + } + + span.Outcome = outcome; + span.End(); + } + + return CallTargetReturn.GetDefault(); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/Offset.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/Offset.cs new file mode 100644 index 000000000..0bbf8019a --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/Offset.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +// ReSharper disable SA1310 + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Partition for duck-typing + /// + [DuckCopy] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public struct Offset + { + private const long RdKafkaOffsetBeginning = -2; + private const long RdKafkaOffsetEnd = -1; + private const long RdKafkaOffsetStored = -1000; + private const long RdKafkaOffsetInvalid = -1001; + + /// + /// Gets the long value corresponding to this offset + /// + public long Value; + + /// + /// Based on the original implementation + /// https://github.com/confluentinc/confluent-kafka-dotnet/blob/643c8fdc90f54f4d82d5135ae7e91a995f0efdee/src/Confluent.Kafka/Offset.cs#L274 + /// + /// A string that represents the Offset object + public override string ToString() => + Value switch + { + RdKafkaOffsetBeginning => $"Beginning [{RdKafkaOffsetBeginning}]", + RdKafkaOffsetEnd => $"End [{RdKafkaOffsetEnd}]", + RdKafkaOffsetStored => $"Stored [{RdKafkaOffsetStored}]", + RdKafkaOffsetInvalid => $"Unset [{RdKafkaOffsetInvalid}]", + _ => Value.ToString() + }; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/Partition.cs b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/Partition.cs new file mode 100644 index 000000000..ac38fb57e --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Integrations/Kafka/Partition.cs @@ -0,0 +1,42 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.ComponentModel; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Integrations.Kafka +{ + /// + /// Partition for duck-typing + /// + [DuckCopy] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public struct Partition + { + /// + /// Gets the int value corresponding to this partition + /// + public int Value; + + /// + /// Gets whether or not this is one of the special + /// partition values. + /// + public bool IsSpecial; + + /// + /// Based on the original implementation + /// https://github.com/confluentinc/confluent-kafka-dotnet/blob/master/src/Confluent.Kafka/Partition.cs#L217-L224 + /// + /// A string that represents the Partition object + public override string ToString() => IsSpecial ? "[Any]" : $"[{Value.ToString()}]"; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Logger.cs b/src/Elastic.Apm.Profiler.Managed/Logger.cs new file mode 100644 index 000000000..15970b8bb --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Logger.cs @@ -0,0 +1,140 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Elastic.Apm.Profiler.Managed +{ + // match the log levels of the profiler logger + internal enum LogLevel + { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Off = 5, + } + + internal static class Logger + { + static Logger() + { + Level = GetLogLevel(LogLevel.Warn); + var logDirectory = GetLogDirectory(); + LogFile = GetLogFile(logDirectory); + Levels = new Dictionary + { + [LogLevel.Off] = "OFF ", + [LogLevel.Error] = "ERROR", + [LogLevel.Warn] = "WARN ", + [LogLevel.Info] = "INFO ", + [LogLevel.Debug] = "DEBUG", + [LogLevel.Trace] = "TRACE", + }; + } + + private static readonly LogLevel Level; + private static readonly string LogFile; + private static readonly Dictionary Levels; + + public static void Log(LogLevel level, Exception exception, string message, params object[] args) + { + if (Level > level) + return; + + Log(level, $"{message}{Environment.NewLine}{exception}", args); + } + + public static void Debug(string message, params object[] args) => Log(LogLevel.Debug, message, args); + + public static void Log(LogLevel level, string message, params object[] args) + { + if (Level > level) + return; + + try + { + if (LogFile != null) + { + try + { + using (var stream = File.Open(LogFile, FileMode.Append, FileAccess.Write, FileShare.Read)) + using (var writer = new StreamWriter(stream, new UTF8Encoding(false))) + { + writer.Write($"[{DateTimeOffset.Now:O}] [{Levels[level]}] "); + writer.WriteLine(message, args); + writer.Flush(); + stream.Flush(true); + } + + return; + } + catch + { + // ignore + } + } + + Console.Error.WriteLine($"[{DateTimeOffset.Now:O}] [{Levels[level]}] {message}", args); + } + catch + { + // ignore + } + } + + private static string GetLogFile(string logDirectory) + { + if (logDirectory is null) + return null; + + var process = Process.GetCurrentProcess(); + return Path.Combine(logDirectory, $"Elastic.Apm.Profiler.Managed_{process.ProcessName}_{process.Id}.log"); + } + + private static LogLevel GetLogLevel(LogLevel defaultLevel) + { + var level = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG"); + if (string.IsNullOrEmpty(level)) + return defaultLevel; + + return Enum.TryParse(level, true, out var parsedLevel) + ? parsedLevel + : defaultLevel; + } + + private static string GetLogDirectory() + { + try + { + var logDirectory = Environment.GetEnvironmentVariable("ELASTIC_APM_PROFILER_LOG_DIR"); + if (string.IsNullOrEmpty(logDirectory)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + var programData = Environment.GetEnvironmentVariable("PROGRAMDATA"); + logDirectory = !string.IsNullOrEmpty(programData) + ? Path.Combine(programData, "elastic", "apm-agent-dotnet", "logs") + : "."; + } + else + logDirectory = "/var/log/elastic/apm-agent-dotnet"; + } + + Directory.CreateDirectory(logDirectory); + return logDirectory; + } + catch + { + return null; + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/DelegateMetadata.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/DelegateMetadata.cs new file mode 100644 index 000000000..d369deba7 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/DelegateMetadata.cs @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + internal class DelegateMetadata + { + public Type Type { get; set; } + + public Type ReturnType { get; set; } + + public Type[] Generics { get; set; } + + public Type[] Parameters { get; set; } + + public static DelegateMetadata Create() + where TDelegate : Delegate + { + var delegateType = typeof(TDelegate); + var genericTypeArguments = delegateType.GenericTypeArguments; + + Type[] parameterTypes; + Type returnType; + + if (delegateType.Name.StartsWith("Func`")) + { + // last generic type argument is the return type + var parameterCount = genericTypeArguments.Length - 1; + parameterTypes = new Type[parameterCount]; + Array.Copy(genericTypeArguments, parameterTypes, parameterCount); + + returnType = genericTypeArguments[parameterCount]; + } + else if (delegateType.Name.StartsWith("Action`")) + { + parameterTypes = genericTypeArguments; + returnType = typeof(void); + } + else + { + throw new Exception($"Only Func<> or Action<> are supported in {nameof(DelegateMetadata)}."); + } + + return new DelegateMetadata + { + Generics = genericTypeArguments, + Parameters = parameterTypes, + ReturnType = returnType, + Type = delegateType + }; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/DynamicMethodBuilder.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/DynamicMethodBuilder.cs new file mode 100644 index 000000000..941fd45c8 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/DynamicMethodBuilder.cs @@ -0,0 +1,359 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + /// + /// Helper class to create instances of using . + /// + /// The type of delegate + internal static class DynamicMethodBuilder + where TDelegate : Delegate + { + private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(new KeyComparer()); + + /// + /// Gets a previously cache delegate used to call the specified method, + /// or creates and caches a new delegate if not found. + /// + /// The that contains the method. + /// The name of the method. + /// The OpCode to use in the method call. + /// The method's return type. + /// optional types for the method parameters + /// optional generic type arguments for a generic method + /// A that can be used to execute the dynamic method. + public static TDelegate GetOrCreateMethodCallDelegate( + Type type, + string methodName, + OpCodeValue callOpCode, + Type returnType = null, + Type[] methodParameterTypes = null, + Type[] methodGenericArguments = null) => + Cache.GetOrAdd( + new Key(type, methodName, callOpCode, returnType, methodParameterTypes, methodGenericArguments), + key => CreateMethodCallDelegate( + key.Type, + key.MethodName, + key.CallOpCode, + key.MethodParameterTypes, + key.MethodGenericArguments)); + + /// + /// Creates a simple using that + /// calls a method with the specified name and parameter types. + /// + /// The that contains the method to call when the returned delegate is executed.. + /// The name of the method to call when the returned delegate is executed. + /// The OpCode to use in the method call. + /// If not null, use method overload that matches the specified parameters. + /// If not null, use method overload that has the same number of generic arguments. + /// A that can be used to execute the dynamic method. + public static TDelegate CreateMethodCallDelegate( + Type type, + string methodName, + OpCodeValue callOpCode, + Type[] methodParameterTypes = null, + Type[] methodGenericArguments = null) + { + var delegateType = typeof(TDelegate); + var genericTypeArguments = delegateType.GenericTypeArguments; + + Type[] parameterTypes; + Type returnType; + + if (delegateType.Name.StartsWith("Func`")) + { + // last generic type argument is the return type + var parameterCount = genericTypeArguments.Length - 1; + parameterTypes = new Type[parameterCount]; + Array.Copy(genericTypeArguments, parameterTypes, parameterCount); + + returnType = genericTypeArguments[parameterCount]; + } + else if (delegateType.Name.StartsWith("Action`")) + { + parameterTypes = genericTypeArguments; + returnType = typeof(void); + } + else + { + throw new Exception($"Only Func<> or Action<> are supported in {nameof(CreateMethodCallDelegate)}."); + } + + // find any method that matches by name and parameter types + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) + .Where(m => m.Name == methodName); + + // if methodParameterTypes was specified, check for a method that matches + if (methodParameterTypes != null) + { + methods = methods.Where( + m => + { + var ps = m.GetParameters(); + if (ps.Length != methodParameterTypes.Length) + { + return false; + } + + for (var i = 0; i < ps.Length; i++) + { + var t1 = ps[i].ParameterType; + var t2 = methodParameterTypes[i]; + + // generics can be tricky to compare for type equality + // so we will just check the namespace and name + if (t1.Namespace != t2.Namespace || t1.Name != t2.Name) + { + return false; + } + } + + return true; + }); + } + + if (methodGenericArguments != null) + { + methods = methods.Where( + m => m.IsGenericMethodDefinition && + m.GetGenericArguments().Length == methodGenericArguments.Length); + } + + var methodInfo = methods.FirstOrDefault(); + if (methodInfo == null) + { + // method not found + // TODO: logging + return null; + } + + if (methodGenericArguments != null) + { + methodInfo = methodInfo.MakeGenericMethod(methodGenericArguments); + } + + Type[] effectiveParameterTypes; + + var reflectedParameterTypes = methodInfo.GetParameters() + .Select(p => p.ParameterType); + if (methodInfo.IsStatic) + { + effectiveParameterTypes = reflectedParameterTypes.ToArray(); + } + else + { + // for instance methods, insert object's type as first element in array + effectiveParameterTypes = new[] { type } + .Concat(reflectedParameterTypes) + .ToArray(); + } + + var dynamicMethod = new DynamicMethod(methodInfo.Name, returnType, parameterTypes, ObjectExtensions.Module, skipVisibility: true); + var il = dynamicMethod.GetILGenerator(); + + // load each argument and cast or unbox as necessary + for (ushort argumentIndex = 0; argumentIndex < parameterTypes.Length; argumentIndex++) + { + var delegateParameterType = parameterTypes[argumentIndex]; + var underlyingParameterType = effectiveParameterTypes[argumentIndex]; + + switch (argumentIndex) + { + case 0: + il.Emit(OpCodes.Ldarg_0); + break; + case 1: + il.Emit(OpCodes.Ldarg_1); + break; + case 2: + il.Emit(OpCodes.Ldarg_2); + break; + case 3: + il.Emit(OpCodes.Ldarg_3); + break; + default: + il.Emit(OpCodes.Ldarg_S, argumentIndex); + break; + } + + if (underlyingParameterType.IsValueType && delegateParameterType == typeof(object)) + { + il.Emit(OpCodes.Unbox_Any, underlyingParameterType); + } + else if (underlyingParameterType != delegateParameterType) + { + il.Emit(OpCodes.Castclass, underlyingParameterType); + } + } + + if (callOpCode == OpCodeValue.Call || methodInfo.IsStatic) + { + // non-virtual call (e.g. static method, or method override calling overriden implementation) + il.Emit(OpCodes.Call, methodInfo); + } + else if (callOpCode == OpCodeValue.Callvirt) + { + // Note: C# compiler uses CALLVIRT for non-virtual + // instance methods to get the cheap null check + il.Emit(OpCodes.Callvirt, methodInfo); + } + else + { + throw new NotSupportedException($"OpCode {callOpCode} not supported when calling a method."); + } + + if (methodInfo.ReturnType.IsValueType && !returnType.IsValueType) + { + il.Emit(OpCodes.Box, methodInfo.ReturnType); + } + else if (methodInfo.ReturnType.IsValueType && returnType.IsValueType && methodInfo.ReturnType != returnType) + { + throw new ArgumentException($"Cannot convert the target method's return type {methodInfo.ReturnType.FullName} (valuetype) to the delegate method's return type {returnType.FullName} (valuetype)"); + } + else if (!methodInfo.ReturnType.IsValueType && returnType.IsValueType) + { + throw new ArgumentException($"Cannot reliably convert the target method's return type {methodInfo.ReturnType.FullName} (reference type) to the delegate method's return type {returnType.FullName} (value type)"); + } + else if (!methodInfo.ReturnType.IsValueType && !returnType.IsValueType && methodInfo.ReturnType != returnType) + { + il.Emit(OpCodes.Castclass, returnType); + } + + il.Emit(OpCodes.Ret); + return (TDelegate)dynamicMethod.CreateDelegate(delegateType); + } + + private struct Key + { + public readonly Type Type; + public readonly string MethodName; + public readonly OpCodeValue CallOpCode; + public readonly Type ReturnType; + public readonly Type[] MethodParameterTypes; + public readonly Type[] MethodGenericArguments; + + public Key(Type type, string methodName, OpCodeValue callOpCode, Type returnType, Type[] methodParameterTypes, Type[] methodGenericArguments) + { + Type = type; + MethodName = methodName; + CallOpCode = callOpCode; + ReturnType = returnType; + MethodParameterTypes = methodParameterTypes; + MethodGenericArguments = methodGenericArguments; + } + } + + private class KeyComparer : IEqualityComparer + { + public bool Equals(Key x, Key y) + { + if (!object.Equals(x.Type, y.Type)) + { + return false; + } + + if (!object.Equals(x.MethodName, y.MethodName)) + { + return false; + } + + if (!object.Equals(x.CallOpCode, y.CallOpCode)) + { + return false; + } + + if (!object.Equals(x.ReturnType, y.ReturnType)) + { + return false; + } + + if (!ArrayEquals(x.MethodParameterTypes, y.MethodParameterTypes)) + { + return false; + } + + if (!ArrayEquals(x.MethodGenericArguments, y.MethodGenericArguments)) + { + return false; + } + + return true; + } + + public int GetHashCode(Key obj) + { + unchecked + { + var hash = 17; + + if (obj.Type != null) + { + hash = (hash * 23) + obj.Type.GetHashCode(); + } + + if (obj.MethodName != null) + { + hash = (hash * 23) + obj.MethodName.GetHashCode(); + } + + hash = (hash * 23) + obj.CallOpCode.GetHashCode(); + + if (obj.MethodParameterTypes != null) + { + foreach (var t in obj.MethodParameterTypes) + { + if (t != null) + { + hash = (hash * 23) + t.GetHashCode(); + } + } + } + + if (obj.MethodGenericArguments != null) + { + foreach (var t in obj.MethodGenericArguments) + { + if (t != null) + { + hash = (hash * 23) + t.GetHashCode(); + } + } + } + + return hash; + } + } + + private static bool ArrayEquals(T[] array1, T[] array2) + { + if (array1 == null && array2 == null) + { + return true; + } + + if (array1 == null || array2 == null) + { + return false; + } + + return ((IStructuralEquatable)array1).Equals(array2, EqualityComparer.Default); + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/Interception.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/Interception.cs new file mode 100644 index 000000000..f27153529 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/Interception.cs @@ -0,0 +1,57 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Diagnostics; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + internal static class Interception + { + internal const Type[] NullTypeArray = null; + internal static readonly object[] NoArgObjects = Array.Empty(); + internal static readonly Type[] NoArgTypes = Type.EmptyTypes; + internal static readonly Type VoidType = typeof(void); + + internal static Type[] ParamsToTypes(params object[] objectsToCheck) + { + var types = new Type[objectsToCheck.Length]; + + for (var i = 0; i < objectsToCheck.Length; i++) + { + types[i] = objectsToCheck[i]?.GetType(); + } + + return types; + } + + internal static string MethodKey( + Type owningType, + Type returnType, + Type[] genericTypes, + Type[] parameterTypes) + { + var key = $"{owningType?.AssemblyQualifiedName}_m_r{returnType?.AssemblyQualifiedName}"; + + for (ushort i = 0; i < (genericTypes?.Length ?? 0); i++) + { + Debug.Assert(genericTypes != null, nameof(genericTypes) + " != null"); + key = string.Concat(key, $"_g{genericTypes[i].AssemblyQualifiedName}"); + } + + for (ushort i = 0; i < (parameterTypes?.Length ?? 0); i++) + { + Debug.Assert(parameterTypes != null, nameof(parameterTypes) + " != null"); + key = string.Concat(key, $"_p{parameterTypes[i].AssemblyQualifiedName}"); + } + + return key; + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/MemberResult.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/MemberResult.cs new file mode 100644 index 000000000..ecfaac5e4 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/MemberResult.cs @@ -0,0 +1,84 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + internal readonly struct MemberResult + { + /// + /// A static value used to represent a member that was not found. + /// + public static readonly MemberResult NotFound = default; + + public readonly bool HasValue; + + private readonly T _value; + + public MemberResult(T value) + { + _value = value; + HasValue = true; + } + + public T Value => + HasValue + ? _value + : throw new InvalidOperationException("Reflected member not found."); + + public T GetValueOrDefault() => _value; + + public MemberResult GetProperty(string propertyName) + { + if (!HasValue || Value == null || !Value.TryGetPropertyValue(propertyName, out TResult result)) + { + return MemberResult.NotFound; + } + + return new MemberResult(result); + } + + public MemberResult GetProperty(string propertyName) => GetProperty(propertyName); + + public MemberResult GetField(string fieldName) + { + if (!HasValue || Value == null || !Value.TryGetFieldValue(fieldName, out TResult result)) + { + return MemberResult.NotFound; + } + + return new MemberResult(result); + } + + public MemberResult GetField(string fieldName) => GetField(fieldName); + + public MemberResult CallMethod(string methodName, TArg1 arg1) + { + if (!HasValue || Value == null || !Value.TryCallMethod(methodName, arg1, out TResult result)) + { + return MemberResult.NotFound; + } + + return new MemberResult(result); + } + + public MemberResult CallMethod(string methodName, TArg1 arg1) => CallMethod(methodName, arg1); + + public override string ToString() + { + if (!HasValue || Value == null) + { + return string.Empty; + } + + return Value.ToString(); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs new file mode 100644 index 000000000..3c32ad8e4 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/MethodBuilder.cs @@ -0,0 +1,823 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.InteropServices; +using Elastic.Apm.Logging; +using Elastic.Apm.Profiler.Managed.Core; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + internal class MethodBuilder + where TDelegate : Delegate + { + /// + /// Global dictionary for caching reflected delegates + /// + private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(new KeyComparer()); + + /// + /// Feature flag used primarily for forcing testing of the token lookup strategy. + /// + // ReSharper disable once StaticMemberInGenericType + private static readonly bool ForceMdTokenLookup; + + /// + /// Feature flag used primarily for forcing testing of the fallback lookup strategy. + /// + // ReSharper disable once StaticMemberInGenericType + private static readonly bool ForceFallbackLookup; + + private readonly Module _resolutionModule; + private readonly int _mdToken; + private readonly int _originalOpCodeValue; + private readonly OpCodeValue _opCode; + private readonly string _methodName; + private readonly Guid? _moduleVersionId; + + private Type _returnType; + private MethodBase _methodBase; + private Type _concreteType; + private string _concreteTypeName; + private Type[] _parameters = Array.Empty(); + private Type[] _explicitParameterTypes = null; + private string[] _namespaceAndNameFilter = null; + private Type[] _declaringTypeGenerics; + private Type[] _methodGenerics; + private bool _forceMethodDefResolve; + + static MethodBuilder() + { + // TODO: implement environment variables for these? + // ForceMdTokenLookup = bool.TryParse(EnvironmentHelpers.GetEnvironmentVariable(ConfigurationKeys.Debug.ForceMdTokenLookup), out bool result) + // ? result + // : false; + // ForceFallbackLookup = bool.TryParse(EnvironmentHelpers.GetEnvironmentVariable(ConfigurationKeys.Debug.ForceFallbackLookup), out result) + // ? result && !ForceMdTokenLookup + // : false; + } + + private MethodBuilder(Guid moduleVersionId, int mdToken, int opCode, string methodName) + : this(ModuleLookup.Get(moduleVersionId), mdToken, opCode, methodName) => + // Save the Guid for logging purposes + _moduleVersionId = moduleVersionId; + + private MethodBuilder(Module resolutionModule, int mdToken, int opCode, string methodName) + { + _resolutionModule = resolutionModule; + _mdToken = mdToken; + _opCode = (OpCodeValue)opCode; + _originalOpCodeValue = opCode; + _methodName = methodName; + _forceMethodDefResolve = false; + } + + public static MethodBuilder Start(Guid moduleVersionId, int mdToken, int opCode, string methodName) => + new MethodBuilder(moduleVersionId, mdToken, opCode, methodName); + + public static MethodBuilder Start(Module module, int mdToken, int opCode, string methodName) => + new MethodBuilder( + module, + mdToken, + opCode, + methodName); + + public static MethodBuilder Start(long moduleVersionPtr, int mdToken, int opCode, string methodName) => + new MethodBuilder( + Marshal.PtrToStructure(new IntPtr(moduleVersionPtr)), + mdToken, + opCode, + methodName); + + public MethodBuilder WithConcreteType(Type type) + { + _concreteType = type; + _concreteTypeName = type?.FullName; + return this; + } + + public MethodBuilder WithNamespaceAndNameFilters(params string[] namespaceNameFilters) + { + _namespaceAndNameFilter = namespaceNameFilters; + return this; + } + + public MethodBuilder WithParameters(params Type[] parameters) + { + _parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + return this; + } + + public MethodBuilder WithParameters(params object[] parameters) + { + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); + + return WithParameters(Interception.ParamsToTypes(parameters)); + } + + public MethodBuilder WithParameters(TParam param1) + { + var types = new[] { param1?.GetType() }; + + return WithParameters(types); + } + + public MethodBuilder WithParameters(TParam1 param1, TParam2 param2) + { + var types = new[] { param1?.GetType(), param2?.GetType() }; + + return WithParameters(types); + } + + public MethodBuilder WithParameters(TParam1 param1, TParam2 param2, TParam3 param3) + { + var types = new[] { param1?.GetType(), param2?.GetType(), param3?.GetType() }; + + return WithParameters(types); + } + + public MethodBuilder WithParameters(TParam1 param1, TParam2 param2, TParam3 param3, TParam4 param4) + { + var types = new[] { param1?.GetType(), param2?.GetType(), param3?.GetType(), param4?.GetType() }; + + return WithParameters(types); + } + + public MethodBuilder WithExplicitParameterTypes(params Type[] types) + { + _explicitParameterTypes = types; + return this; + } + + public MethodBuilder WithMethodGenerics(params Type[] generics) + { + _methodGenerics = generics; + return this; + } + + public MethodBuilder WithDeclaringTypeGenerics(params Type[] generics) + { + _declaringTypeGenerics = generics; + return this; + } + + public MethodBuilder ForceMethodDefinitionResolution() + { + _forceMethodDefResolve = true; + return this; + } + + public MethodBuilder WithReturnType(Type returnType) + { + _returnType = returnType; + return this; + } + + public TDelegate Build() + { + var cacheKey = new Key( + this, + callingModule: _resolutionModule); + + return Cache.GetOrAdd(cacheKey, key => + { + // Validate requirements at the last possible moment + // Don't do more than needed before checking the cache + key.Builder.ValidateRequirements(); + return key.Builder.EmitDelegate(); + }); + } + + private TDelegate EmitDelegate() + { + var requiresBestEffortMatching = false; + + if (_resolutionModule != null) + { + try + { + // Don't resolve until we build, as it may be an unnecessary lookup because of the cache + // We also may need the generics which were specified + if (_forceMethodDefResolve || (_declaringTypeGenerics == null && _methodGenerics == null)) + { + _methodBase = + _resolutionModule.ResolveMethod(metadataToken: _mdToken); + } + else + { + _methodBase = + _resolutionModule.ResolveMethod( + metadataToken: _mdToken, + genericTypeArguments: _declaringTypeGenerics, + genericMethodArguments: _methodGenerics); + } + } + catch (Exception ex) + { + Logger.Log(LogLevel.Error, ex, "Unable to resolve method {0}.{1} by metadata token: {2}", + _concreteType, + _methodName, + _mdToken); + requiresBestEffortMatching = true; + } + } + else + { + Logger.Log(LogLevel.Warn, "Unable to resolve module version id {0}. Using method builder fallback.", _moduleVersionId); + } + + MethodInfo methodInfo = null; + + if (!requiresBestEffortMatching && _methodBase is MethodInfo info) + { + if (info.IsGenericMethodDefinition) + { + info = MakeGenericMethod(info); + } + + methodInfo = VerifyMethodFromToken(info); + } + + if (methodInfo == null && ForceMdTokenLookup) + { + throw new Exception($"Unable to resolve method {_concreteTypeName}.{_methodName} by metadata token: {_mdToken}. Exiting because {nameof(ForceMdTokenLookup)}() is true."); + } + else if (methodInfo == null || ForceFallbackLookup) + { + // mdToken didn't work out, fallback + methodInfo = TryFindMethod(); + } + + var delegateType = typeof(TDelegate); + var delegateGenericArgs = delegateType.GenericTypeArguments; + + Type[] delegateParameterTypes; + Type returnType; + + if (delegateType.Name.StartsWith("Func`")) + { + // last generic type argument is the return type + var parameterCount = delegateGenericArgs.Length - 1; + delegateParameterTypes = new Type[parameterCount]; + Array.Copy(delegateGenericArgs, delegateParameterTypes, parameterCount); + + returnType = delegateGenericArgs[parameterCount]; + } + else if (delegateType.Name.StartsWith("Action`")) + { + delegateParameterTypes = delegateGenericArgs; + returnType = typeof(void); + } + else + { + throw new Exception($"Only Func<> or Action<> are supported in {nameof(MethodBuilder)}."); + } + + if (methodInfo.IsGenericMethodDefinition) + { + methodInfo = MakeGenericMethod(methodInfo); + } + + Type[] effectiveParameterTypes; + + var reflectedParameterTypes = + methodInfo.GetParameters().Select(p => p.ParameterType); + + if (methodInfo.IsStatic) + { + effectiveParameterTypes = reflectedParameterTypes.ToArray(); + } + else + { + // for instance methods, insert object's type as first element in array + effectiveParameterTypes = new[] { _concreteType } + .Concat(reflectedParameterTypes) + .ToArray(); + } + + var dynamicMethod = new DynamicMethod(methodInfo.Name, returnType, delegateParameterTypes, ObjectExtensions.Module, skipVisibility: true); + var il = dynamicMethod.GetILGenerator(); + + // load each argument and cast or unbox as necessary + for (ushort argumentIndex = 0; argumentIndex < delegateParameterTypes.Length; argumentIndex++) + { + var delegateParameterType = delegateParameterTypes[argumentIndex]; + var underlyingParameterType = effectiveParameterTypes[argumentIndex]; + + switch (argumentIndex) + { + case 0: + il.Emit(OpCodes.Ldarg_0); + break; + case 1: + il.Emit(OpCodes.Ldarg_1); + break; + case 2: + il.Emit(OpCodes.Ldarg_2); + break; + case 3: + il.Emit(OpCodes.Ldarg_3); + break; + default: + il.Emit(OpCodes.Ldarg_S, argumentIndex); + break; + } + + if (underlyingParameterType.IsValueType && delegateParameterType == typeof(object)) + { + il.Emit(OpCodes.Unbox_Any, underlyingParameterType); + } + else if (underlyingParameterType != delegateParameterType) + { + il.Emit(OpCodes.Castclass, underlyingParameterType); + } + } + + if (_opCode == OpCodeValue.Call || methodInfo.IsStatic) + { + // non-virtual call (e.g. static method, or method override calling overriden implementation) + il.Emit(OpCodes.Call, methodInfo); + } + else if (_opCode == OpCodeValue.Callvirt) + { + // Note: C# compiler uses CALLVIRT for non-virtual + // instance methods to get the cheap null check + il.Emit(OpCodes.Callvirt, methodInfo); + } + else + { + throw new NotSupportedException($"OpCode {_originalOpCodeValue} not supported when calling a method."); + } + + if (methodInfo.ReturnType.IsValueType && !returnType.IsValueType) + { + il.Emit(OpCodes.Box, methodInfo.ReturnType); + } + else if (methodInfo.ReturnType.IsValueType && returnType.IsValueType && methodInfo.ReturnType != returnType) + { + throw new ArgumentException($"Cannot convert the target method's return type {methodInfo.ReturnType.FullName} (value type) to the delegate method's return type {returnType.FullName} (value type)"); + } + else if (!methodInfo.ReturnType.IsValueType && returnType.IsValueType) + { + throw new ArgumentException($"Cannot reliably convert the target method's return type {methodInfo.ReturnType.FullName} (reference type) to the delegate method's return type {returnType.FullName} (value type)"); + } + else if (!methodInfo.ReturnType.IsValueType && !returnType.IsValueType && methodInfo.ReturnType != returnType) + { + il.Emit(OpCodes.Castclass, returnType); + } + + il.Emit(OpCodes.Ret); + return (TDelegate)dynamicMethod.CreateDelegate(typeof(TDelegate)); + } + + private MethodInfo MakeGenericMethod(MethodInfo methodInfo) + { + if (_methodGenerics == null || _methodGenerics.Length == 0) + { + throw new ArgumentException($"Must specify {nameof(_methodGenerics)} for a generic method."); + } + + return methodInfo.MakeGenericMethod(_methodGenerics); + } + + private MethodInfo VerifyMethodFromToken(MethodInfo methodInfo) + { + // Verify baselines to ensure this isn't the wrong method somehow + var detailMessage = $"Unexpected method: {_concreteTypeName}.{_methodName} received for mdToken: {_mdToken} in module: {_resolutionModule?.FullyQualifiedName ?? "NULL"}, {_resolutionModule?.ModuleVersionId ?? _moduleVersionId}"; + + if (!string.Equals(_methodName, methodInfo.Name)) + { + Logger.Log(LogLevel.Warn, $"Method name mismatch: {detailMessage}"); + return null; + } + + if (!GenericsAreViable(methodInfo)) + { + Logger.Log(LogLevel.Warn, $"Generics not viable: {detailMessage}"); + return null; + } + + if (!ParametersAreViable(methodInfo)) + { + Logger.Log(LogLevel.Warn, $"Parameters not viable: {detailMessage}"); + return null; + } + + if (!methodInfo.IsStatic && !methodInfo.ReflectedType.IsAssignableFrom(_concreteType)) + { + Logger.Log(LogLevel.Warn, $"{_concreteType} cannot be assigned to the type containing the MethodInfo representing the instance method: {detailMessage}"); + return null; + } + + return methodInfo; + } + + private void ValidateRequirements() + { + if (_concreteType == null) + { + throw new ArgumentException($"{nameof(_concreteType)} must be specified."); + } + + if (string.IsNullOrWhiteSpace(_methodName)) + { + throw new ArgumentException($"There must be a {nameof(_methodName)} specified to ensure fallback {nameof(TryFindMethod)} is viable."); + } + + if (_namespaceAndNameFilter != null && _namespaceAndNameFilter.Length != _parameters.Length + 1) + { + throw new ArgumentException($"The length of {nameof(_namespaceAndNameFilter)} must match the length of {nameof(_parameters)} + 1 for the return type."); + } + + if (_explicitParameterTypes != null) + { + if (_explicitParameterTypes.Length != _parameters.Length) + { + throw new ArgumentException($"The {nameof(_explicitParameterTypes)} must match the {_parameters} count."); + } + + for (var i = 0; i < _explicitParameterTypes.Length; i++) + { + var explicitType = _explicitParameterTypes[i]; + var parameterType = _parameters[i]; + + if (parameterType == null) + { + // Nothing to check + continue; + } + + if (!explicitType.IsAssignableFrom(parameterType)) + { + throw new ArgumentException($"Parameter Index {i}: Explicit type {explicitType.FullName} is not assignable from {parameterType}"); + } + } + } + } + + private MethodInfo TryFindMethod() + { + var logDetail = $"mdToken {_mdToken} on {_concreteTypeName}.{_methodName} in {_resolutionModule?.FullyQualifiedName ?? "NULL"}, {_resolutionModule?.ModuleVersionId ?? _moduleVersionId}"; + Logger.Log(LogLevel.Warn, $"Using fallback method matching ({logDetail})"); + + var methods = + _concreteType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + + // A legacy fallback attempt to match on the concrete type + methods = + methods + .Where(mi => mi.Name == _methodName && (_returnType == null || mi.ReturnType == _returnType)) + .ToArray(); + + var matchesOnNameAndReturn = methods.Length; + + if (_namespaceAndNameFilter != null) + { + methods = methods.Where(m => + { + var parameters = m.GetParameters(); + + if ((parameters.Length + 1) != _namespaceAndNameFilter.Length) + { + return false; + } + + var typesToCheck = new Type[] { m.ReturnType }.Concat(m.GetParameters().Select(p => p.ParameterType)).ToArray(); + for (var i = 0; i < typesToCheck.Length; i++) + { + if (_namespaceAndNameFilter[i] == ClrTypeNames.Ignore) + { + // Allow for not specifying + continue; + } + + if ($"{typesToCheck[i].Namespace}.{typesToCheck[i].Name}" != _namespaceAndNameFilter[i]) + { + return false; + } + } + + return true; + }).ToArray(); + } + + if (methods.Length == 1) + { + Logger.Log(LogLevel.Info, $"Resolved by name and namespaceName filters ({logDetail})"); + return methods[0]; + } + + methods = + methods + .Where(ParametersAreViable) + .ToArray(); + + if (methods.Length == 1) + { + Logger.Log(LogLevel.Info, $"Resolved by viable parameters ({logDetail})"); + return methods[0]; + } + + methods = + methods + .Where(GenericsAreViable) + .ToArray(); + + if (methods.Length == 1) + { + Logger.Log(LogLevel.Info, $"Resolved by viable generics ({logDetail})"); + return methods[0]; + } + + // Attempt to trim down further + methods = methods.Where(ParametersAreExact).ToArray(); + + if (methods.Length > 1) + { + throw new ArgumentException($"Unable to safely resolve method, found {methods.Length} matches ({logDetail})"); + } + + var methodInfo = methods.SingleOrDefault(); + + if (methodInfo == null) + { + throw new ArgumentException($"Unable to resolve method, started with {matchesOnNameAndReturn} by name match ({logDetail})"); + } + + return methodInfo; + } + + private bool ParametersAreViable(MethodInfo mi) + { + var parameters = mi.GetParameters(); + + if (parameters.Length != _parameters.Length) + { + // expected parameters don't match actual count + return false; + } + + for (var i = 0; i < parameters.Length; i++) + { + var candidateParameter = parameters[i]; + + var parameterType = candidateParameter.ParameterType; + + var expectedParameterType = GetExpectedParameterTypeByIndex(i); + + if (expectedParameterType == null) + { + // Skip the rest of this check, as we can't know the type + continue; + } + + if (parameterType.IsGenericParameter) + { + // This requires different evaluation + if (MeetsGenericArgumentRequirements(parameterType, expectedParameterType)) + { + // Good to go + continue; + } + + // We didn't meet this generic argument's requirements + return false; + } + + if (!parameterType.IsAssignableFrom(expectedParameterType)) + { + return false; + } + } + + return true; + } + + private bool ParametersAreExact(MethodInfo mi) + { + // We can already assume that the counts match by this point + var parameters = mi.GetParameters(); + + for (var i = 0; i < parameters.Length; i++) + { + var candidateParameter = parameters[i]; + + var parameterType = candidateParameter.ParameterType; + + var actualArgumentType = GetExpectedParameterTypeByIndex(i); + + if (actualArgumentType == null) + { + // Skip the rest of this check, as we can't know the type + continue; + } + + if (parameterType != actualArgumentType) + { + return false; + } + } + + return true; + } + + private Type GetExpectedParameterTypeByIndex(int i) => + _explicitParameterTypes != null + ? _explicitParameterTypes[i] + : _parameters[i]; + + private bool GenericsAreViable(MethodInfo mi) + { + // Non-Generic Method - { IsGenericMethod: false, ContainsGenericParameters: false, IsGenericMethodDefinition: false } + // Generic Method Definition - { IsGenericMethod: true, ContainsGenericParameters: true, IsGenericMethodDefinition: true } + // Open Constructed Method - { IsGenericMethod: true, ContainsGenericParameters: true, IsGenericMethodDefinition: false } + // Closed Constructed Method - { IsGenericMethod: true, ContainsGenericParameters: false, IsGenericMethodDefinition: false } + + if (_methodGenerics == null) + { + // We expect no generic arguments for this method + return mi.ContainsGenericParameters == false; + } + + if (!mi.IsGenericMethod) + { + // There is really nothing to compare here + // Make sure we aren't looking for generics where there aren't + return _methodGenerics?.Length == 0; + } + + var genericArgs = mi.GetGenericArguments(); + + if (genericArgs.Length != _methodGenerics.Length) + { + // Count of arguments mismatch + return false; + } + + foreach (var actualGenericArg in genericArgs) + { + if (actualGenericArg.IsGenericParameter) + { + var expectedGenericArg = _methodGenerics[actualGenericArg.GenericParameterPosition]; + + if (!MeetsGenericArgumentRequirements(actualGenericArg, expectedGenericArg)) + { + return false; + } + } + } + + return true; + } + + private bool MeetsGenericArgumentRequirements(Type actualGenericArg, Type expectedArg) + { + var constraints = actualGenericArg.GetGenericParameterConstraints(); + + if (constraints.Any(constraint => !constraint.IsAssignableFrom(expectedArg))) + { + // We have failed to meet a constraint + return false; + } + + return true; + } + + private struct Key + { + public readonly int CallingModuleMetadataToken; + public readonly MethodBuilder Builder; + + public Key( + MethodBuilder builder, + Module callingModule) + { + Builder = builder; + CallingModuleMetadataToken = callingModule.MetadataToken; + } + + public Type[] ExplicitParams => Builder._explicitParameterTypes ?? Builder._parameters; + } + + private class KeyComparer : IEqualityComparer + { + public bool Equals(Key x, Key y) + { + if (x.CallingModuleMetadataToken != y.CallingModuleMetadataToken) + { + return false; + } + + var builder1 = x.Builder; + var builder2 = y.Builder; + + if (builder1._mdToken != builder2._mdToken) + { + return false; + } + + if (builder1._opCode != builder2._opCode) + { + return false; + } + + if (builder1._concreteType != builder2._concreteType) + { + return false; + } + + if (!ArrayEquals(x.ExplicitParams, y.ExplicitParams)) + { + return false; + } + + if (!ArrayEquals(builder1._methodGenerics, builder2._methodGenerics)) + { + return false; + } + + if (!ArrayEquals(builder1._declaringTypeGenerics, builder2._declaringTypeGenerics)) + { + return false; + } + + return true; + } + + public int GetHashCode(Key obj) + { + unchecked + { + var builder = obj.Builder; + + var hash = 17; + hash = (hash * 23) + obj.CallingModuleMetadataToken.GetHashCode(); + hash = (hash * 23) + builder._mdToken.GetHashCode(); + hash = (hash * 23) + ((short)builder._opCode).GetHashCode(); + hash = (hash * 23) + builder._concreteType.GetHashCode(); + hash = (hash * 23) + GetHashCode(builder._methodGenerics); + hash = (hash * 23) + GetHashCode(obj.ExplicitParams); + hash = (hash * 23) + GetHashCode(builder._declaringTypeGenerics); + return hash; + } + } + + private static int GetHashCode(Type[] array) + { + if (array == null) + { + return 0; + } + + var value = array.Length; + + for (var i = 0; i < array.Length; i++) + { + value = unchecked((value * 31) + array[i]?.GetHashCode() ?? 0); + } + + return value; + } + + private static bool ArrayEquals(Type[] array1, Type[] array2) + { + if (array1 == null) + { + return array2 == null; + } + + if (array2 == null) + { + return false; + } + + if (array1.Length != array2.Length) + { + return false; + } + + for (var i = 0; i < array1.Length; i++) + { + if (array1[i] != array2[i]) + { + return false; + } + } + + return true; + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/ModuleLookup.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/ModuleLookup.cs new file mode 100644 index 000000000..f921fd962 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/ModuleLookup.cs @@ -0,0 +1,98 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + internal static class ModuleLookup + { + /// + /// Some naive upper limit to resolving assemblies that we can use to stop making expensive calls. + /// + private const int MaxFailures = 50; + + private static ManualResetEventSlim _populationResetEvent = new ManualResetEventSlim(initialState: true); + private static ConcurrentDictionary _modules = new ConcurrentDictionary(); + + private static int _failures = 0; + private static bool _shortCircuitLogicHasLogged = false; + + public static Module GetByPointer(long moduleVersionPointer) => + Get(Marshal.PtrToStructure(new IntPtr(moduleVersionPointer))); + + public static Module Get(Guid moduleVersionId) + { + // First attempt at cached values with no blocking + if (_modules.TryGetValue(moduleVersionId, out var value)) + { + return value; + } + + // Block if a population event is happening + _populationResetEvent.Wait(); + + // See if the previous population event populated what we need + if (_modules.TryGetValue(moduleVersionId, out value)) + { + return value; + } + + if (_failures >= MaxFailures) + { + // For some unforeseeable reason we have failed on a lot of AppDomain lookups + if (!_shortCircuitLogicHasLogged) + { + Logger.Log(LogLevel.Warn, "Elastic APM is unable to continue attempting module lookups for this AppDomain. Falling back to legacy method lookups."); + } + + return null; + } + + // Block threads on this event + _populationResetEvent.Reset(); + + try + { + PopulateModules(); + } + catch (Exception ex) + { + _failures++; + Logger.Log(LogLevel.Error, ex, "Error when populating modules."); + } + finally + { + // Continue threads blocked on this event + _populationResetEvent.Set(); + } + + _modules.TryGetValue(moduleVersionId, out value); + + return value; + } + + private static void PopulateModules() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + foreach (var module in assembly.Modules) + { + _modules.TryAdd(module.ModuleVersionId, module); + } + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/ObjectExtensions.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/ObjectExtensions.cs new file mode 100644 index 000000000..b6b4bc907 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/ObjectExtensions.cs @@ -0,0 +1,312 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Emit; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + /// + /// Provides helper methods to access object members by emitting IL dynamically. + /// + internal static class ObjectExtensions + { + // A new module to be emitted in the current AppDomain which will contain DynamicMethods + // and have same evidence/permissions as this AppDomain + internal static readonly ModuleBuilder Module; + + private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary PropertyFetcherCache = new ConcurrentDictionary(); + + static ObjectExtensions() + { +#if NETFRAMEWORK + var asm = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Elastic.Apm.Profiler.Managed.DynamicAssembly"), AssemblyBuilderAccess.Run); +#else + var asm = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Elastic.Apm.Profiler.Managed.DynamicAssembly"), AssemblyBuilderAccess.Run); +#endif + Module = asm.DefineDynamicModule("DynamicModule"); + } + + /// + /// Tries to call an instance method with the specified name, a single parameter, and a return value. + /// + /// The type of the method's single parameter. + /// The type of the method's result value. + /// The object to call the method on. + /// The name of the method to call. + /// The value to pass as the method's single argument. + /// The value returned by the method. + /// true if the method was found, false otherwise. + public static bool TryCallMethod(this object source, string methodName, TArg1 arg1, out TResult value) + { + var type = source.GetType(); + var paramType1 = typeof(TArg1); + var returnType = typeof(TResult); + + var cachedItem = Cache.GetOrAdd( + new PropertyFetcherCacheKey(type, paramType1, returnType, methodName), + key => + DynamicMethodBuilder> + .CreateMethodCallDelegate( + key.Type1, + key.Name, + OpCodeValue.Callvirt, + methodParameterTypes: new[] { key.Type2 })); + + if (cachedItem is Func func) + { + value = func(source, arg1); + return true; + } + + value = default; + return false; + } + + /// + /// Tries to call an instance method with the specified name, two parameters, and no return value. + /// + /// The type of the method's first parameter. + /// The type of the method's second parameter. + /// The object to call the method on. + /// The name of the method to call. + /// The value to pass as the method's first argument. + /// The value to pass as the method's second argument. + /// true if the method was found, false otherwise. + public static bool TryCallVoidMethod(this object source, string methodName, TArg1 arg1, TArg2 arg2) + { + var type = source.GetType(); + var paramType1 = typeof(TArg1); + var paramType2 = typeof(TArg2); + + var cachedItem = Cache.GetOrAdd( + new PropertyFetcherCacheKey(type, paramType1, paramType2, methodName), + key => + DynamicMethodBuilder> + .CreateMethodCallDelegate( + key.Type1, + key.Name, + OpCodeValue.Callvirt, + methodParameterTypes: new[] { key.Type2, key.Type3 })); + + if (cachedItem is Action func) + { + func(source, arg1, arg2); + return true; + } + + return false; + } + + /// + /// Tries to call an instance method with the specified name and a return value. + /// + /// The type of the method's result value. + /// The object to call the method on. + /// The name of the method to call. + /// The value returned by the method. + /// true if the method was found, false otherwise. + public static bool TryCallMethod(this object source, string methodName, out TResult value) + { + var type = source.GetType(); + var returnType = typeof(TResult); + + var cachedItem = Cache.GetOrAdd( + new PropertyFetcherCacheKey(type, returnType, methodName), + key => + DynamicMethodBuilder> + .CreateMethodCallDelegate( + key.Type1, + key.Name, + OpCodeValue.Callvirt)); + + if (cachedItem is Func func) + { + value = func(source); + return true; + } + + value = default; + return false; + } + + public static MemberResult CallMethod(this object source, string methodName, TArg1 arg1) => + source.TryCallMethod(methodName, arg1, out TResult result) + ? new MemberResult(result) + : MemberResult.NotFound; + + public static MemberResult CallMethod(this object source, string methodName) => + source.TryCallMethod(methodName, out TResult result) + ? new MemberResult(result) + : MemberResult.NotFound; + + public static MemberResult CallVoidMethod(this object source, string methodName, TArg1 arg1, TArg2 arg2) => + source.TryCallVoidMethod(methodName, arg1, arg2) + ? new MemberResult(null) + : MemberResult.NotFound; + + /// + /// Tries to get the value of an instance property with the specified name. + /// + /// The type of the property. + /// The value that contains the property. + /// The name of the property. + /// The value of the property, or null if the property is not found. + /// true if the property exists, otherwise false. + public static bool TryGetPropertyValue(this object source, string propertyName, out TResult value) + { + if (source != null) + { + var type = source.GetType(); + + var fetcher = PropertyFetcherCache.GetOrAdd( + GetKey(propertyName, type), + key => new PropertyFetcher(key.Name)); + + if (fetcher != null) + { + value = fetcher.Fetch(source, type); + return true; + } + } + + value = default; + return false; + } + + public static MemberResult GetProperty(this object source, string propertyName) + { + if (source == null) + { + return MemberResult.NotFound; + } + + return source.TryGetPropertyValue(propertyName, out TResult result) + ? new MemberResult(result) + : MemberResult.NotFound; + } + + public static MemberResult GetProperty(this object source, string propertyName) => + GetProperty(source, propertyName); + + /// + /// Tries to get the value of an instance field with the specified name. + /// + /// The type of the field. + /// The value that contains the field. + /// The name of the field. + /// The value of the field, or null if the field is not found. + /// true if the field exists, otherwise false. + public static bool TryGetFieldValue(this object source, string fieldName, out TResult value) + { + var type = source.GetType(); + + var cachedItem = Cache.GetOrAdd( + GetKey(fieldName, type), + key => CreateFieldDelegate(key.Type1, key.Name)); + + if (cachedItem is Func func) + { + value = func(source); + return true; + } + + value = default; + return false; + } + + public static MemberResult GetField(this object source, string fieldName) => + source.TryGetFieldValue(fieldName, out TResult result) + ? new MemberResult(result) + : MemberResult.NotFound; + + private static PropertyFetcherCacheKey GetKey(string name, Type type) => + new PropertyFetcherCacheKey(type, typeof(TResult), name); + + private static Func CreateFieldDelegate(Type containerType, string fieldName) + { + var fieldInfo = containerType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (fieldInfo == null) + { + return null; + } + + var dynamicMethod = new DynamicMethod($"{containerType.FullName}.{fieldName}", typeof(TResult), new Type[] { typeof(object) }, ObjectExtensions.Module, skipVisibility: true); + var il = dynamicMethod.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + + if (containerType.IsValueType) + { + il.Emit(OpCodes.Unbox_Any, containerType); + } + else + { + il.Emit(OpCodes.Castclass, containerType); + } + + il.Emit(OpCodes.Ldfld, fieldInfo); + + if (fieldInfo.FieldType.IsValueType && typeof(TResult) == typeof(object)) + { + il.Emit(OpCodes.Box, fieldInfo.FieldType); + } + else if (fieldInfo.FieldType != typeof(TResult)) + { + il.Emit(OpCodes.Castclass, typeof(TResult)); + } + + il.Emit(OpCodes.Ret); + return (Func)dynamicMethod.CreateDelegate(typeof(Func)); + } + + private readonly struct PropertyFetcherCacheKey : IEquatable + { + public readonly Type Type1; + public readonly Type Type2; + public readonly Type Type3; + public readonly string Name; + + public PropertyFetcherCacheKey(Type type1, Type type2, string name) + : this(type1, type2, null, name) + { + } + + public PropertyFetcherCacheKey(Type type1, Type type2, Type type3, string name) + { + Type1 = type1 ?? throw new ArgumentNullException(nameof(type1)); + Type2 = type2; + Type3 = type3; + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + public bool Equals(PropertyFetcherCacheKey other) => + Equals(Type1, other.Type1) && Equals(Type2, other.Type2) && Equals(Type3, other.Type3) && Name == other.Name; + + public override bool Equals(object obj) => + obj is PropertyFetcherCacheKey other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hashCode = Type1.GetHashCode(); + hashCode = (hashCode * 397) ^ (Type2 != null ? Type2.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Type3 != null ? Type3.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ Name.GetHashCode(); + return hashCode; + } + } + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/OpCodeValues.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/OpCodeValues.cs new file mode 100644 index 000000000..4c3bf96b7 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/OpCodeValues.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + internal enum OpCodeValue : short + { + /// + Call = 40, + + /// + Callvirt = 111 + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/PropertyFetcher.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/PropertyFetcher.cs new file mode 100644 index 000000000..12889d81c --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/PropertyFetcher.cs @@ -0,0 +1,108 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + // TODO: Use one defined in Elastic.Apm + internal class PropertyFetcher + { + private readonly string _propertyName; + private Type _expectedType; + private object _fetchForExpectedType; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the property that this instance will fetch. + public PropertyFetcher(string propertyName) => _propertyName = propertyName; + + /// + /// Gets the value of the property on the specified object. + /// + /// Type of the result. + /// The object that contains the property. + /// The value of the property on the specified object. + public T Fetch(object obj) => Fetch(obj, obj.GetType()); + + /// + /// Gets the value of the property on the specified object. + /// + /// Type of the result. + /// The object that contains the property. + /// Type of the object + /// The value of the property on the specified object. + public T Fetch(object obj, Type objType) + { + if (objType != _expectedType) + { + var propertyInfo = objType.GetProperty(_propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase); + _fetchForExpectedType = PropertyFetch.FetcherForProperty(propertyInfo); + _expectedType = objType; + } + + return ((PropertyFetch)_fetchForExpectedType).Fetch(obj); + } + + /// + /// PropertyFetch is a helper class. It takes a PropertyInfo and then knows how + /// to efficiently fetch that property from a .NET object (See Fetch method). + /// It hides some slightly complex generic code. + /// + /// Return type of the property. + private class PropertyFetch + { + private readonly Func _propertyFetch; + + private PropertyFetch() => _propertyFetch = _ => default; + + private PropertyFetch(PropertyInfo propertyInfo) + { + // Generate lambda: arg => (T)((TObject)arg).get_property(); + var param = Expression.Parameter(typeof(object), "arg"); // arg => + var cast = Expression.Convert(param, propertyInfo.DeclaringType); // (TObject)arg + var propertyFetch = Expression.Property(cast, propertyInfo); // get_property() + var castResult = Expression.Convert(propertyFetch, typeof(T)); // (T)result + + // Generate the actual lambda + var lambda = Expression.Lambda(typeof(Func), castResult, param); + + // Compile it for faster access + _propertyFetch = (Func)lambda.Compile(); + } + + /// + /// Create a property fetcher from a .NET Reflection class that + /// represents a property of a particular type. + /// + /// The property that this instance will fetch. + /// The new property fetcher. + public static PropertyFetch FetcherForProperty(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + // returns null on any fetch. + return new PropertyFetch(); + } + + return new PropertyFetch(propertyInfo); + } + + /// + /// Gets the value of the property on the specified object. + /// + /// The object that contains the property. + /// The value of the property on the specified object. + public T Fetch(object obj) => _propertyFetch(obj); + } + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/Reflection/TypeExtensions.cs b/src/Elastic.Apm.Profiler.Managed/Reflection/TypeExtensions.cs new file mode 100644 index 000000000..2cd3b92f4 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/Reflection/TypeExtensions.cs @@ -0,0 +1,41 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Reflection +{ + public static class TypeExtensions + { + public static System.Type GetInstrumentedType( + this object runtimeObject, + string instrumentedNamespace, + string instrumentedTypeName) + { + if (runtimeObject == null) + { + return null; + } + + var currentType = runtimeObject.GetType(); + + while (currentType != null) + { + if (currentType.Name == instrumentedTypeName && currentType.Namespace == instrumentedNamespace) + { + return currentType; + } + + currentType = currentType.BaseType; + } + + return null; + } + + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/TracerExtensions.cs b/src/Elastic.Apm.Profiler.Managed/TracerExtensions.cs new file mode 100644 index 000000000..08cc126fb --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/TracerExtensions.cs @@ -0,0 +1,15 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Apm.Api; + +namespace Elastic.Apm.Profiler.Managed +{ + // TODO: this should be public in Elastic.Apm somewhere + internal static class TracerExtensions + { + public static IExecutionSegment CurrentExecutionSegment(this ITracer tracer) => (IExecutionSegment)tracer.CurrentSpan ?? tracer.CurrentTransaction; + } +} diff --git a/src/Elastic.Apm.Profiler.Managed/integrations.yml b/src/Elastic.Apm.Profiler.Managed/integrations.yml new file mode 100644 index 000000000..65494e575 --- /dev/null +++ b/src/Elastic.Apm.Profiler.Managed/integrations.yml @@ -0,0 +1,1573 @@ +- name: AdoNet + method_replacements: + - target: + assembly: System.Data + type: System.Data.Common.DbCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.Common + type: System.Data.Common.DbCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.Common.DbCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.Common + type: System.Data.Common.DbCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.Common.DbCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.Common + type: System.Data.Common.DbCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification +- name: AspNet + method_replacements: + - target: + assembly: System.Web + type: System.Web.Compilation.BuildManager + method: InvokePreStartInitMethodsCore + signature_types: + - System.Void + - System.Collections.Generic.ICollection`1[System.Reflection.MethodInfo] + - System.Func`1[System.IDisposable] + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AspNet.ElasticApmModuleIntegration + action: CallTargetModification +- name: Kafka + method_replacements: + - target: + assembly: Confluent.Kafka + type: Confluent.Kafka.Consumer`2 + method: Close + signature_types: + - System.Void + minimum_version: 1.4.0 + maximum_version: 1.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.Kafka.KafkaConsumerCloseIntegration + action: CallTargetModification + - target: + assembly: Confluent.Kafka + type: Confluent.Kafka.Consumer`2 + method: Consume + signature_types: + - Confluent.Kafka.ConsumeResult`2[!0,!1] + - System.Int32 + minimum_version: 1.4.0 + maximum_version: 1.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.Kafka.KafkaConsumerConsumeIntegration + action: CallTargetModification + - target: + assembly: Confluent.Kafka + type: Confluent.Kafka.Consumer`2 + method: Dispose + signature_types: + - System.Void + minimum_version: 1.4.0 + maximum_version: 1.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.Kafka.KafkaConsumerDisposeIntegration + action: CallTargetModification + - target: + assembly: Confluent.Kafka + type: Confluent.Kafka.Consumer`2 + method: Unsubscribe + signature_types: + - System.Void + minimum_version: 1.4.0 + maximum_version: 1.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.Kafka.KafkaConsumerUnsubscribeIntegration + action: CallTargetModification + - target: + assembly: Confluent.Kafka + type: Confluent.Kafka.Producer`2 + method: ProduceAsync + signature_types: + - System.Threading.Tasks.Task`1[Confluent.Kafka.DeliveryReport`2[!0,!1]] + - Confluent.Kafka.TopicPartition + - Confluent.Kafka.Message`2[!0,!1] + - System.Threading.CancellationToken + minimum_version: 1.4.0 + maximum_version: 1.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.Kafka.KafkaProduceAsyncIntegration + action: CallTargetModification + - target: + assembly: Confluent.Kafka + type: Confluent.Kafka.Producer`2+TypedDeliveryHandlerShim_Action + method: .ctor + signature_types: + - System.Void + - System.String + - '!0' + - '!1' + - System.Action`1[Confluent.Kafka.DeliveryReport`2[!0,!1]] + minimum_version: 1.4.0 + maximum_version: 1.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.Kafka.KafkaProduceSyncDeliveryHandlerIntegration + action: CallTargetModification + - target: + assembly: Confluent.Kafka + type: Confluent.Kafka.Producer`2 + method: Produce + signature_types: + - System.Void + - Confluent.Kafka.TopicPartition + - Confluent.Kafka.Message`2[!0,!1] + - System.Action`1[Confluent.Kafka.DeliveryReport`2[!0,!1]] + minimum_version: 1.4.0 + maximum_version: 1.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.Kafka.KafkaProduceSyncIntegration + action: CallTargetModification +- name: MySqlCommand + method_replacements: + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteReader + signature_types: + - MySql.Data.MySqlClient.MySqlDataReader + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteReader + signature_types: + - MySql.Data.MySqlClient.MySqlDataReader + - System.Data.CommandBehavior + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: MySql.Data + type: MySql.Data.MySqlClient.MySqlCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 6.7.0 + maximum_version: 8.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification +- name: NpgsqlCommand + method_replacements: + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteReader + signature_types: + - Npgsql.NpgsqlDataReader + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteReader + signature_types: + - Npgsql.NpgsqlDataReader + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: Npgsql + type: Npgsql.NpgsqlCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification +- name: OracleCommand + method_replacements: + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReader + signature_types: + - Oracle.ManagedDataAccess.Client.OracleDataReader + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReader + signature_types: + - Oracle.ManagedDataAccess.Client.OracleDataReader + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReader + signature_types: + - Oracle.ManagedDataAccess.Client.OracleDataReader + - System.Data.CommandBehavior + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteReader + signature_types: + - Oracle.ManagedDataAccess.Client.OracleDataReader + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 4.122.0 + maximum_version: 4.122.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Oracle.ManagedDataAccess + type: Oracle.ManagedDataAccess.Client.OracleCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification +- name: SqlCommand + method_replacements: + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteReader + signature_types: + - System.Data.SqlClient.SqlDataReader + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteReader + signature_types: + - System.Data.SqlClient.SqlDataReader + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteReader + signature_types: + - Microsoft.Data.SqlClient.SqlDataReader + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteReader + signature_types: + - System.Data.SqlClient.SqlDataReader + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteReader + signature_types: + - System.Data.SqlClient.SqlDataReader + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteReader + signature_types: + - Microsoft.Data.SqlClient.SqlDataReader + - System.Data.CommandBehavior + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: System.Data + type: System.Data.SqlClient.SqlCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data.SqlClient + type: System.Data.SqlClient.SqlCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.SqlClient + type: Microsoft.Data.SqlClient.SqlCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification +- name: SqliteCommand + method_replacements: + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteNonQuery + signature_types: + - System.Int32 + - System.Data.CommandBehavior + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteNonQueryWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteReader + signature_types: + - Microsoft.Data.Sqlite.SqliteDataReader + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteReader + signature_types: + - System.Data.SQLite.SQLiteDataReader + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteDbDataReaderAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Data.CommandBehavior + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteReader + signature_types: + - Microsoft.Data.Sqlite.SqliteDataReader + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteReader + signature_types: + - System.Data.SQLite.SQLiteDataReader + - System.Data.CommandBehavior + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteDbDataReader + signature_types: + - System.Data.Common.DbDataReader + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteReaderWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteScalarAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarAsyncIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteScalar + signature_types: + - System.Object + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarIntegration + action: CallTargetModification + - target: + assembly: Microsoft.Data.Sqlite + type: Microsoft.Data.Sqlite.SqliteCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 2.0.0 + maximum_version: 5.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification + - target: + assembly: System.Data.SQLite + type: System.Data.SQLite.SQLiteCommand + method: ExecuteScalar + signature_types: + - System.Object + - System.Data.CommandBehavior + minimum_version: 1.0.0 + maximum_version: 2.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.11.1.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Managed.Integrations.AdoNet.CommandExecuteScalarWithBehaviorIntegration + action: CallTargetModification diff --git a/src/elastic_apm_profiler/Cargo.toml b/src/elastic_apm_profiler/Cargo.toml new file mode 100644 index 000000000..071f652d3 --- /dev/null +++ b/src/elastic_apm_profiler/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "elastic_apm_profiler" +version = "1.11.1-alpha.2" +edition = "2018" +authors = ["Elastic and Contributors"] +description = "Elastic APM .NET agent CLR profiler" +repository = "https://github.com/elastic/apm-agent-dotnet" +keywords = ["elasticsearch", "elastic", "apm", "dotnet"] +license = "Apache-2.0" + +[dependencies] +bitflags = "1.2.1" +c_vec = "2.0.0" +com = { version = "0.6.0", features = ["production"] } +hex = "0.4.3" +log = "0.4.14" +log4rs = { version = "1.0.0", default_features = false, features = ["console_appender", "rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] } +num-derive = "0.3" +num-traits = "0.2" +once_cell = "1.8.0" +rust-crypto = "^0.2" +rust-embed = { version = "5.9.0", features = ["compression", "debug-embed"] } +serde = { version = "1.0.126", features = ["derive"] } +serde_yaml = "0.8.17" +widestring = "0.4.2" + +[lib] +crate-type = ["cdylib"] + + diff --git a/src/elastic_apm_profiler/NOTICE b/src/elastic_apm_profiler/NOTICE new file mode 100644 index 000000000..c89ae6bc6 --- /dev/null +++ b/src/elastic_apm_profiler/NOTICE @@ -0,0 +1,168 @@ +elastic_apm_profiler +CLR profiler for the .NET runtime +Copyright 2021 Elasticsearch B.V. + +--- +This product includes a Rust port of functions and COM interfaces defined +in the .NET runtime (https://github.com/dotnet/runtime). + +Copyright (c) .NET Foundation and Contributors. + +All rights reserved. +Licensed under the MIT License. + +Available at +https://github.com/dotnet/runtime/blob/main/LICENSE.TXT + +--- +This product includes software derived from software developed by Datadog +(https://github.com/DataDog/dd-trace-dotnet). + +Copyright 2017-2021 Datadog, Inc. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/DataDog/dd-trace-dotnet/blob/eff230c477d8729f166b2e4f8fa5849d8ff7f78e/LICENSE + +--- +This product includes software derived from clr-profiler. + +Copyright 2019 Camden Reslink +Licensed under the MIT license + +Available at +https://github.com/camdenreslink/clr-profiler/blob/master/LICENSE + +--- +This product includes software derived from winapi-rs. + +Copyright (c) 2015-2018 The winapi-rs Developers +Licensed under the Apache 2.0 license + +Available at +https://github.com/retep998/winapi-rs/blob/0.3/LICENSE-APACHE + +--- +License notice for bitflags crate + +Copyright (c) 2014 The Rust Project Developers +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/bitflags/bitflags/blob/main/LICENSE-APACHE + +--- +License notice for c_vec crate + +Copyright (c) 2015 Guillaume Gomez +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/GuillaumeGomez/c_vec-rs/blob/master/LICENSE-APACHE + +--- +License notice for com crate + +Copyright (c) Microsoft Corporation. +Licensed under the MIT License. + +Available at +https://github.com/microsoft/com-rs/blob/master/LICENSE + +--- +License notice for hex crate + +Copyright (c) 2013-2014 The Rust Project Developers. +Copyright (c) 2015-2020 The rust-hex Developers. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/KokaKiwi/rust-hex/blob/main/LICENSE-APACHE + +--- +License notice for log crate + +Copyright (c) 2014 The Rust Project Developers. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/rust-lang/log/blob/master/LICENSE-APACHE + +--- +License notice for log4rs crate + +Copyright (c) 2015-2016 Steven Fackler +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/estk/log4rs/blob/master/LICENSE-APACHE + +--- +License notice for num-derive crate + +Copyright (c) 2014 The Rust Project Developers +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/rust-num/num-derive/blob/master/LICENSE-APACHE + +--- +License notice for num-traits crate + +Copyright (c) 2014 The Rust Project Developers +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/rust-num/num-traits/blob/master/LICENSE-APACHE + +--- +License notice for once_cell crate + +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/matklad/once_cell/blob/master/LICENSE-APACHE + +--- +License notice for rust-crypto crate + +Copyright (c) 2006-2009 Graydon Hoare +Copyright (c) 2009-2013 Mozilla Foundation +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/DaGenix/rust-crypto/blob/master/LICENSE-APACHE + +--- +License notice for rust-embed crate + +Copyright (c) 2018 pyros2097 +Licensed under The MIT License (MIT). + +Available at +https://github.com/pyros2097/rust-embed/blob/master/license + +--- +License notice for serde crate + +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/serde-rs/serde/blob/master/LICENSE-APACHE + +--- +License notice for serde_yaml crate + +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/dtolnay/serde-yaml/blob/master/LICENSE-APACHE + +--- +License notice for widestring crate + +Copyright (c) 2016 Kathryn Long +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/starkat99/widestring-rs/blob/master/LICENSE-APACHE diff --git a/src/elastic_apm_profiler/build.rs b/src/elastic_apm_profiler/build.rs new file mode 100644 index 000000000..545d1dba1 --- /dev/null +++ b/src/elastic_apm_profiler/build.rs @@ -0,0 +1,7 @@ +use std::process::Command; + +fn main() { + let output = Command::new("git").args(&["rev-parse", "HEAD"]).output().unwrap(); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); +} \ No newline at end of file diff --git a/src/elastic_apm_profiler/src/cil/cor.rs b/src/elastic_apm_profiler/src/cil/cor.rs new file mode 100644 index 000000000..9f6ed7c8e --- /dev/null +++ b/src/elastic_apm_profiler/src/cil/cor.rs @@ -0,0 +1,116 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Contains Rust ports of .NET runtime functions defined in the .NET runtime in Cor.h. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +use crate::ffi::{ + mdToken, mdTokenNil, rid_from_token, token_from_rid, type_from_token, CorTokenType, BYTE, ULONG, +}; + +const ENCODE_TOKEN: [mdToken; 4] = [ + CorTokenType::mdtTypeDef.bits(), + CorTokenType::mdtTypeRef.bits(), + CorTokenType::mdtTypeSpec.bits(), + CorTokenType::mdtBaseType.bits(), +]; + +/// Compress a token. +/// The least significant bit of the first compress byte will indicate the token type. +pub fn compress_token(token: mdToken) -> Option> { + let mut rid = rid_from_token(token); + let typ = type_from_token(token); + + if rid > 0x3FFFFFF { + return None; + } + + rid <<= 2; + + // TypeDef is encoded with low bits 00 + // TypeRef is encoded with low bits 01 + // TypeSpec is encoded with low bits 10 + // BaseType is encoded with low bit 11 + if typ == ENCODE_TOKEN[1] { + // make the last two bits 01 + rid |= 0x1; + } else if typ == ENCODE_TOKEN[2] { + // make last two bits 0 + rid |= 0x2; + } else if typ == ENCODE_TOKEN[3] { + rid |= 0x3; + } + + compress_data(rid) +} + +/// Given an uncompressed unsigned integer (int), Store it in a compressed format. +/// Based on CorSigCompressData: https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/coreclr/inc/cor.h#L2111 +pub fn compress_data(int: ULONG) -> Option> { + if int <= 0x7F { + let buffer = vec![int as BYTE]; + return Some(buffer); + } + + if int <= 0x3FFF { + let buffer = vec![((int >> 8) | 0x80) as BYTE, (int & 0xff) as BYTE]; + return Some(buffer); + } + + if int <= 0x1FFFFFFF { + let buffer = vec![ + ((int >> 24) | 0xC0) as BYTE, + ((int >> 16) & 0xff) as BYTE, + ((int >> 8) & 0xff) as BYTE, + (int & 0xff) as BYTE, + ]; + return Some(buffer); + } + + None +} + +/// https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/coreclr/inc/cor.h#L1817 +pub fn uncompress_data(data: &[u8]) -> Option<(ULONG, usize)> { + if data.is_empty() { + None + } else if data[0] & 0x80 == 0x00 { + // 0??? ???? + Some((data[0] as ULONG, 1_usize)) + } else if data[0] & 0xC0 == 0x80 { + // 10?? ???? + if data.len() < 2 { + None + } else { + let mut out = ((data[0] as ULONG) & 0x3f) << 8; + out |= data[1] as ULONG; + Some((out, 2_usize)) + } + } else if data[0] & 0xE0 == 0xC0 { + // 110? ???? + if data.len() < 4 { + None + } else { + let mut out = ((data[0] as ULONG) & 0x1f) << 24; + out |= (data[1] as ULONG) << 16; + out |= (data[2] as ULONG) << 8; + out |= data[3] as ULONG; + Some((out, 4_usize)) + } + } else { + None + } +} + +pub fn uncompress_token(data: &[u8]) -> (mdToken, usize) { + if let Some((uncompressed_data, len)) = uncompress_data(data) { + let token_type = ENCODE_TOKEN[(uncompressed_data & 0x3) as usize]; + let token = token_from_rid(uncompressed_data >> 2, token_type); + (token, len) + } else { + (mdTokenNil, 0) + } +} diff --git a/src/elastic_apm_profiler/src/cil/helpers.rs b/src/elastic_apm_profiler/src/cil/helpers.rs new file mode 100644 index 000000000..c67fa1b6a --- /dev/null +++ b/src/elastic_apm_profiler/src/cil/helpers.rs @@ -0,0 +1,96 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Copyright 2019 Camden Reslink +// MIT License +// https://github.com/camdenreslink/clr-profiler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +use crate::error::Error; + +pub fn il_u8(il: &[u8], index: usize) -> Result { + il.get(index).ok_or(Error::InvalidCil).map(|v| *v) +} +pub fn il_u16(il: &[u8], index: usize) -> Result { + let byte_1 = il_u8(il, index)?; + let byte_2 = il_u8(il, index + 1)?; + Ok(u16::from_le_bytes([byte_1, byte_2])) +} +pub fn il_u32(il: &[u8], index: usize) -> Result { + let byte_1 = il_u8(il, index)?; + let byte_2 = il_u8(il, index + 1)?; + let byte_3 = il_u8(il, index + 2)?; + let byte_4 = il_u8(il, index + 3)?; + Ok(u32::from_le_bytes([byte_1, byte_2, byte_3, byte_4])) +} +pub fn il_i8(il: &[u8], index: usize) -> Result { + let byte = il_u8(il, index)?; + Ok(i8::from_le_bytes([byte])) +} +pub fn il_i16(il: &[u8], index: usize) -> Result { + let byte_1 = il_u8(il, index)?; + let byte_2 = il_u8(il, index + 1)?; + Ok(i16::from_le_bytes([byte_1, byte_2])) +} +pub fn il_i32(il: &[u8], index: usize) -> Result { + let byte_1 = il_u8(il, index)?; + let byte_2 = il_u8(il, index + 1)?; + let byte_3 = il_u8(il, index + 2)?; + let byte_4 = il_u8(il, index + 3)?; + Ok(i32::from_le_bytes([byte_1, byte_2, byte_3, byte_4])) +} +pub fn il_i64(il: &[u8], index: usize) -> Result { + let byte_1 = il_u8(il, index)?; + let byte_2 = il_u8(il, index + 1)?; + let byte_3 = il_u8(il, index + 2)?; + let byte_4 = il_u8(il, index + 3)?; + let byte_5 = il_u8(il, index + 4)?; + let byte_6 = il_u8(il, index + 5)?; + let byte_7 = il_u8(il, index + 6)?; + let byte_8 = il_u8(il, index + 7)?; + Ok(i64::from_le_bytes([ + byte_1, byte_2, byte_3, byte_4, byte_5, byte_6, byte_7, byte_8, + ])) +} +pub fn il_f32(il: &[u8], index: usize) -> Result { + let byte_1 = il_u8(il, index)?; + let byte_2 = il_u8(il, index + 1)?; + let byte_3 = il_u8(il, index + 2)?; + let byte_4 = il_u8(il, index + 3)?; + Ok(f32::from_le_bytes([byte_1, byte_2, byte_3, byte_4])) +} +pub fn il_f64(il: &[u8], index: usize) -> Result { + let byte_1 = il_u8(il, index)?; + let byte_2 = il_u8(il, index + 1)?; + let byte_3 = il_u8(il, index + 2)?; + let byte_4 = il_u8(il, index + 3)?; + let byte_5 = il_u8(il, index + 4)?; + let byte_6 = il_u8(il, index + 5)?; + let byte_7 = il_u8(il, index + 6)?; + let byte_8 = il_u8(il, index + 7)?; + Ok(f64::from_le_bytes([ + byte_1, byte_2, byte_3, byte_4, byte_5, byte_6, byte_7, byte_8, + ])) +} +pub fn check_flag(flags: u8, flag: u8) -> bool { + (flags & flag) == flag +} +pub fn nearest_multiple(multiple: usize, value: usize) -> usize { + (value + (multiple - 1)) & !(multiple - 1) +} diff --git a/src/elastic_apm_profiler/src/cil/instruction.rs b/src/elastic_apm_profiler/src/cil/instruction.rs new file mode 100644 index 000000000..92230a9ea --- /dev/null +++ b/src/elastic_apm_profiler/src/cil/instruction.rs @@ -0,0 +1,1626 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Copyright 2019 Camden Reslink +// MIT License +// https://github.com/camdenreslink/clr-profiler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +use crate::{ + cil::{il_f32, il_f64, il_i32, il_i64, il_i8, il_u16, il_u32, il_u8, opcode::*, OperandParams}, + error::{Error, Error::InvalidCil}, +}; +use std::fmt::{Display, Formatter}; + +/// A signed or unsigned 8-bit integer type +#[derive(Debug, Copy, Clone)] +pub enum SingleByte { + Signed(i8), + Unsigned(u8), +} + +impl SingleByte { + /// Return the memory representation of this integer as a byte array in little-endian byte order. + pub fn to_le_bytes(self) -> [u8; 1] { + match self { + SingleByte::Signed(val) => val.to_le_bytes(), + SingleByte::Unsigned(val) => val.to_le_bytes(), + } + } +} + +impl Display for SingleByte { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SingleByte::Signed(i) => i.fmt(f), + SingleByte::Unsigned(u) => u.fmt(f), + } + } +} + +#[derive(Debug, Clone)] +pub enum Operand { + InlineNone, + ShortInlineVar(u8), + InlineVar(u16), + ShortInlineI(SingleByte), + InlineI(i32), + InlineI8(i64), + ShortInlineR(f32), + InlineR(f64), + InlineMethod(u32), + InlineSig(u32), + ShortInlineBrTarget(i8), + InlineBrTarget(i32), + InlineSwitch(u32, Vec), + InlineType(u32), + InlineString(u32), + InlineField(u32), + InlineTok(u32), +} + +#[allow(clippy::len_without_is_empty)] +impl Operand { + pub fn len(&self) -> usize { + match self { + Self::InlineNone => 0, + Self::ShortInlineVar(_) => 1, + Self::InlineVar(_) => 2, + Self::ShortInlineI(_) => 1, + Self::InlineI(_) => 4, + Self::InlineI8(_) => 8, + Self::ShortInlineR(_) => 4, + Self::InlineR(_) => 8, + Self::InlineMethod(_) => 4, + Self::InlineSig(_) => 4, + Self::ShortInlineBrTarget(_) => 1, + Self::InlineBrTarget(_) => 4, + Self::InlineSwitch(length, _) => ((*length + 1) * 4) as usize, + Self::InlineType(_) => 4, + Self::InlineString(_) => 4, + Self::InlineField(_) => 4, + Self::InlineTok(_) => 4, + } + } +} +#[derive(Debug, Clone)] +pub struct Instruction { + pub opcode: Opcode, + pub operand: Operand, +} + +#[allow(clippy::len_without_is_empty)] +impl Instruction { + /// Attempts to parse the first instruction at the beginning + /// of the given byte array. Array must be at a valid instruction + /// boundary. + pub fn from_bytes(il: &[u8]) -> Result { + let byte_1 = il_u8(il, 0)?; + let opcode = if byte_1 == 0xFE { + // In this case, we have a multibyte opcode + let byte_2 = il_u8(il, 1)?; + Opcode::from_byte_pair((byte_1, byte_2)) + } else { + Ok(Opcode::from_byte(byte_1)) + }?; + let operand_index = opcode.len as usize; + let operand = match &opcode.operand_params { + OperandParams::InlineNone => Operand::InlineNone, + OperandParams::ShortInlineVar => { + let val = il_u8(il, operand_index)?; + Operand::ShortInlineVar(val) + } + OperandParams::InlineVar => { + let val = il_u16(il, operand_index)?; + Operand::InlineVar(val) + } + OperandParams::ShortInlineI => { + match opcode { + LDC_I4_S => { + let val = il_i8(il, operand_index)?; + Operand::ShortInlineI(SingleByte::Signed(val)) + } + UNALIGNED => { + let val = il_u8(il, operand_index)?; + Operand::ShortInlineI(SingleByte::Unsigned(val)) + } + _ => { + // Handle other future instructions, in the future... + return Err(InvalidCil); + } + } + } + OperandParams::InlineI => { + let val = il_i32(il, operand_index)?; + Operand::InlineI(val) + } + OperandParams::InlineI8 => { + let val = il_i64(il, operand_index)?; + Operand::InlineI8(val) + } + OperandParams::ShortInlineR => { + let val = il_f32(il, operand_index)?; + Operand::ShortInlineR(val) + } + OperandParams::InlineR => { + let val = il_f64(il, operand_index)?; + Operand::InlineR(val) + } + OperandParams::InlineMethod => { + let val = il_u32(il, operand_index)?; + Operand::InlineMethod(val) + } + OperandParams::InlineSig => { + let val = il_u32(il, operand_index)?; + Operand::InlineSig(val) + } + OperandParams::ShortInlineBrTarget => { + let val = il_i8(il, operand_index)?; + Operand::ShortInlineBrTarget(val) + } + OperandParams::InlineBrTarget => { + let val = il_i32(il, operand_index)?; + Operand::InlineBrTarget(val) + } + OperandParams::InlineSwitch => { + let length = il_u32(il, operand_index)?; + let mut val: Vec = Vec::with_capacity(length as usize); + for i in 1..=length { + let target_index = operand_index + ((i * 4) as usize); + let target = il_i32(il, target_index)?; + val.push(target); + } + Operand::InlineSwitch(length, val) + } + OperandParams::InlineType => { + let val = il_u32(il, operand_index)?; + Operand::InlineType(val) + } + OperandParams::InlineString => { + let val = il_u32(il, operand_index)?; + Operand::InlineString(val) + } + OperandParams::InlineField => { + let val = il_u32(il, operand_index)?; + Operand::InlineField(val) + } + OperandParams::InlineTok => { + let val = il_u32(il, operand_index)?; + Operand::InlineTok(val) + } + }; + Ok(Instruction { opcode, operand }) + } + pub fn into_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + if self.opcode.len == 1 { + bytes.push(self.opcode.byte_2); + } else if self.opcode.len == 2 { + bytes.push(self.opcode.byte_1); + bytes.push(self.opcode.byte_2); + } + match &self.operand { + Operand::InlineNone => (), + Operand::ShortInlineVar(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineVar(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::ShortInlineI(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineI(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineI8(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::ShortInlineR(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineR(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineMethod(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineSig(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::ShortInlineBrTarget(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineBrTarget(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineSwitch(length, val) => { + bytes.extend_from_slice(&length.to_le_bytes()); + let mut target_bytes: Vec = + val.iter().flat_map(|s| s.to_le_bytes().to_vec()).collect(); + bytes.append(&mut target_bytes); + } + Operand::InlineType(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineString(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineField(val) => bytes.extend_from_slice(&val.to_le_bytes()), + Operand::InlineTok(val) => bytes.extend_from_slice(&val.to_le_bytes()), + } + + bytes + } + + /// stack size of the instruction + pub fn stack_size(&self) -> usize { + self.opcode.stack_behavior_push.size() + } + + /// length of the instruction + pub fn len(&self) -> usize { + self.opcode.len as usize + self.operand.len() + } + + pub fn nop() -> Self { + Self { + opcode: NOP, + operand: Operand::InlineNone, + } + } + pub fn break_() -> Self { + Self { + opcode: BREAK, + operand: Operand::InlineNone, + } + } + pub fn ldarg_0() -> Self { + Self { + opcode: LDARG_0, + operand: Operand::InlineNone, + } + } + pub fn ldarg_1() -> Self { + Self { + opcode: LDARG_1, + operand: Operand::InlineNone, + } + } + pub fn ldarg_2() -> Self { + Self { + opcode: LDARG_2, + operand: Operand::InlineNone, + } + } + pub fn ldarg_3() -> Self { + Self { + opcode: LDARG_3, + operand: Operand::InlineNone, + } + } + pub fn ldloc_0() -> Self { + Self { + opcode: LDLOC_0, + operand: Operand::InlineNone, + } + } + pub fn ldloc_1() -> Self { + Self { + opcode: LDLOC_1, + operand: Operand::InlineNone, + } + } + pub fn ldloc_2() -> Self { + Self { + opcode: LDLOC_2, + operand: Operand::InlineNone, + } + } + pub fn ldloc_3() -> Self { + Self { + opcode: LDLOC_3, + operand: Operand::InlineNone, + } + } + pub fn stloc_0() -> Self { + Self { + opcode: STLOC_0, + operand: Operand::InlineNone, + } + } + pub fn stloc_1() -> Self { + Self { + opcode: STLOC_1, + operand: Operand::InlineNone, + } + } + pub fn stloc_2() -> Self { + Self { + opcode: STLOC_2, + operand: Operand::InlineNone, + } + } + pub fn stloc_3() -> Self { + Self { + opcode: STLOC_3, + operand: Operand::InlineNone, + } + } + pub fn ldarg_s(val: u8) -> Self { + Self { + opcode: LDARG_S, + operand: Operand::ShortInlineVar(val), + } + } + pub fn ldarga_s(val: u8) -> Self { + Self { + opcode: LDARGA_S, + operand: Operand::ShortInlineVar(val), + } + } + pub fn starg_s(val: u8) -> Self { + Self { + opcode: STARG_S, + operand: Operand::ShortInlineVar(val), + } + } + /// Loads the local variable at a specific index onto the evaluation stack, short form. + pub fn ldloc_s(val: u8) -> Self { + Self { + opcode: LDLOC_S, + operand: Operand::ShortInlineVar(val), + } + } + /// Loads the address of the local variable at a specific index onto the evaluation stack, short form. + pub fn ldloca_s(val: u8) -> Self { + Self { + opcode: LDLOCA_S, + operand: Operand::ShortInlineVar(val), + } + } + pub fn stloc_s(val: u8) -> Self { + Self { + opcode: STLOC_S, + operand: Operand::ShortInlineVar(val), + } + } + /// Pushes a null reference (type O) onto the evaluation stack. + pub fn ldnull() -> Self { + Self { + opcode: LDNULL, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_m1() -> Self { + Self { + opcode: LDC_I4_M1, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_0() -> Self { + Self { + opcode: LDC_I4_0, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_1() -> Self { + Self { + opcode: LDC_I4_1, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_2() -> Self { + Self { + opcode: LDC_I4_2, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_3() -> Self { + Self { + opcode: LDC_I4_3, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_4() -> Self { + Self { + opcode: LDC_I4_4, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_5() -> Self { + Self { + opcode: LDC_I4_5, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_6() -> Self { + Self { + opcode: LDC_I4_6, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_7() -> Self { + Self { + opcode: LDC_I4_7, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_8() -> Self { + Self { + opcode: LDC_I4_8, + operand: Operand::InlineNone, + } + } + pub fn ldc_i4_s(val: i8) -> Self { + Self { + opcode: LDC_I4_S, + operand: Operand::ShortInlineI(SingleByte::Signed(val)), + } + } + pub fn ldc_i4(val: i32) -> Self { + Self { + opcode: LDC_I4, + operand: Operand::InlineI(val), + } + } + pub fn ldc_i8(val: i64) -> Self { + Self { + opcode: LDC_I8, + operand: Operand::InlineI8(val), + } + } + pub fn ldc_r4(val: f32) -> Self { + Self { + opcode: LDC_R4, + operand: Operand::ShortInlineR(val), + } + } + pub fn ldc_r8(val: f64) -> Self { + Self { + opcode: LDC_R8, + operand: Operand::InlineR(val), + } + } + pub fn dup() -> Self { + Self { + opcode: DUP, + operand: Operand::InlineNone, + } + } + pub fn pop() -> Self { + Self { + opcode: POP, + operand: Operand::InlineNone, + } + } + pub fn jmp(val: u32) -> Self { + Self { + opcode: JMP, + operand: Operand::InlineMethod(val), + } + } + pub fn call(val: u32) -> Self { + Self { + opcode: CALL, + operand: Operand::InlineMethod(val), + } + } + pub fn calli(val: u32) -> Self { + Self { + opcode: CALLI, + operand: Operand::InlineSig(val), + } + } + pub fn ret() -> Self { + Self { + opcode: RET, + operand: Operand::InlineNone, + } + } + pub fn br_s(val: i8) -> Self { + Self { + opcode: BR_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn brfalse_s(val: i8) -> Self { + Self { + opcode: BRFALSE_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn brtrue_s(val: i8) -> Self { + Self { + opcode: BRTRUE_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn beq_s(val: i8) -> Self { + Self { + opcode: BEQ_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn bge_s(val: i8) -> Self { + Self { + opcode: BGE_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn bgt_s(val: i8) -> Self { + Self { + opcode: BGT_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn ble_s(val: i8) -> Self { + Self { + opcode: BLE_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn blt_s(val: i8) -> Self { + Self { + opcode: BLT_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn bne_un_s(val: i8) -> Self { + Self { + opcode: BNE_UN_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn bge_un_s(val: i8) -> Self { + Self { + opcode: BGE_UN_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn bgt_un_s(val: i8) -> Self { + Self { + opcode: BGT_UN_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn ble_un_s(val: i8) -> Self { + Self { + opcode: BLE_UN_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn blt_un_s(val: i8) -> Self { + Self { + opcode: BLT_UN_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn br(val: i32) -> Self { + Self { + opcode: BR, + operand: Operand::InlineBrTarget(val), + } + } + pub fn brfalse(val: i32) -> Self { + Self { + opcode: BRFALSE, + operand: Operand::InlineBrTarget(val), + } + } + pub fn brtrue(val: i32) -> Self { + Self { + opcode: BRTRUE, + operand: Operand::InlineBrTarget(val), + } + } + pub fn beq(val: i32) -> Self { + Self { + opcode: BEQ, + operand: Operand::InlineBrTarget(val), + } + } + pub fn bge(val: i32) -> Self { + Self { + opcode: BGE, + operand: Operand::InlineBrTarget(val), + } + } + pub fn bgt(val: i32) -> Self { + Self { + opcode: BGT, + operand: Operand::InlineBrTarget(val), + } + } + pub fn ble(val: i32) -> Self { + Self { + opcode: BLE, + operand: Operand::InlineBrTarget(val), + } + } + pub fn blt(val: i32) -> Self { + Self { + opcode: BLT, + operand: Operand::InlineBrTarget(val), + } + } + pub fn bne_un(val: i32) -> Self { + Self { + opcode: BNE_UN, + operand: Operand::InlineBrTarget(val), + } + } + pub fn bge_un(val: i32) -> Self { + Self { + opcode: BGE_UN, + operand: Operand::InlineBrTarget(val), + } + } + pub fn bgt_un(val: i32) -> Self { + Self { + opcode: BGT_UN, + operand: Operand::InlineBrTarget(val), + } + } + pub fn ble_un(val: i32) -> Self { + Self { + opcode: BLE_UN, + operand: Operand::InlineBrTarget(val), + } + } + pub fn blt_un(val: i32) -> Self { + Self { + opcode: BLT_UN, + operand: Operand::InlineBrTarget(val), + } + } + pub fn switch(length: u32, targets: Vec) -> Self { + Self { + opcode: SWITCH, + operand: Operand::InlineSwitch(length, targets), + } + } + pub fn ldind_i1() -> Self { + Self { + opcode: LDIND_I1, + operand: Operand::InlineNone, + } + } + pub fn ldind_u1() -> Self { + Self { + opcode: LDIND_U1, + operand: Operand::InlineNone, + } + } + pub fn ldind_i2() -> Self { + Self { + opcode: LDIND_I2, + operand: Operand::InlineNone, + } + } + pub fn ldind_u2() -> Self { + Self { + opcode: LDIND_U2, + operand: Operand::InlineNone, + } + } + pub fn ldind_i4() -> Self { + Self { + opcode: LDIND_I4, + operand: Operand::InlineNone, + } + } + pub fn ldind_u4() -> Self { + Self { + opcode: LDIND_U4, + operand: Operand::InlineNone, + } + } + pub fn ldind_i8() -> Self { + Self { + opcode: LDIND_I8, + operand: Operand::InlineNone, + } + } + pub fn ldind_i() -> Self { + Self { + opcode: LDIND_I, + operand: Operand::InlineNone, + } + } + pub fn ldind_r4() -> Self { + Self { + opcode: LDIND_R4, + operand: Operand::InlineNone, + } + } + pub fn ldind_r8() -> Self { + Self { + opcode: LDIND_R8, + operand: Operand::InlineNone, + } + } + pub fn ldind_ref() -> Self { + Self { + opcode: LDIND_REF, + operand: Operand::InlineNone, + } + } + pub fn stind_ref() -> Self { + Self { + opcode: STIND_REF, + operand: Operand::InlineNone, + } + } + pub fn stind_i1() -> Self { + Self { + opcode: STIND_I1, + operand: Operand::InlineNone, + } + } + pub fn stind_i2() -> Self { + Self { + opcode: STIND_I2, + operand: Operand::InlineNone, + } + } + pub fn stind_i4() -> Self { + Self { + opcode: STIND_I4, + operand: Operand::InlineNone, + } + } + pub fn stind_i8() -> Self { + Self { + opcode: STIND_I8, + operand: Operand::InlineNone, + } + } + pub fn stind_r4() -> Self { + Self { + opcode: STIND_R4, + operand: Operand::InlineNone, + } + } + pub fn stind_r8() -> Self { + Self { + opcode: STIND_R8, + operand: Operand::InlineNone, + } + } + pub fn add() -> Self { + Self { + opcode: ADD, + operand: Operand::InlineNone, + } + } + pub fn sub() -> Self { + Self { + opcode: SUB, + operand: Operand::InlineNone, + } + } + pub fn mul() -> Self { + Self { + opcode: MUL, + operand: Operand::InlineNone, + } + } + pub fn div() -> Self { + Self { + opcode: DIV, + operand: Operand::InlineNone, + } + } + pub fn div_un() -> Self { + Self { + opcode: DIV_UN, + operand: Operand::InlineNone, + } + } + pub fn rem() -> Self { + Self { + opcode: REM, + operand: Operand::InlineNone, + } + } + pub fn rem_un() -> Self { + Self { + opcode: REM_UN, + operand: Operand::InlineNone, + } + } + pub fn and() -> Self { + Self { + opcode: AND, + operand: Operand::InlineNone, + } + } + pub fn or() -> Self { + Self { + opcode: OR, + operand: Operand::InlineNone, + } + } + pub fn xor() -> Self { + Self { + opcode: XOR, + operand: Operand::InlineNone, + } + } + pub fn shl() -> Self { + Self { + opcode: SHL, + operand: Operand::InlineNone, + } + } + pub fn shr() -> Self { + Self { + opcode: SHR, + operand: Operand::InlineNone, + } + } + pub fn shr_un() -> Self { + Self { + opcode: SHR_UN, + operand: Operand::InlineNone, + } + } + pub fn neg() -> Self { + Self { + opcode: NEG, + operand: Operand::InlineNone, + } + } + pub fn not() -> Self { + Self { + opcode: NOT, + operand: Operand::InlineNone, + } + } + pub fn conv_i1() -> Self { + Self { + opcode: CONV_I1, + operand: Operand::InlineNone, + } + } + pub fn conv_i2() -> Self { + Self { + opcode: CONV_I2, + operand: Operand::InlineNone, + } + } + pub fn conv_i4() -> Self { + Self { + opcode: CONV_I4, + operand: Operand::InlineNone, + } + } + pub fn conv_i8() -> Self { + Self { + opcode: CONV_I8, + operand: Operand::InlineNone, + } + } + pub fn conv_r4() -> Self { + Self { + opcode: CONV_R4, + operand: Operand::InlineNone, + } + } + pub fn conv_r8() -> Self { + Self { + opcode: CONV_R8, + operand: Operand::InlineNone, + } + } + pub fn conv_u4() -> Self { + Self { + opcode: CONV_U4, + operand: Operand::InlineNone, + } + } + pub fn conv_u8() -> Self { + Self { + opcode: CONV_U8, + operand: Operand::InlineNone, + } + } + pub fn callvirt(val: u32) -> Self { + Self { + opcode: CALLVIRT, + operand: Operand::InlineMethod(val), + } + } + pub fn cpobj(val: u32) -> Self { + Self { + opcode: CPOBJ, + operand: Operand::InlineType(val), + } + } + pub fn ldobj(val: u32) -> Self { + Self { + opcode: LDOBJ, + operand: Operand::InlineType(val), + } + } + pub fn ldstr(val: u32) -> Self { + Self { + opcode: LDSTR, + operand: Operand::InlineString(val), + } + } + pub fn newobj(val: u32) -> Self { + Self { + opcode: NEWOBJ, + operand: Operand::InlineMethod(val), + } + } + pub fn castclass(val: u32) -> Self { + Self { + opcode: CASTCLASS, + operand: Operand::InlineType(val), + } + } + pub fn isinst(val: u32) -> Self { + Self { + opcode: ISINST, + operand: Operand::InlineType(val), + } + } + pub fn conv_r_un() -> Self { + Self { + opcode: CONV_R_UN, + operand: Operand::InlineNone, + } + } + pub fn unbox(val: u32) -> Self { + Self { + opcode: UNBOX, + operand: Operand::InlineType(val), + } + } + pub fn throw() -> Self { + Self { + opcode: THROW, + operand: Operand::InlineNone, + } + } + pub fn ldfld(val: u32) -> Self { + Self { + opcode: LDFLD, + operand: Operand::InlineField(val), + } + } + pub fn ldflda(val: u32) -> Self { + Self { + opcode: LDFLDA, + operand: Operand::InlineField(val), + } + } + pub fn stfld(val: u32) -> Self { + Self { + opcode: STFLD, + operand: Operand::InlineField(val), + } + } + pub fn ldsfld(val: u32) -> Self { + Self { + opcode: LDSFLD, + operand: Operand::InlineField(val), + } + } + pub fn ldsflda(val: u32) -> Self { + Self { + opcode: LDSFLDA, + operand: Operand::InlineField(val), + } + } + pub fn stsfld(val: u32) -> Self { + Self { + opcode: STSFLD, + operand: Operand::InlineField(val), + } + } + pub fn stobj(val: u32) -> Self { + Self { + opcode: STOBJ, + operand: Operand::InlineType(val), + } + } + pub fn conv_ovf_i1_un() -> Self { + Self { + opcode: CONV_OVF_I1_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i2_un() -> Self { + Self { + opcode: CONV_OVF_I2_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i4_un() -> Self { + Self { + opcode: CONV_OVF_I4_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i8_un() -> Self { + Self { + opcode: CONV_OVF_I8_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u1_un() -> Self { + Self { + opcode: CONV_OVF_U1_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u2_un() -> Self { + Self { + opcode: CONV_OVF_U2_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u4_un() -> Self { + Self { + opcode: CONV_OVF_U4_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u8_un() -> Self { + Self { + opcode: CONV_OVF_U8_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i_un() -> Self { + Self { + opcode: CONV_OVF_I_UN, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u_un() -> Self { + Self { + opcode: CONV_OVF_U_UN, + operand: Operand::InlineNone, + } + } + pub fn box_(val: u32) -> Self { + Self { + opcode: BOX, + operand: Operand::InlineType(val), + } + } + pub fn newarr(val: u32) -> Self { + Self { + opcode: NEWARR, + operand: Operand::InlineType(val), + } + } + pub fn ldlen() -> Self { + Self { + opcode: LDLEN, + operand: Operand::InlineNone, + } + } + pub fn ldelema(val: u32) -> Self { + Self { + opcode: LDELEMA, + operand: Operand::InlineType(val), + } + } + pub fn ldelem_i1() -> Self { + Self { + opcode: LDELEM_I1, + operand: Operand::InlineNone, + } + } + pub fn ldelem_u1() -> Self { + Self { + opcode: LDELEM_U1, + operand: Operand::InlineNone, + } + } + pub fn ldelem_i2() -> Self { + Self { + opcode: LDELEM_I2, + operand: Operand::InlineNone, + } + } + pub fn ldelem_u2() -> Self { + Self { + opcode: LDELEM_U2, + operand: Operand::InlineNone, + } + } + pub fn ldelem_i4() -> Self { + Self { + opcode: LDELEM_I4, + operand: Operand::InlineNone, + } + } + pub fn ldelem_u4() -> Self { + Self { + opcode: LDELEM_U4, + operand: Operand::InlineNone, + } + } + pub fn ldelem_i8() -> Self { + Self { + opcode: LDELEM_I8, + operand: Operand::InlineNone, + } + } + pub fn ldelem_i() -> Self { + Self { + opcode: LDELEM_I, + operand: Operand::InlineNone, + } + } + pub fn ldelem_r4() -> Self { + Self { + opcode: LDELEM_R4, + operand: Operand::InlineNone, + } + } + pub fn ldelem_r8() -> Self { + Self { + opcode: LDELEM_R8, + operand: Operand::InlineNone, + } + } + pub fn ldelem_ref() -> Self { + Self { + opcode: LDELEM_REF, + operand: Operand::InlineNone, + } + } + pub fn stelem_i() -> Self { + Self { + opcode: STELEM_I, + operand: Operand::InlineNone, + } + } + pub fn stelem_i1() -> Self { + Self { + opcode: STELEM_I1, + operand: Operand::InlineNone, + } + } + pub fn stelem_i2() -> Self { + Self { + opcode: STELEM_I2, + operand: Operand::InlineNone, + } + } + pub fn stelem_i4() -> Self { + Self { + opcode: STELEM_I4, + operand: Operand::InlineNone, + } + } + pub fn stelem_i8() -> Self { + Self { + opcode: STELEM_I8, + operand: Operand::InlineNone, + } + } + pub fn stelem_r4() -> Self { + Self { + opcode: STELEM_R4, + operand: Operand::InlineNone, + } + } + pub fn stelem_r8() -> Self { + Self { + opcode: STELEM_R8, + operand: Operand::InlineNone, + } + } + pub fn stelem_ref() -> Self { + Self { + opcode: STELEM_REF, + operand: Operand::InlineNone, + } + } + pub fn ldelem(val: u32) -> Self { + Self { + opcode: LDELEM, + operand: Operand::InlineType(val), + } + } + pub fn stelem(val: u32) -> Self { + Self { + opcode: STELEM, + operand: Operand::InlineType(val), + } + } + pub fn unbox_any(val: u32) -> Self { + Self { + opcode: UNBOX_ANY, + operand: Operand::InlineType(val), + } + } + pub fn conv_ovf_i1() -> Self { + Self { + opcode: CONV_OVF_I1, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u1() -> Self { + Self { + opcode: CONV_OVF_U1, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i2() -> Self { + Self { + opcode: CONV_OVF_I2, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u2() -> Self { + Self { + opcode: CONV_OVF_U2, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i4() -> Self { + Self { + opcode: CONV_OVF_I4, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u4() -> Self { + Self { + opcode: CONV_OVF_U4, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i8() -> Self { + Self { + opcode: CONV_OVF_I8, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u8() -> Self { + Self { + opcode: CONV_OVF_U8, + operand: Operand::InlineNone, + } + } + pub fn refanyval(val: u32) -> Self { + Self { + opcode: REFANYVAL, + operand: Operand::InlineType(val), + } + } + pub fn ckfinite() -> Self { + Self { + opcode: CKFINITE, + operand: Operand::InlineNone, + } + } + pub fn mkrefany(val: u32) -> Self { + Self { + opcode: MKREFANY, + operand: Operand::InlineType(val), + } + } + pub fn ldtoken(val: u32) -> Self { + Self { + opcode: LDTOKEN, + operand: Operand::InlineTok(val), + } + } + pub fn conv_u2() -> Self { + Self { + opcode: CONV_U2, + operand: Operand::InlineNone, + } + } + pub fn conv_u1() -> Self { + Self { + opcode: CONV_U1, + operand: Operand::InlineNone, + } + } + pub fn conv_i() -> Self { + Self { + opcode: CONV_I, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_i() -> Self { + Self { + opcode: CONV_OVF_I, + operand: Operand::InlineNone, + } + } + pub fn conv_ovf_u() -> Self { + Self { + opcode: CONV_OVF_U, + operand: Operand::InlineNone, + } + } + pub fn add_ovf() -> Self { + Self { + opcode: ADD_OVF, + operand: Operand::InlineNone, + } + } + pub fn add_ovf_un() -> Self { + Self { + opcode: ADD_OVF_UN, + operand: Operand::InlineNone, + } + } + pub fn mul_ovf() -> Self { + Self { + opcode: MUL_OVF, + operand: Operand::InlineNone, + } + } + pub fn mul_ovf_un() -> Self { + Self { + opcode: MUL_OVF_UN, + operand: Operand::InlineNone, + } + } + pub fn sub_ovf() -> Self { + Self { + opcode: SUB_OVF, + operand: Operand::InlineNone, + } + } + pub fn sub_ovf_un() -> Self { + Self { + opcode: SUB_OVF_UN, + operand: Operand::InlineNone, + } + } + pub fn endfinally() -> Self { + Self { + opcode: ENDFINALLY, + operand: Operand::InlineNone, + } + } + pub fn leave(val: i32) -> Self { + Self { + opcode: LEAVE, + operand: Operand::InlineBrTarget(val), + } + } + pub fn leave_s(val: i8) -> Self { + Self { + opcode: LEAVE_S, + operand: Operand::ShortInlineBrTarget(val), + } + } + pub fn stind_i() -> Self { + Self { + opcode: STIND_I, + operand: Operand::InlineNone, + } + } + pub fn conv_u() -> Self { + Self { + opcode: CONV_U, + operand: Operand::InlineNone, + } + } + pub fn arglist() -> Self { + Self { + opcode: ARGLIST, + operand: Operand::InlineNone, + } + } + pub fn ceq() -> Self { + Self { + opcode: CEQ, + operand: Operand::InlineNone, + } + } + pub fn cgt() -> Self { + Self { + opcode: CGT, + operand: Operand::InlineNone, + } + } + pub fn cgt_un() -> Self { + Self { + opcode: CGT_UN, + operand: Operand::InlineNone, + } + } + pub fn clt() -> Self { + Self { + opcode: CLT, + operand: Operand::InlineNone, + } + } + pub fn clt_un() -> Self { + Self { + opcode: CLT_UN, + operand: Operand::InlineNone, + } + } + pub fn ldftn(val: u32) -> Self { + Self { + opcode: LDFTN, + operand: Operand::InlineMethod(val), + } + } + pub fn ldvirtftn(val: u32) -> Self { + Self { + opcode: LDVIRTFTN, + operand: Operand::InlineMethod(val), + } + } + pub fn ldarg(val: u16) -> Self { + Self { + opcode: LDARG, + operand: Operand::InlineVar(val), + } + } + pub fn ldarga(val: u16) -> Self { + Self { + opcode: LDARGA, + operand: Operand::InlineVar(val), + } + } + pub fn starg(val: u16) -> Self { + Self { + opcode: STARG, + operand: Operand::InlineVar(val), + } + } + pub fn ldloc(val: u16) -> Self { + Self { + opcode: LDLOC, + operand: Operand::InlineVar(val), + } + } + pub fn ldloca(val: u16) -> Self { + Self { + opcode: LDLOCA, + operand: Operand::InlineVar(val), + } + } + pub fn stloc(val: u16) -> Self { + Self { + opcode: STLOC, + operand: Operand::InlineVar(val), + } + } + pub fn localloc() -> Self { + Self { + opcode: LOCALLOC, + operand: Operand::InlineNone, + } + } + pub fn endfilter() -> Self { + Self { + opcode: ENDFILTER, + operand: Operand::InlineNone, + } + } + pub fn unaligned(val: u8) -> Self { + Self { + opcode: UNALIGNED, + operand: Operand::ShortInlineI(SingleByte::Unsigned(val)), + } + } + pub fn volatile() -> Self { + Self { + opcode: VOLATILE, + operand: Operand::InlineNone, + } + } + pub fn tailcall() -> Self { + Self { + opcode: TAILCALL, + operand: Operand::InlineNone, + } + } + pub fn initobj(val: u32) -> Self { + Self { + opcode: INITOBJ, + operand: Operand::InlineType(val), + } + } + pub fn constrained(val: u32) -> Self { + Self { + opcode: CONSTRAINED, + operand: Operand::InlineType(val), + } + } + pub fn cpblk() -> Self { + Self { + opcode: CPBLK, + operand: Operand::InlineNone, + } + } + pub fn initblk() -> Self { + Self { + opcode: INITBLK, + operand: Operand::InlineNone, + } + } + pub fn rethrow() -> Self { + Self { + opcode: RETHROW, + operand: Operand::InlineNone, + } + } + pub fn sizeof(val: u32) -> Self { + Self { + opcode: SIZEOF, + operand: Operand::InlineType(val), + } + } + pub fn refanytype() -> Self { + Self { + opcode: REFANYTYPE, + operand: Operand::InlineNone, + } + } + pub fn readonly() -> Self { + Self { + opcode: READONLY, + operand: Operand::InlineNone, + } + } + + // Convenience methods + + pub fn load_int32(val: i32) -> Self { + match val { + 0 => Self::ldc_i4_0(), + 1 => Self::ldc_i4_1(), + 2 => Self::ldc_i4_2(), + 3 => Self::ldc_i4_3(), + 4 => Self::ldc_i4_4(), + 5 => Self::ldc_i4_5(), + 6 => Self::ldc_i4_6(), + 7 => Self::ldc_i4_7(), + 8 => Self::ldc_i4_8(), + i if i8::MIN as i32 <= i && i <= i8::MAX as i32 => Self::ldc_i4_s(i as i8), + i => Self::ldc_i4(i), + } + } + + pub fn load_argument(val: u16) -> Self { + match val { + 0 => Self::ldarg_0(), + 1 => Self::ldarg_1(), + 2 => Self::ldarg_2(), + 3 => Self::ldarg_3(), + i if i <= u8::MAX as u16 => Self::ldarg_s(i as u8), + i => Self::ldarg(i), + } + } + + pub fn store_local(val: u16) -> Self { + match val { + 0 => Self::stloc_0(), + 1 => Self::stloc_1(), + 2 => Self::stloc_2(), + 3 => Self::stloc_3(), + i if i <= u8::MAX as u16 => Self::stloc_s(i as u8), + i => Self::stloc(i), + } + } + + pub fn load_local(val: u16) -> Self { + match val { + 0 => Self::ldloc_0(), + 1 => Self::ldloc_1(), + 2 => Self::ldloc_2(), + 3 => Self::ldloc_3(), + i if i <= u8::MAX as u16 => Self::ldloc_s(i as u8), + i => Self::ldloc(i), + } + } + + pub fn load_local_address(val: u16) -> Self { + if val <= u8::MAX as u16 { + Self::ldloca_s(val as u8) + } else { + Self::ldloca(val) + } + } +} diff --git a/src/elastic_apm_profiler/src/cil/method.rs b/src/elastic_apm_profiler/src/cil/method.rs new file mode 100644 index 000000000..0dc6472dd --- /dev/null +++ b/src/elastic_apm_profiler/src/cil/method.rs @@ -0,0 +1,755 @@ +#![allow(non_upper_case_globals)] + +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Copyright 2019 Camden Reslink +// MIT License +// https://github.com/camdenreslink/clr-profiler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +use crate::{ + cil::{ + check_flag, il_u32, nearest_multiple, CorExceptionFlag, FatSectionClause, FatSectionHeader, + Instruction, Opcode, + Operand::{InlineBrTarget, InlineSwitch, ShortInlineBrTarget}, + Section, + }, + error::Error, + ffi::{mdToken, mdTokenNil}, +}; +use std::{ + convert::TryFrom, + fmt::{Display, Formatter}, +}; + +bitflags! { + /// Method header flags + pub struct CorILMethodFlags: u8 { + const CorILMethod_FatFormat = 0x3; + const CorILMethod_TinyFormat = 0x2; + const CorILMethod_MoreSects = 0x8; + const CorILMethod_InitLocals = 0x10; + const CorILMethod_FormatShift = 3; + const CorILMethod_FormatMask = ((1 << CorILMethodFlags::CorILMethod_FormatShift.bits()) - 1); + const CorILMethod_SmallFormat = 0x0000; + const CorILMethod_TinyFormat1 = 0x0006; + } +} + +#[derive(Debug)] +pub struct FatMethodHeader { + more_sects: bool, + init_locals: bool, + max_stack: u16, + code_size: u32, + local_var_sig_tok: u32, +} + +impl FatMethodHeader { + pub const SIZE: u8 = 12; +} + +#[derive(Debug)] +pub struct TinyMethodHeader { + code_size: u8, +} + +impl TinyMethodHeader { + pub const MAX_STACK: u8 = 8; +} + +#[derive(Debug)] +pub enum MethodHeader { + Fat(FatMethodHeader), + Tiny(TinyMethodHeader), +} + +impl MethodHeader { + /// creates a tiny method header + pub fn tiny(code_size: u8) -> MethodHeader { + MethodHeader::Tiny(TinyMethodHeader { code_size }) + } + + /// creates a fat method header + pub fn fat( + more_sects: bool, + init_locals: bool, + max_stack: u16, + code_size: u32, + local_var_sig_tok: u32, + ) -> Self { + MethodHeader::Fat(FatMethodHeader { + more_sects, + init_locals, + max_stack, + code_size, + local_var_sig_tok, + }) + } + + pub fn from_bytes(method_il: &[u8]) -> Result { + let header_flags = method_il[0]; + if Self::is_tiny(header_flags) { + let code_size = method_il[0] >> (CorILMethodFlags::CorILMethod_FormatShift.bits() - 1); + Ok(MethodHeader::Tiny(TinyMethodHeader { code_size })) + } else if Self::is_fat(header_flags) { + let more_sects = Self::more_sects(header_flags); + let init_locals = Self::init_locals(header_flags); + let max_stack = u16::from_le_bytes([method_il[2], method_il[3]]); + let code_size = il_u32(method_il, 4)?; + let local_var_sig_tok = il_u32(method_il, 8)?; + Ok(MethodHeader::Fat(FatMethodHeader { + more_sects, + init_locals, + max_stack, + code_size, + local_var_sig_tok, + })) + } else { + Err(Error::InvalidMethodHeader) + } + } + + pub fn into_bytes(&self) -> Vec { + match &self { + MethodHeader::Fat(header) => { + let mut bytes = Vec::with_capacity(FatMethodHeader::SIZE as usize); + let mut flags = CorILMethodFlags::CorILMethod_FatFormat.bits(); + if header.more_sects { + flags |= CorILMethodFlags::CorILMethod_MoreSects.bits(); + } + if header.init_locals { + flags |= CorILMethodFlags::CorILMethod_InitLocals.bits(); + } + bytes.push(flags); + bytes.push(FatMethodHeader::SIZE.reverse_bits()); + bytes.extend_from_slice(&header.max_stack.to_le_bytes()); + bytes.extend_from_slice(&header.code_size.to_le_bytes()); + bytes.extend_from_slice(&header.local_var_sig_tok.to_le_bytes()); + bytes + } + MethodHeader::Tiny(header) => { + let byte = header.code_size << 2 | CorILMethodFlags::CorILMethod_TinyFormat.bits(); + vec![byte] + } + } + } + + /// Instructions start and end + pub fn instructions(&self) -> (usize, usize) { + match self { + MethodHeader::Fat(header) => ( + FatMethodHeader::SIZE as usize, + (FatMethodHeader::SIZE as u32 + header.code_size - 1) as usize, + ), + MethodHeader::Tiny(header) => (1, header.code_size as usize), + } + } + + pub fn local_var_sig_tok(&self) -> u32 { + match self { + MethodHeader::Fat(header) => header.local_var_sig_tok, + MethodHeader::Tiny(_) => mdTokenNil, + } + } + + pub fn set_local_var_sig_tok(&mut self, token: mdToken) { + match self { + MethodHeader::Fat(header) => header.local_var_sig_tok = token, + MethodHeader::Tiny(_) => (), + } + } + + pub fn max_stack(&self) -> u16 { + match self { + MethodHeader::Fat(header) => header.max_stack, + MethodHeader::Tiny(_) => TinyMethodHeader::MAX_STACK as u16, + } + } + + pub fn code_size(&self) -> u32 { + match self { + MethodHeader::Fat(header) => header.code_size, + MethodHeader::Tiny(header) => header.code_size as u32, + } + } + + fn more_sects(method_header_flags: u8) -> bool { + check_flag( + method_header_flags, + CorILMethodFlags::CorILMethod_MoreSects.bits(), + ) + } + fn init_locals(method_header_flags: u8) -> bool { + check_flag( + method_header_flags, + CorILMethodFlags::CorILMethod_InitLocals.bits(), + ) + } + + fn is_tiny(method_header_flags: u8) -> bool { + // Check only the 2 least significant bits + (method_header_flags & 0b00000011) == CorILMethodFlags::CorILMethod_TinyFormat.bits() + } + + fn is_fat(method_header_flags: u8) -> bool { + // Check only the 2 least significant bits + (method_header_flags & 0b00000011) == CorILMethodFlags::CorILMethod_FatFormat.bits() + } +} + +#[derive(Debug)] +pub struct Method { + /// The starting memory address of the method, if read from IL + pub address: usize, + pub header: MethodHeader, + pub instructions: Vec, + pub sections: Vec
, +} + +impl Method { + /// Creates a tiny method with the given instructions. If the code size + /// of the instructions is greater than [u8::MAX], an error result is returned. + pub fn tiny(instructions: Vec) -> Result { + let code_size: usize = instructions.iter().map(|i| i.len()).sum(); + if code_size > u8::MAX as usize { + Err(Error::InvalidCil) + } else { + Ok(Method { + address: 0, + header: MethodHeader::tiny(code_size as u8), + instructions, + sections: vec![], + }) + } + } + + pub fn new(body: &[u8]) -> Result { + let address = body.as_ptr() as usize; + let header = MethodHeader::from_bytes(&body)?; + let (instructions_start, instructions_end) = header.instructions(); + let instruction_bytes = &body[instructions_start..=instructions_end]; + let instructions = Self::instructions_from_bytes(instruction_bytes)?; + let sections = match &header { + MethodHeader::Fat(header) if header.more_sects => { + // Sections are DWORD aligned + let sections_start = nearest_multiple(4, instructions_end + 1); + let sections_bytes = &body[sections_start..]; + Self::sections_from_bytes(sections_bytes)? + } + _ => vec![], // only fat headers with the more sections flag set have additional sections + }; + Ok(Method { + address, + header, + instructions, + sections, + }) + } + + /// Expands a tiny method into a fat method + pub fn expand_tiny_to_fat(&mut self) { + if let MethodHeader::Tiny(_) = self.header { + self.header = MethodHeader::Fat(FatMethodHeader { + more_sects: false, + init_locals: false, + max_stack: self.header.max_stack(), + code_size: self.instructions.iter().map(|i| i.len()).sum::() as u32, + local_var_sig_tok: mdTokenNil, + }); + } + } + + /// Expands small sections to fat sections + pub fn expand_small_sections_to_fat(&mut self) { + match &mut self.header { + MethodHeader::Fat(header) if header.more_sects => { + for section in &mut self.sections { + if let Section::SmallSection(section_header, clauses) = section { + let fat_section_header = FatSectionHeader { + is_eh_table: section_header.is_eh_table, + more_sects: section_header.more_sects, + data_size: (FatSectionHeader::LENGTH + + (FatSectionClause::LENGTH * clauses.len())) + as u32, + }; + + let fat_section_clauses: Vec = clauses + .iter() + .map(|c| FatSectionClause { + flag: c.flag, + try_offset: c.try_offset as u32, + try_length: c.try_length as u32, + handler_offset: c.handler_offset as u32, + handler_length: c.handler_length as u32, + class_token_or_filter_offset: c.class_token_or_filter_offset, + }) + .collect(); + *section = Section::FatSection(fat_section_header, fat_section_clauses); + } + } + } + _ => (), + } + } + + pub fn push_clauses(&mut self, mut clauses: Vec) -> Result<(), Error> { + match &mut self.header { + MethodHeader::Fat(method_header) => { + if method_header.more_sects { + // find the last fat section. + // Current versions of the CLR allow only one kind of additional section, + // an exception handling section + let mut idx = -1; + for (i, s) in self.sections.iter().enumerate() { + if let Section::FatSection(_, _) = s { + idx = i as i32; + } + } + + if idx == -1 { + return Err(Error::InvalidSectionHeader); + } + let section = self.sections.get_mut(idx as usize).unwrap(); + if let Section::FatSection(section_header, section_clauses) = section { + section_header.is_eh_table = true; + section_header.data_size += + (FatSectionClause::LENGTH * clauses.len()) as u32; + section_clauses.append(clauses.as_mut()); + } + } else { + method_header.more_sects = true; + self.sections.push(Section::FatSection( + FatSectionHeader { + is_eh_table: true, + more_sects: false, + data_size: (FatSectionHeader::LENGTH + + (FatSectionClause::LENGTH * clauses.len())) + as u32, + }, + clauses, + )) + } + + Ok(()) + } + _ => Err(Error::InvalidMethodHeader), + } + } + + pub fn push_clause(&mut self, clause: FatSectionClause) -> Result<(), Error> { + self.push_clauses(vec![clause]) + } + + pub fn into_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + let mut header_bytes = self.header.into_bytes(); + bytes.append(&mut header_bytes); + let mut instructions = self.instructions_to_bytes(); + let instructions_len = instructions.len(); + bytes.append(&mut instructions); + let mut section_bytes = self.sections_to_bytes(instructions_len); + bytes.append(&mut section_bytes); + bytes + } + + /// replaces an instruction at the specified index + pub fn replace(&mut self, index: usize, instruction: Instruction) -> Result<(), Error> { + let offset = self.get_offset(index); + + let (existing_len, existing_stack_size) = { + let existing_instruction = &self.instructions[index]; + ( + existing_instruction.len() as i64, + existing_instruction.stack_size() as i64, + ) + }; + + let len = instruction.len() as i64; + let stack_size = instruction.stack_size() as i64; + let len_diff = len - existing_len; + let stack_size_diff = stack_size - existing_stack_size; + + self.update_header(len_diff, Some(stack_size_diff))?; + self.update_sections(offset, len_diff)?; + self.update_instructions(index, offset, len_diff); + let _ = self.instructions.remove(index); + self.instructions.insert(index, instruction); + Ok(()) + } + + /// inserts an instruction at the specified index + pub fn insert(&mut self, index: usize, instruction: Instruction) -> Result<(), Error> { + let len = instruction.len() as i64; + let stack_size = instruction.stack_size() as i64; + self.update_header(len, Some(stack_size))?; + + let offset = self.get_offset(index); + self.update_sections(offset, len)?; + self.update_instructions(index, offset, len); + self.instructions.insert(index, instruction); + Ok(()) + } + + #[inline] + fn get_offset(&self, index: usize) -> usize { + self.instructions + .iter() + .take(index + 1) + .map(|i| i.len()) + .sum() + } + + pub fn get_instruction_offsets(&self) -> Vec { + let mut offset = 0; + let mut offsets = Vec::with_capacity(self.instructions.len()); + for instruction in &self.instructions { + offsets.push(offset); + offset += instruction.len() as u32; + } + + offsets + } + + fn update_instructions(&mut self, index: usize, offset: usize, len: i64) { + // update the offsets of control flow instructions and expand any short instructions: + // + // 1. for control flow instructions before the target index, + // if the offset is positive and results in an index after the target index, + // add len to the offset + // 2. for control flow instructions after the target index, + // if the offset is negative and results in an index before the target index, + // subtract len from the offset i.e. offset is further away + let mut map: Vec = self.instructions.iter().map(|i| i.len()).collect(); + let mut updated_instructions = vec![]; + for (i, instruction) in self.instructions.iter_mut().enumerate() { + if i < index { + if let ShortInlineBrTarget(target_offset) = &mut instruction.operand { + if *target_offset >= 0 { + let mut sum_len = 0; + let mut j = 1; + while sum_len < *target_offset as usize { + sum_len += map[i + j]; + j += 1; + } + if i + j > index { + let n = *target_offset as i32 + len as i32; + if n > i8::MAX as i32 { + let current_len = instruction.len(); + + // update the instruction + instruction.operand = InlineBrTarget(n); + instruction.opcode = Opcode::short_to_long_form(instruction.opcode); + + // update the map with the new instruction len and record + // the original offset and len diff. + let new_len = instruction.len(); + map[i] = new_len; + updated_instructions.push((offset, (new_len - current_len) as i64)); + } else { + *target_offset = n as i8; + } + } + } + } else if let InlineBrTarget(target_offset) = &mut instruction.operand { + if *target_offset >= 0 { + let mut sum_len = 0; + let mut j = 1; + while sum_len < *target_offset as usize { + sum_len += map[i + j]; + j += 1; + } + if i + j > index { + let n = *target_offset + len as i32; + *target_offset = n; + } + } + } else if let InlineSwitch(count, target_offsets) = &mut instruction.operand { + for target_offset in target_offsets { + if *target_offset >= 0 { + let mut sum_len = 0; + let mut j = 1; + while sum_len < *target_offset as usize { + sum_len += map[i + j]; + j += 1; + } + if i + j > index { + *target_offset += len as i32; + } + } + } + } + } else { + if let ShortInlineBrTarget(target_offset) = &mut instruction.operand { + if *target_offset < 0 { + let mut sum_len = 0; + let mut j = 0; + while *target_offset < sum_len { + sum_len -= map[i - j] as i8; + j += 1; + } + if i - j < index { + let n = *target_offset as i32 - len as i32; + if n < i8::MIN as i32 { + let current_len = instruction.len(); + + // update the instruction + instruction.operand = InlineBrTarget(n); + instruction.opcode = Opcode::short_to_long_form(instruction.opcode); + + // update the map with the new instruction len and record + // the original offset and len diff. + let new_len = instruction.len(); + map[i] = new_len; + updated_instructions.push((offset, (new_len - current_len) as i64)); + } else { + *target_offset = n as i8; + } + } + } + } else if let InlineBrTarget(target_offset) = &mut instruction.operand { + if *target_offset < 0 { + let mut sum_len = 0; + let mut j = 0; + while *target_offset < sum_len { + sum_len -= map[i - j] as i32; + j += 1; + } + if i - j < index { + let n = *target_offset - len as i32; + *target_offset = n; + } + } + } else if let InlineSwitch(count, target_offsets) = &mut instruction.operand { + for target_offset in target_offsets { + if *target_offset < 0 { + let mut sum_len = 0; + let mut j = 0; + while *target_offset < sum_len { + sum_len -= map[i - j] as i32; + j += 1; + } + if i - j < index { + *target_offset -= len as i32; + } + } + } + } + } + } + + if !updated_instructions.is_empty() { + for (offset, len) in updated_instructions { + self.update_header(len, None).unwrap(); + self.update_sections(offset, len).unwrap(); + } + } + } + + fn update_header(&mut self, len: i64, stack_size: Option) -> Result<(), Error> { + // update code_size and max_stack in method_header + match &mut self.header { + MethodHeader::Fat(header) => { + let size = header.code_size as i64 + len as i64; + header.code_size = u32::try_from(size).or(Err(Error::CodeSize))?; + if let Some(stack_size) = stack_size { + let max_stack = header.max_stack as i64 + stack_size as i64; + header.max_stack = u16::try_from(max_stack).or(Err(Error::StackSize))?; + } + } + MethodHeader::Tiny(header) => { + let size = header.code_size as i64 + len as i64; + header.code_size = u8::try_from(size) + .or_else(|err| todo!("Expand tiny into fat header!, {:?}", err))?; + } + } + + Ok(()) + } + + fn update_sections(&mut self, offset: usize, len: i64) -> Result<(), Error> { + // update try offset, try length, handler offset, handler length, and filter offset + for section in &mut self.sections { + match section { + Section::FatSection(_, clauses) => { + for clause in clauses { + if (offset as u32) <= clause.try_offset { + let try_offset = clause.try_offset as i64 + len as i64; + clause.try_offset = + u32::try_from(try_offset).or(Err(Error::CodeSize))?; + } else if (offset as u32) <= clause.try_offset + clause.try_length { + let try_length = clause.try_length as i64 + len as i64; + clause.try_length = + u32::try_from(try_length).or(Err(Error::CodeSize))?; + } + + if (offset as u32) <= clause.handler_offset { + let handler_offset = clause.handler_offset as i64 + len as i64; + clause.handler_offset = + u32::try_from(handler_offset).or(Err(Error::CodeSize))?; + } else if (offset as u32) <= clause.handler_offset + clause.handler_length { + let handler_length = clause.handler_length as i64 + len as i64; + clause.handler_length = + u32::try_from(handler_length).or(Err(Error::CodeSize))?; + } + + if clause + .flag + .contains(CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_FILTER) + && (offset as u32) <= clause.class_token_or_filter_offset + { + let filter_offset = + clause.class_token_or_filter_offset as i64 + len as i64; + clause.class_token_or_filter_offset = + u32::try_from(filter_offset).or(Err(Error::CodeSize))?; + } + } + } + Section::SmallSection(_, clauses) => { + for clause in clauses { + if (offset as u16) <= clause.try_offset { + let try_offset = clause.try_offset as i64 + len as i64; + clause.try_offset = u16::try_from(try_offset) + .or_else(|err| todo!("Expand into fat section!, {:?}", err))?; + } else if (offset as u16) <= clause.try_offset + clause.try_length as u16 { + let try_length = clause.try_length as i64 + len as i64; + clause.try_length = + u8::try_from(try_length).or(Err(Error::CodeSize))?; + } + + if (offset as u16) <= clause.handler_offset { + let handler_offset = clause.handler_offset as i64 + len as i64; + clause.handler_offset = u16::try_from(handler_offset) + .or_else(|err| todo!("Expand into fat section!, {:?}", err))?; + } else if (offset as u16) + <= clause.handler_offset + clause.handler_length as u16 + { + let handler_length = clause.handler_length as i64 + len as i64; + clause.handler_length = + u8::try_from(handler_length).or(Err(Error::CodeSize))?; + } + + if clause + .flag + .contains(CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_FILTER) + && (offset as u16) <= clause.class_token_or_filter_offset as u16 + { + let filter_offset = + clause.class_token_or_filter_offset as i64 + len as i64; + clause.class_token_or_filter_offset = + u32::try_from(filter_offset).or(Err(Error::CodeSize))?; + } + } + } + } + } + + Ok(()) + } + + /// Inserts instructions at the start + pub fn insert_prelude(&mut self, instructions: Vec) -> Result<(), Error> { + let len: usize = instructions.iter().map(|i| i.len()).sum(); + let stack_size: usize = instructions.iter().map(|i| i.stack_size()).sum(); + self.update_header(len as i64, Some(stack_size as i64))?; + self.update_sections(0, len as i64)?; + self.instructions.splice(0..0, instructions); + Ok(()) + } + + fn instructions_from_bytes(il: &[u8]) -> Result, Error> { + let mut index = 0; + let mut instructions = Vec::new(); + while index < il.len() { + let il = &il[index..]; + let instruction = Instruction::from_bytes(il)?; + index += instruction.len(); + instructions.push(instruction); + } + Ok(instructions) + } + + fn sections_from_bytes(il: &[u8]) -> Result, Error> { + let mut index = 0; + let mut sections = Vec::new(); + while index < il.len() { + let il = &il[index..]; + let section = Section::from_bytes(il)?; + index += section.data_size(); + sections.push(section); + } + Ok(sections) + } + + fn instructions_to_bytes(&self) -> Vec { + self.instructions + .iter() + .flat_map(|i| i.into_bytes()) + .collect() + } + + fn sections_to_bytes(&self, instruction_len: usize) -> Vec { + let mut bytes = Vec::new(); + match &self.header { + MethodHeader::Fat(header) if header.more_sects => { + // Sections must be DWORD aligned. Add zero padding at the end of instructions to achieve alignment. + let padding_byte_size = nearest_multiple(4, instruction_len) - instruction_len; + bytes.resize(padding_byte_size, 0); + let mut section_bytes = self.sections.iter().flat_map(|s| s.into_bytes()).collect(); + bytes.append(&mut section_bytes); + } + _ => (), + } + bytes + } +} + +impl Display for Method { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut d = f.debug_struct("Method"); + let instructions = self.instructions_to_bytes(); + d.field("header", &self.header.into_bytes()) + .field("instructions", &instructions); + + match &self.header { + MethodHeader::Fat(header) if header.more_sects => { + let padding_byte_size = + nearest_multiple(4, instructions.len()) - instructions.len(); + if padding_byte_size > 0 { + d.field("alignment", &vec![0; padding_byte_size]); + } + } + _ => (), + }; + + if !self.sections.is_empty() { + d.field( + "sections", + &self + .sections + .iter() + .map(|s| s.into_bytes()) + .collect::>(), + ); + } + + d.finish() + } +} diff --git a/src/elastic_apm_profiler/src/cil/mod.rs b/src/elastic_apm_profiler/src/cil/mod.rs new file mode 100644 index 000000000..496df0208 --- /dev/null +++ b/src/elastic_apm_profiler/src/cil/mod.rs @@ -0,0 +1,15 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +mod cor; +mod helpers; +mod instruction; +mod method; +mod opcode; +mod section; + +pub use self::{cor::*, helpers::*, instruction::*, method::*, opcode::*, section::*}; + +pub const MAX_LENGTH: u32 = 1024; diff --git a/src/elastic_apm_profiler/src/cil/opcode.rs b/src/elastic_apm_profiler/src/cil/opcode.rs new file mode 100644 index 000000000..74cd7a559 --- /dev/null +++ b/src/elastic_apm_profiler/src/cil/opcode.rs @@ -0,0 +1,2226 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Copyright 2019 Camden Reslink +// MIT License +// https://github.com/camdenreslink/clr-profiler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::error::Error; + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum StackBehaviorPop { + Pop0, + Pop1, + VarPop, + PopI, + Pop1Pop1, + PopIPopI, + PopIPopI8, + PopIPopR4, + PopIPopR8, + PopRef, + PopRefPop1, + PopIPop1, + PopRefPopI, + PopRefPopIPopI, + PopRefPopIPopI8, + PopRefPopIPopR4, + PopRefPopIPopR8, + PopRefPopIPopRef, + PopRefPopIPop1, + PopIPopIPopI, +} +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum StackBehaviorPush { + Push0, + Push1, + PushI, + PushRef, + PushI8, + PushR4, + PushR8, + Push1Push1, + VarPush, +} +impl StackBehaviorPush { + /// the size on the stack + pub fn size(&self) -> usize { + match self { + Push0 => 0, + Push1 => 1, + PushI => 1, + PushRef => 1, + PushI8 => 1, + PushR4 => 1, + PushR8 => 1, + Push1Push1 => 2, + VarPush => 1, + } + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum OperandParams { + InlineNone, + ShortInlineVar, + InlineVar, + ShortInlineI, + InlineI, + InlineI8, + ShortInlineR, + InlineR, + InlineMethod, + InlineSig, + ShortInlineBrTarget, + InlineBrTarget, + InlineSwitch, + InlineType, + InlineString, + InlineField, + InlineTok, +} +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum OpcodeKind { + Primitive, + Macro, + ObjModel, + Internal, + Prefix, +} +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum ControlFlow { + Next, + Break, + Return, + Branch, + CondBranch, + Call, + Throw, + Meta, +} +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct Opcode { + pub name: &'static str, + pub stack_behavior_pop: StackBehaviorPop, + pub stack_behavior_push: StackBehaviorPush, + pub operand_params: OperandParams, + pub opcode_kind: OpcodeKind, + pub len: u8, + pub byte_1: u8, + pub byte_2: u8, + pub control_flow: ControlFlow, +} + +impl Opcode { + pub const fn new( + name: &'static str, + stack_behavior_pop: StackBehaviorPop, + stack_behavior_push: StackBehaviorPush, + operand_params: OperandParams, + opcode_kind: OpcodeKind, + len: u8, + byte_1: u8, + byte_2: u8, + control_flow: ControlFlow, + ) -> Self { + Opcode { + name, + stack_behavior_pop, + stack_behavior_push, + operand_params, + opcode_kind, + len, + byte_1, + byte_2, + control_flow, + } + } + //noinspection RsNonExhaustiveMatch + pub fn from_byte(byte: u8) -> Self { + match byte { + 0x00 => NOP, + 0x01 => BREAK, + 0x02 => LDARG_0, + 0x03 => LDARG_1, + 0x04 => LDARG_2, + 0x05 => LDARG_3, + 0x06 => LDLOC_0, + 0x07 => LDLOC_1, + 0x08 => LDLOC_2, + 0x09 => LDLOC_3, + 0x0A => STLOC_0, + 0x0B => STLOC_1, + 0x0C => STLOC_2, + 0x0D => STLOC_3, + 0x0E => LDARG_S, + 0x0F => LDARGA_S, + 0x10 => STARG_S, + 0x11 => LDLOC_S, + 0x12 => LDLOCA_S, + 0x13 => STLOC_S, + 0x14 => LDNULL, + 0x15 => LDC_I4_M1, + 0x16 => LDC_I4_0, + 0x17 => LDC_I4_1, + 0x18 => LDC_I4_2, + 0x19 => LDC_I4_3, + 0x1A => LDC_I4_4, + 0x1B => LDC_I4_5, + 0x1C => LDC_I4_6, + 0x1D => LDC_I4_7, + 0x1E => LDC_I4_8, + 0x1F => LDC_I4_S, + 0x20 => LDC_I4, + 0x21 => LDC_I8, + 0x22 => LDC_R4, + 0x23 => LDC_R8, + 0x24 => UNUSED49, + 0x25 => DUP, + 0x26 => POP, + 0x27 => JMP, + 0x28 => CALL, + 0x29 => CALLI, + 0x2A => RET, + 0x2B => BR_S, + 0x2C => BRFALSE_S, + 0x2D => BRTRUE_S, + 0x2E => BEQ_S, + 0x2F => BGE_S, + 0x30 => BGT_S, + 0x31 => BLE_S, + 0x32 => BLT_S, + 0x33 => BNE_UN_S, + 0x34 => BGE_UN_S, + 0x35 => BGT_UN_S, + 0x36 => BLE_UN_S, + 0x37 => BLT_UN_S, + 0x38 => BR, + 0x39 => BRFALSE, + 0x3A => BRTRUE, + 0x3B => BEQ, + 0x3C => BGE, + 0x3D => BGT, + 0x3E => BLE, + 0x3F => BLT, + 0x40 => BNE_UN, + 0x41 => BGE_UN, + 0x42 => BGT_UN, + 0x43 => BLE_UN, + 0x44 => BLT_UN, + 0x45 => SWITCH, + 0x46 => LDIND_I1, + 0x47 => LDIND_U1, + 0x48 => LDIND_I2, + 0x49 => LDIND_U2, + 0x4A => LDIND_I4, + 0x4B => LDIND_U4, + 0x4C => LDIND_I8, + 0x4D => LDIND_I, + 0x4E => LDIND_R4, + 0x4F => LDIND_R8, + 0x50 => LDIND_REF, + 0x51 => STIND_REF, + 0x52 => STIND_I1, + 0x53 => STIND_I2, + 0x54 => STIND_I4, + 0x55 => STIND_I8, + 0x56 => STIND_R4, + 0x57 => STIND_R8, + 0x58 => ADD, + 0x59 => SUB, + 0x5A => MUL, + 0x5B => DIV, + 0x5C => DIV_UN, + 0x5D => REM, + 0x5E => REM_UN, + 0x5F => AND, + 0x60 => OR, + 0x61 => XOR, + 0x62 => SHL, + 0x63 => SHR, + 0x64 => SHR_UN, + 0x65 => NEG, + 0x66 => NOT, + 0x67 => CONV_I1, + 0x68 => CONV_I2, + 0x69 => CONV_I4, + 0x6A => CONV_I8, + 0x6B => CONV_R4, + 0x6C => CONV_R8, + 0x6D => CONV_U4, + 0x6E => CONV_U8, + 0x6F => CALLVIRT, + 0x70 => CPOBJ, + 0x71 => LDOBJ, + 0x72 => LDSTR, + 0x73 => NEWOBJ, + 0x74 => CASTCLASS, + 0x75 => ISINST, + 0x76 => CONV_R_UN, + 0x77 => UNUSED58, + 0x78 => UNUSED1, + 0x79 => UNBOX, + 0x7A => THROW, + 0x7B => LDFLD, + 0x7C => LDFLDA, + 0x7D => STFLD, + 0x7E => LDSFLD, + 0x7F => LDSFLDA, + 0x80 => STSFLD, + 0x81 => STOBJ, + 0x82 => CONV_OVF_I1_UN, + 0x83 => CONV_OVF_I2_UN, + 0x84 => CONV_OVF_I4_UN, + 0x85 => CONV_OVF_I8_UN, + 0x86 => CONV_OVF_U1_UN, + 0x87 => CONV_OVF_U2_UN, + 0x88 => CONV_OVF_U4_UN, + 0x89 => CONV_OVF_U8_UN, + 0x8A => CONV_OVF_I_UN, + 0x8B => CONV_OVF_U_UN, + 0x8C => BOX, + 0x8D => NEWARR, + 0x8E => LDLEN, + 0x8F => LDELEMA, + 0x90 => LDELEM_I1, + 0x91 => LDELEM_U1, + 0x92 => LDELEM_I2, + 0x93 => LDELEM_U2, + 0x94 => LDELEM_I4, + 0x95 => LDELEM_U4, + 0x96 => LDELEM_I8, + 0x97 => LDELEM_I, + 0x98 => LDELEM_R4, + 0x99 => LDELEM_R8, + 0x9A => LDELEM_REF, + 0x9B => STELEM_I, + 0x9C => STELEM_I1, + 0x9D => STELEM_I2, + 0x9E => STELEM_I4, + 0x9F => STELEM_I8, + 0xA0 => STELEM_R4, + 0xA1 => STELEM_R8, + 0xA2 => STELEM_REF, + 0xA3 => LDELEM, + 0xA4 => STELEM, + 0xA5 => UNBOX_ANY, + 0xA6 => UNUSED5, + 0xA7 => UNUSED6, + 0xA8 => UNUSED7, + 0xA9 => UNUSED8, + 0xAA => UNUSED9, + 0xAB => UNUSED10, + 0xAC => UNUSED11, + 0xAD => UNUSED12, + 0xAE => UNUSED13, + 0xAF => UNUSED14, + 0xB0 => UNUSED15, + 0xB1 => UNUSED16, + 0xB2 => UNUSED17, + 0xB3 => CONV_OVF_I1, + 0xB4 => CONV_OVF_U1, + 0xB5 => CONV_OVF_I2, + 0xB6 => CONV_OVF_U2, + 0xB7 => CONV_OVF_I4, + 0xB8 => CONV_OVF_U4, + 0xB9 => CONV_OVF_I8, + 0xBA => CONV_OVF_U8, + 0xBB => UNUSED50, + 0xBC => UNUSED18, + 0xBD => UNUSED19, + 0xBE => UNUSED20, + 0xBF => UNUSED21, + 0xC0 => UNUSED22, + 0xC1 => UNUSED23, + 0xC2 => REFANYVAL, + 0xC3 => CKFINITE, + 0xC4 => UNUSED24, + 0xC5 => UNUSED25, + 0xC6 => MKREFANY, + 0xC7 => UNUSED59, + 0xC8 => UNUSED60, + 0xC9 => UNUSED61, + 0xCA => UNUSED62, + 0xCB => UNUSED63, + 0xCC => UNUSED64, + 0xCD => UNUSED65, + 0xCE => UNUSED66, + 0xCF => UNUSED67, + 0xD0 => LDTOKEN, + 0xD1 => CONV_U2, + 0xD2 => CONV_U1, + 0xD3 => CONV_I, + 0xD4 => CONV_OVF_I, + 0xD5 => CONV_OVF_U, + 0xD6 => ADD_OVF, + 0xD7 => ADD_OVF_UN, + 0xD8 => MUL_OVF, + 0xD9 => MUL_OVF_UN, + 0xDA => SUB_OVF, + 0xDB => SUB_OVF_UN, + 0xDC => ENDFINALLY, + 0xDD => LEAVE, + 0xDE => LEAVE_S, + 0xDF => STIND_I, + 0xE0 => CONV_U, + 0xE1 => UNUSED26, + 0xE2 => UNUSED27, + 0xE3 => UNUSED28, + 0xE4 => UNUSED29, + 0xE5 => UNUSED30, + 0xE6 => UNUSED31, + 0xE7 => UNUSED32, + 0xE8 => UNUSED33, + 0xE9 => UNUSED34, + 0xEA => UNUSED35, + 0xEB => UNUSED36, + 0xEC => UNUSED37, + 0xED => UNUSED38, + 0xEE => UNUSED39, + 0xEF => UNUSED40, + 0xF0 => UNUSED41, + 0xF1 => UNUSED42, + 0xF2 => UNUSED43, + 0xF3 => UNUSED44, + 0xF4 => UNUSED45, + 0xF5 => UNUSED46, + 0xF6 => UNUSED47, + 0xF7 => UNUSED48, + 0xF8 => PREFIX7, + 0xF9 => PREFIX6, + 0xFA => PREFIX5, + 0xFB => PREFIX4, + 0xFC => PREFIX3, + 0xFD => PREFIX2, + 0xFE => PREFIX1, + 0xFF => PREFIXREF, + } + } + pub fn from_byte_pair(pair: (u8, u8)) -> Result { + // #define STP1 0xFE + // #define REFPRE 0xFF + match pair { + (0xFE, 0x00) => Ok(ARGLIST), + (0xFE, 0x01) => Ok(CEQ), + (0xFE, 0x02) => Ok(CGT), + (0xFE, 0x03) => Ok(CGT_UN), + (0xFE, 0x04) => Ok(CLT), + (0xFE, 0x05) => Ok(CLT_UN), + (0xFE, 0x06) => Ok(LDFTN), + (0xFE, 0x07) => Ok(LDVIRTFTN), + (0xFE, 0x08) => Ok(UNUSED56), + (0xFE, 0x09) => Ok(LDARG), + (0xFE, 0x0A) => Ok(LDARGA), + (0xFE, 0x0B) => Ok(STARG), + (0xFE, 0x0C) => Ok(LDLOC), + (0xFE, 0x0D) => Ok(LDLOCA), + (0xFE, 0x0E) => Ok(STLOC), + (0xFE, 0x0F) => Ok(LOCALLOC), + (0xFE, 0x10) => Ok(UNUSED57), + (0xFE, 0x11) => Ok(ENDFILTER), + (0xFE, 0x12) => Ok(UNALIGNED), + (0xFE, 0x13) => Ok(VOLATILE), + (0xFE, 0x14) => Ok(TAILCALL), + (0xFE, 0x15) => Ok(INITOBJ), + (0xFE, 0x16) => Ok(CONSTRAINED), + (0xFE, 0x17) => Ok(CPBLK), + (0xFE, 0x18) => Ok(INITBLK), + (0xFE, 0x19) => Ok(UNUSED69), + (0xFE, 0x1A) => Ok(RETHROW), + (0xFE, 0x1B) => Ok(UNUSED51), + (0xFE, 0x1C) => Ok(SIZEOF), + (0xFE, 0x1D) => Ok(REFANYTYPE), + (0xFE, 0x1E) => Ok(READONLY), + (0xFE, 0x1F) => Ok(UNUSED53), + (0xFE, 0x20) => Ok(UNUSED54), + (0xFE, 0x21) => Ok(UNUSED55), + (0xFE, 0x22) => Ok(UNUSED70), + _ => Err(Error::InvalidCilOpcode), + } + } + + pub fn short_to_long_form(opcode: Opcode) -> Opcode { + match opcode { + BRFALSE_S => BRFALSE, + BRTRUE_S => BRTRUE, + BEQ_S => BEQ, + BGE_S => BGE, + BGT_S => BGT, + BLE_S => BLE, + BLT_S => BLT, + BR_S => BR, + BGE_UN_S => BGE_UN, + BGT_UN_S => BGT_UN, + BLE_UN_S => BLE_UN, + BLT_UN_S => BLT_UN, + BNE_UN_S => BNE_UN, + _ => opcode, + } + } +} + +use self::{ + ControlFlow::*, OpcodeKind::*, OperandParams::*, StackBehaviorPop::*, StackBehaviorPush::*, +}; + +pub const NOP: Opcode = Opcode::new( + "nop", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0x00, Next, +); +pub const BREAK: Opcode = Opcode::new( + "break", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0x01, Break, +); +pub const LDARG_0: Opcode = Opcode::new( + "ldarg.0", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x02, Next, +); +pub const LDARG_1: Opcode = Opcode::new( + "ldarg.1", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x03, Next, +); +pub const LDARG_2: Opcode = Opcode::new( + "ldarg.2", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x04, Next, +); +pub const LDARG_3: Opcode = Opcode::new( + "ldarg.3", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x05, Next, +); +pub const LDLOC_0: Opcode = Opcode::new( + "ldloc.0", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x06, Next, +); +pub const LDLOC_1: Opcode = Opcode::new( + "ldloc.1", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x07, Next, +); +pub const LDLOC_2: Opcode = Opcode::new( + "ldloc.2", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x08, Next, +); +pub const LDLOC_3: Opcode = Opcode::new( + "ldloc.3", Pop0, Push1, InlineNone, Macro, 1, 0xFF, 0x09, Next, +); +pub const STLOC_0: Opcode = Opcode::new( + "stloc.0", Pop1, Push0, InlineNone, Macro, 1, 0xFF, 0x0A, Next, +); +pub const STLOC_1: Opcode = Opcode::new( + "stloc.1", Pop1, Push0, InlineNone, Macro, 1, 0xFF, 0x0B, Next, +); +pub const STLOC_2: Opcode = Opcode::new( + "stloc.2", Pop1, Push0, InlineNone, Macro, 1, 0xFF, 0x0C, Next, +); +pub const STLOC_3: Opcode = Opcode::new( + "stloc.3", Pop1, Push0, InlineNone, Macro, 1, 0xFF, 0x0D, Next, +); +pub const LDARG_S: Opcode = Opcode::new( + "ldarg.s", + Pop0, + Push1, + ShortInlineVar, + Macro, + 1, + 0xFF, + 0x0E, + Next, +); +pub const LDARGA_S: Opcode = Opcode::new( + "ldarga.s", + Pop0, + PushI, + ShortInlineVar, + Macro, + 1, + 0xFF, + 0x0F, + Next, +); +pub const STARG_S: Opcode = Opcode::new( + "starg.s", + Pop1, + Push0, + ShortInlineVar, + Macro, + 1, + 0xFF, + 0x10, + Next, +); +pub const LDLOC_S: Opcode = Opcode::new( + "ldloc.s", + Pop0, + Push1, + ShortInlineVar, + Macro, + 1, + 0xFF, + 0x11, + Next, +); +pub const LDLOCA_S: Opcode = Opcode::new( + "ldloca.s", + Pop0, + PushI, + ShortInlineVar, + Macro, + 1, + 0xFF, + 0x12, + Next, +); +pub const STLOC_S: Opcode = Opcode::new( + "stloc.s", + Pop1, + Push0, + ShortInlineVar, + Macro, + 1, + 0xFF, + 0x13, + Next, +); +pub const LDNULL: Opcode = Opcode::new( + "ldnull", Pop0, PushRef, InlineNone, Primitive, 1, 0xFF, 0x14, Next, +); +pub const LDC_I4_M1: Opcode = Opcode::new( + "ldc.i4.m1", + Pop0, + PushI, + InlineNone, + Macro, + 1, + 0xFF, + 0x15, + Next, +); +pub const LDC_I4_0: Opcode = Opcode::new( + "ldc.i4.0", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x16, Next, +); +pub const LDC_I4_1: Opcode = Opcode::new( + "ldc.i4.1", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x17, Next, +); +pub const LDC_I4_2: Opcode = Opcode::new( + "ldc.i4.2", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x18, Next, +); +pub const LDC_I4_3: Opcode = Opcode::new( + "ldc.i4.3", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x19, Next, +); +pub const LDC_I4_4: Opcode = Opcode::new( + "ldc.i4.4", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x1A, Next, +); +pub const LDC_I4_5: Opcode = Opcode::new( + "ldc.i4.5", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x1B, Next, +); +pub const LDC_I4_6: Opcode = Opcode::new( + "ldc.i4.6", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x1C, Next, +); +pub const LDC_I4_7: Opcode = Opcode::new( + "ldc.i4.7", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x1D, Next, +); +pub const LDC_I4_8: Opcode = Opcode::new( + "ldc.i4.8", Pop0, PushI, InlineNone, Macro, 1, 0xFF, 0x1E, Next, +); +pub const LDC_I4_S: Opcode = Opcode::new( + "ldc.i4.s", + Pop0, + PushI, + ShortInlineI, + Macro, + 1, + 0xFF, + 0x1F, + Next, +); +pub const LDC_I4: Opcode = Opcode::new( + "ldc.i4", Pop0, PushI, InlineI, Primitive, 1, 0xFF, 0x20, Next, +); +pub const LDC_I8: Opcode = Opcode::new( + "ldc.i8", Pop0, PushI8, InlineI8, Primitive, 1, 0xFF, 0x21, Next, +); +pub const LDC_R4: Opcode = Opcode::new( + "ldc.r4", + Pop0, + PushR4, + ShortInlineR, + Primitive, + 1, + 0xFF, + 0x22, + Next, +); +pub const LDC_R8: Opcode = Opcode::new( + "ldc.r8", Pop0, PushR8, InlineR, Primitive, 1, 0xFF, 0x23, Next, +); +pub const UNUSED49: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0x24, Next, +); +pub const DUP: Opcode = Opcode::new( + "dup", Pop1, Push1Push1, InlineNone, Primitive, 1, 0xFF, 0x25, Next, +); +pub const POP: Opcode = Opcode::new( + "pop", Pop1, Push0, InlineNone, Primitive, 1, 0xFF, 0x26, Next, +); +pub const JMP: Opcode = Opcode::new( + "jmp", + Pop0, + Push0, + InlineMethod, + Primitive, + 1, + 0xFF, + 0x27, + Call, +); +pub const CALL: Opcode = Opcode::new( + "call", + VarPop, + VarPush, + InlineMethod, + Primitive, + 1, + 0xFF, + 0x28, + Call, +); +pub const CALLI: Opcode = Opcode::new( + "calli", VarPop, VarPush, InlineSig, Primitive, 1, 0xFF, 0x29, Call, +); +pub const RET: Opcode = Opcode::new( + "ret", VarPop, Push0, InlineNone, Primitive, 1, 0xFF, 0x2A, Return, +); +pub const BR_S: Opcode = Opcode::new( + "br.s", + Pop0, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x2B, + Branch, +); +pub const BRFALSE_S: Opcode = Opcode::new( + "brfalse.s", + PopI, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x2C, + CondBranch, +); +pub const BRTRUE_S: Opcode = Opcode::new( + "brtrue.s", + PopI, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x2D, + CondBranch, +); +pub const BEQ_S: Opcode = Opcode::new( + "beq.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x2E, + CondBranch, +); +pub const BGE_S: Opcode = Opcode::new( + "bge.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x2F, + CondBranch, +); +pub const BGT_S: Opcode = Opcode::new( + "bgt.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x30, + CondBranch, +); +pub const BLE_S: Opcode = Opcode::new( + "ble.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x31, + CondBranch, +); +pub const BLT_S: Opcode = Opcode::new( + "blt.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x32, + CondBranch, +); +pub const BNE_UN_S: Opcode = Opcode::new( + "bne.un.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x33, + CondBranch, +); +pub const BGE_UN_S: Opcode = Opcode::new( + "bge.un.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x34, + CondBranch, +); +pub const BGT_UN_S: Opcode = Opcode::new( + "bgt.un.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x35, + CondBranch, +); +pub const BLE_UN_S: Opcode = Opcode::new( + "ble.un.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x36, + CondBranch, +); +pub const BLT_UN_S: Opcode = Opcode::new( + "blt.un.s", + Pop1Pop1, + Push0, + ShortInlineBrTarget, + Macro, + 1, + 0xFF, + 0x37, + CondBranch, +); +pub const BR: Opcode = Opcode::new( + "br", + Pop0, + Push0, + InlineBrTarget, + Primitive, + 1, + 0xFF, + 0x38, + Branch, +); +pub const BRFALSE: Opcode = Opcode::new( + "brfalse", + PopI, + Push0, + InlineBrTarget, + Primitive, + 1, + 0xFF, + 0x39, + CondBranch, +); +pub const BRTRUE: Opcode = Opcode::new( + "brtrue", + PopI, + Push0, + InlineBrTarget, + Primitive, + 1, + 0xFF, + 0x3A, + CondBranch, +); +pub const BEQ: Opcode = Opcode::new( + "beq", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x3B, + CondBranch, +); +pub const BGE: Opcode = Opcode::new( + "bge", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x3C, + CondBranch, +); +pub const BGT: Opcode = Opcode::new( + "bgt", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x3D, + CondBranch, +); +pub const BLE: Opcode = Opcode::new( + "ble", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x3E, + CondBranch, +); +pub const BLT: Opcode = Opcode::new( + "blt", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x3F, + CondBranch, +); +pub const BNE_UN: Opcode = Opcode::new( + "bne.un", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x40, + CondBranch, +); +pub const BGE_UN: Opcode = Opcode::new( + "bge.un", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x41, + CondBranch, +); +pub const BGT_UN: Opcode = Opcode::new( + "bgt.un", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x42, + CondBranch, +); +pub const BLE_UN: Opcode = Opcode::new( + "ble.un", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x43, + CondBranch, +); +pub const BLT_UN: Opcode = Opcode::new( + "blt.un", + Pop1Pop1, + Push0, + InlineBrTarget, + Macro, + 1, + 0xFF, + 0x44, + CondBranch, +); +pub const SWITCH: Opcode = Opcode::new( + "switch", + PopI, + Push0, + InlineSwitch, + Primitive, + 1, + 0xFF, + 0x45, + CondBranch, +); +pub const LDIND_I1: Opcode = Opcode::new( + "ldind.i1", PopI, PushI, InlineNone, Primitive, 1, 0xFF, 0x46, Next, +); +pub const LDIND_U1: Opcode = Opcode::new( + "ldind.u1", PopI, PushI, InlineNone, Primitive, 1, 0xFF, 0x47, Next, +); +pub const LDIND_I2: Opcode = Opcode::new( + "ldind.i2", PopI, PushI, InlineNone, Primitive, 1, 0xFF, 0x48, Next, +); +pub const LDIND_U2: Opcode = Opcode::new( + "ldind.u2", PopI, PushI, InlineNone, Primitive, 1, 0xFF, 0x49, Next, +); +pub const LDIND_I4: Opcode = Opcode::new( + "ldind.i4", PopI, PushI, InlineNone, Primitive, 1, 0xFF, 0x4A, Next, +); +pub const LDIND_U4: Opcode = Opcode::new( + "ldind.u4", PopI, PushI, InlineNone, Primitive, 1, 0xFF, 0x4B, Next, +); +pub const LDIND_I8: Opcode = Opcode::new( + "ldind.i8", PopI, PushI8, InlineNone, Primitive, 1, 0xFF, 0x4C, Next, +); +pub const LDIND_I: Opcode = Opcode::new( + "ldind.i", PopI, PushI, InlineNone, Primitive, 1, 0xFF, 0x4D, Next, +); +pub const LDIND_R4: Opcode = Opcode::new( + "ldind.r4", PopI, PushR4, InlineNone, Primitive, 1, 0xFF, 0x4E, Next, +); +pub const LDIND_R8: Opcode = Opcode::new( + "ldind.r8", PopI, PushR8, InlineNone, Primitive, 1, 0xFF, 0x4F, Next, +); +pub const LDIND_REF: Opcode = Opcode::new( + "ldind.ref", + PopI, + PushRef, + InlineNone, + Primitive, + 1, + 0xFF, + 0x50, + Next, +); +pub const STIND_REF: Opcode = Opcode::new( + "stind.ref", + PopIPopI, + Push0, + InlineNone, + Primitive, + 1, + 0xFF, + 0x51, + Next, +); +pub const STIND_I1: Opcode = Opcode::new( + "stind.i1", PopIPopI, Push0, InlineNone, Primitive, 1, 0xFF, 0x52, Next, +); +pub const STIND_I2: Opcode = Opcode::new( + "stind.i2", PopIPopI, Push0, InlineNone, Primitive, 1, 0xFF, 0x53, Next, +); +pub const STIND_I4: Opcode = Opcode::new( + "stind.i4", PopIPopI, Push0, InlineNone, Primitive, 1, 0xFF, 0x54, Next, +); +pub const STIND_I8: Opcode = Opcode::new( + "stind.i8", PopIPopI8, Push0, InlineNone, Primitive, 1, 0xFF, 0x55, Next, +); +pub const STIND_R4: Opcode = Opcode::new( + "stind.r4", PopIPopR4, Push0, InlineNone, Primitive, 1, 0xFF, 0x56, Next, +); +pub const STIND_R8: Opcode = Opcode::new( + "stind.r8", PopIPopR8, Push0, InlineNone, Primitive, 1, 0xFF, 0x57, Next, +); +pub const ADD: Opcode = Opcode::new( + "add", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x58, Next, +); +pub const SUB: Opcode = Opcode::new( + "sub", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x59, Next, +); +pub const MUL: Opcode = Opcode::new( + "mul", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x5A, Next, +); +pub const DIV: Opcode = Opcode::new( + "div", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x5B, Next, +); +pub const DIV_UN: Opcode = Opcode::new( + "div.un", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x5C, Next, +); +pub const REM: Opcode = Opcode::new( + "rem", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x5D, Next, +); +pub const REM_UN: Opcode = Opcode::new( + "rem.un", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x5E, Next, +); +pub const AND: Opcode = Opcode::new( + "and", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x5F, Next, +); +pub const OR: Opcode = Opcode::new( + "or", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x60, Next, +); +pub const XOR: Opcode = Opcode::new( + "xor", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x61, Next, +); +pub const SHL: Opcode = Opcode::new( + "shl", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x62, Next, +); +pub const SHR: Opcode = Opcode::new( + "shr", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x63, Next, +); +pub const SHR_UN: Opcode = Opcode::new( + "shr.un", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x64, Next, +); +pub const NEG: Opcode = Opcode::new( + "neg", Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x65, Next, +); +pub const NOT: Opcode = Opcode::new( + "not", Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0x66, Next, +); +pub const CONV_I1: Opcode = Opcode::new( + "conv.i1", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0x67, Next, +); +pub const CONV_I2: Opcode = Opcode::new( + "conv.i2", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0x68, Next, +); +pub const CONV_I4: Opcode = Opcode::new( + "conv.i4", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0x69, Next, +); +pub const CONV_I8: Opcode = Opcode::new( + "conv.i8", Pop1, PushI8, InlineNone, Primitive, 1, 0xFF, 0x6A, Next, +); +pub const CONV_R4: Opcode = Opcode::new( + "conv.r4", Pop1, PushR4, InlineNone, Primitive, 1, 0xFF, 0x6B, Next, +); +pub const CONV_R8: Opcode = Opcode::new( + "conv.r8", Pop1, PushR8, InlineNone, Primitive, 1, 0xFF, 0x6C, Next, +); +pub const CONV_U4: Opcode = Opcode::new( + "conv.u4", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0x6D, Next, +); +pub const CONV_U8: Opcode = Opcode::new( + "conv.u8", Pop1, PushI8, InlineNone, Primitive, 1, 0xFF, 0x6E, Next, +); +pub const CALLVIRT: Opcode = Opcode::new( + "callvirt", + VarPop, + VarPush, + InlineMethod, + ObjModel, + 1, + 0xFF, + 0x6F, + Call, +); +pub const CPOBJ: Opcode = Opcode::new( + "cpobj", PopIPopI, Push0, InlineType, ObjModel, 1, 0xFF, 0x70, Next, +); +pub const LDOBJ: Opcode = Opcode::new( + "ldobj", PopI, Push1, InlineType, ObjModel, 1, 0xFF, 0x71, Next, +); +pub const LDSTR: Opcode = Opcode::new( + "ldstr", + Pop0, + PushRef, + InlineString, + ObjModel, + 1, + 0xFF, + 0x72, + Next, +); +pub const NEWOBJ: Opcode = Opcode::new( + "newobj", + VarPop, + PushRef, + InlineMethod, + ObjModel, + 1, + 0xFF, + 0x73, + Call, +); +pub const CASTCLASS: Opcode = Opcode::new( + "castclass", + PopRef, + PushRef, + InlineType, + ObjModel, + 1, + 0xFF, + 0x74, + Next, +); +pub const ISINST: Opcode = Opcode::new( + "isinst", PopRef, PushI, InlineType, ObjModel, 1, 0xFF, 0x75, Next, +); +pub const CONV_R_UN: Opcode = Opcode::new( + "conv.r.un", + Pop1, + PushR8, + InlineNone, + Primitive, + 1, + 0xFF, + 0x76, + Next, +); +pub const UNUSED58: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0x77, Next, +); +pub const UNUSED1: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0x78, Next, +); +pub const UNBOX: Opcode = Opcode::new( + "unbox", PopRef, PushI, InlineType, Primitive, 1, 0xFF, 0x79, Next, +); +pub const THROW: Opcode = Opcode::new( + "throw", PopRef, Push0, InlineNone, ObjModel, 1, 0xFF, 0x7A, Throw, +); +pub const LDFLD: Opcode = Opcode::new( + "ldfld", + PopRef, + Push1, + InlineField, + ObjModel, + 1, + 0xFF, + 0x7B, + Next, +); +pub const LDFLDA: Opcode = Opcode::new( + "ldflda", + PopRef, + PushI, + InlineField, + ObjModel, + 1, + 0xFF, + 0x7C, + Next, +); +pub const STFLD: Opcode = Opcode::new( + "stfld", + PopRefPop1, + Push0, + InlineField, + ObjModel, + 1, + 0xFF, + 0x7D, + Next, +); +pub const LDSFLD: Opcode = Opcode::new( + "ldsfld", + Pop0, + Push1, + InlineField, + ObjModel, + 1, + 0xFF, + 0x7E, + Next, +); +pub const LDSFLDA: Opcode = Opcode::new( + "ldsflda", + Pop0, + PushI, + InlineField, + ObjModel, + 1, + 0xFF, + 0x7F, + Next, +); +pub const STSFLD: Opcode = Opcode::new( + "stsfld", + Pop1, + Push0, + InlineField, + ObjModel, + 1, + 0xFF, + 0x80, + Next, +); +pub const STOBJ: Opcode = Opcode::new( + "stobj", PopIPop1, Push0, InlineType, Primitive, 1, 0xFF, 0x81, Next, +); +pub const CONV_OVF_I1_UN: Opcode = Opcode::new( + "conv.ovf.i1.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x82, + Next, +); +pub const CONV_OVF_I2_UN: Opcode = Opcode::new( + "conv.ovf.i2.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x83, + Next, +); +pub const CONV_OVF_I4_UN: Opcode = Opcode::new( + "conv.ovf.i4.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x84, + Next, +); +pub const CONV_OVF_I8_UN: Opcode = Opcode::new( + "conv.ovf.i8.un", + Pop1, + PushI8, + InlineNone, + Primitive, + 1, + 0xFF, + 0x85, + Next, +); +pub const CONV_OVF_U1_UN: Opcode = Opcode::new( + "conv.ovf.u1.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x86, + Next, +); +pub const CONV_OVF_U2_UN: Opcode = Opcode::new( + "conv.ovf.u2.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x87, + Next, +); +pub const CONV_OVF_U4_UN: Opcode = Opcode::new( + "conv.ovf.u4.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x88, + Next, +); +pub const CONV_OVF_U8_UN: Opcode = Opcode::new( + "conv.ovf.u8.un", + Pop1, + PushI8, + InlineNone, + Primitive, + 1, + 0xFF, + 0x89, + Next, +); +pub const CONV_OVF_I_UN: Opcode = Opcode::new( + "conv.ovf.i.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x8A, + Next, +); +pub const CONV_OVF_U_UN: Opcode = Opcode::new( + "conv.ovf.u.un", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0x8B, + Next, +); +pub const BOX: Opcode = Opcode::new( + "box", Pop1, PushRef, InlineType, Primitive, 1, 0xFF, 0x8C, Next, +); +pub const NEWARR: Opcode = Opcode::new( + "newarr", PopI, PushRef, InlineType, ObjModel, 1, 0xFF, 0x8D, Next, +); +pub const LDLEN: Opcode = Opcode::new( + "ldlen", PopRef, PushI, InlineNone, ObjModel, 1, 0xFF, 0x8E, Next, +); +pub const LDELEMA: Opcode = Opcode::new( + "ldelema", PopRefPopI, PushI, InlineType, ObjModel, 1, 0xFF, 0x8F, Next, +); +pub const LDELEM_I1: Opcode = Opcode::new( + "ldelem.i1", + PopRefPopI, + PushI, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x90, + Next, +); +pub const LDELEM_U1: Opcode = Opcode::new( + "ldelem.u1", + PopRefPopI, + PushI, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x91, + Next, +); +pub const LDELEM_I2: Opcode = Opcode::new( + "ldelem.i2", + PopRefPopI, + PushI, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x92, + Next, +); +pub const LDELEM_U2: Opcode = Opcode::new( + "ldelem.u2", + PopRefPopI, + PushI, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x93, + Next, +); +pub const LDELEM_I4: Opcode = Opcode::new( + "ldelem.i4", + PopRefPopI, + PushI, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x94, + Next, +); +pub const LDELEM_U4: Opcode = Opcode::new( + "ldelem.u4", + PopRefPopI, + PushI, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x95, + Next, +); +pub const LDELEM_I8: Opcode = Opcode::new( + "ldelem.i8", + PopRefPopI, + PushI8, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x96, + Next, +); +pub const LDELEM_I: Opcode = Opcode::new( + "ldelem.i", PopRefPopI, PushI, InlineNone, ObjModel, 1, 0xFF, 0x97, Next, +); +pub const LDELEM_R4: Opcode = Opcode::new( + "ldelem.r4", + PopRefPopI, + PushR4, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x98, + Next, +); +pub const LDELEM_R8: Opcode = Opcode::new( + "ldelem.r8", + PopRefPopI, + PushR8, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x99, + Next, +); +pub const LDELEM_REF: Opcode = Opcode::new( + "ldelem.ref", + PopRefPopI, + PushRef, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x9A, + Next, +); +pub const STELEM_I: Opcode = Opcode::new( + "stelem.i", + PopRefPopIPopI, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x9B, + Next, +); +pub const STELEM_I1: Opcode = Opcode::new( + "stelem.i1", + PopRefPopIPopI, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x9C, + Next, +); +pub const STELEM_I2: Opcode = Opcode::new( + "stelem.i2", + PopRefPopIPopI, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x9D, + Next, +); +pub const STELEM_I4: Opcode = Opcode::new( + "stelem.i4", + PopRefPopIPopI, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x9E, + Next, +); +pub const STELEM_I8: Opcode = Opcode::new( + "stelem.i8", + PopRefPopIPopI8, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0x9F, + Next, +); +pub const STELEM_R4: Opcode = Opcode::new( + "stelem.r4", + PopRefPopIPopR4, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0xA0, + Next, +); +pub const STELEM_R8: Opcode = Opcode::new( + "stelem.r8", + PopRefPopIPopR8, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0xA1, + Next, +); +pub const STELEM_REF: Opcode = Opcode::new( + "stelem.ref", + PopRefPopIPopRef, + Push0, + InlineNone, + ObjModel, + 1, + 0xFF, + 0xA2, + Next, +); +pub const LDELEM: Opcode = Opcode::new( + "ldelem", PopRefPopI, Push1, InlineType, ObjModel, 1, 0xFF, 0xA3, Next, +); +pub const STELEM: Opcode = Opcode::new( + "stelem", + PopRefPopIPop1, + Push0, + InlineType, + ObjModel, + 1, + 0xFF, + 0xA4, + Next, +); +pub const UNBOX_ANY: Opcode = Opcode::new( + "unbox.any", + PopRef, + Push1, + InlineType, + ObjModel, + 1, + 0xFF, + 0xA5, + Next, +); +pub const UNUSED5: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xA6, Next, +); +pub const UNUSED6: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xA7, Next, +); +pub const UNUSED7: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xA8, Next, +); +pub const UNUSED8: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xA9, Next, +); +pub const UNUSED9: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xAA, Next, +); +pub const UNUSED10: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xAB, Next, +); +pub const UNUSED11: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xAC, Next, +); +pub const UNUSED12: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xAD, Next, +); +pub const UNUSED13: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xAE, Next, +); +pub const UNUSED14: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xAF, Next, +); +pub const UNUSED15: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xB0, Next, +); +pub const UNUSED16: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xB1, Next, +); +pub const UNUSED17: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xB2, Next, +); +pub const CONV_OVF_I1: Opcode = Opcode::new( + "conv.ovf.i1", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xB3, + Next, +); +pub const CONV_OVF_U1: Opcode = Opcode::new( + "conv.ovf.u1", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xB4, + Next, +); +pub const CONV_OVF_I2: Opcode = Opcode::new( + "conv.ovf.i2", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xB5, + Next, +); +pub const CONV_OVF_U2: Opcode = Opcode::new( + "conv.ovf.u2", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xB6, + Next, +); +pub const CONV_OVF_I4: Opcode = Opcode::new( + "conv.ovf.i4", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xB7, + Next, +); +pub const CONV_OVF_U4: Opcode = Opcode::new( + "conv.ovf.u4", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xB8, + Next, +); +pub const CONV_OVF_I8: Opcode = Opcode::new( + "conv.ovf.i8", + Pop1, + PushI8, + InlineNone, + Primitive, + 1, + 0xFF, + 0xB9, + Next, +); +pub const CONV_OVF_U8: Opcode = Opcode::new( + "conv.ovf.u8", + Pop1, + PushI8, + InlineNone, + Primitive, + 1, + 0xFF, + 0xBA, + Next, +); +pub const UNUSED50: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xBB, Next, +); +pub const UNUSED18: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xBC, Next, +); +pub const UNUSED19: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xBD, Next, +); +pub const UNUSED20: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xBE, Next, +); +pub const UNUSED21: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xBF, Next, +); +pub const UNUSED22: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xC0, Next, +); +pub const UNUSED23: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xC1, Next, +); +pub const REFANYVAL: Opcode = Opcode::new( + "refanyval", + Pop1, + PushI, + InlineType, + Primitive, + 1, + 0xFF, + 0xC2, + Next, +); +pub const CKFINITE: Opcode = Opcode::new( + "ckfinite", Pop1, PushR8, InlineNone, Primitive, 1, 0xFF, 0xC3, Next, +); +pub const UNUSED24: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xC4, Next, +); +pub const UNUSED25: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xC5, Next, +); +pub const MKREFANY: Opcode = Opcode::new( + "mkrefany", PopI, Push1, InlineType, Primitive, 1, 0xFF, 0xC6, Next, +); +pub const UNUSED59: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xC7, Next, +); +pub const UNUSED60: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xC8, Next, +); +pub const UNUSED61: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xC9, Next, +); +pub const UNUSED62: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xCA, Next, +); +pub const UNUSED63: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xCB, Next, +); +pub const UNUSED64: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xCC, Next, +); +pub const UNUSED65: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xCD, Next, +); +pub const UNUSED66: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xCE, Next, +); +pub const UNUSED67: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xCF, Next, +); +pub const LDTOKEN: Opcode = Opcode::new( + "ldtoken", Pop0, PushI, InlineTok, Primitive, 1, 0xFF, 0xD0, Next, +); +pub const CONV_U2: Opcode = Opcode::new( + "conv.u2", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0xD1, Next, +); +pub const CONV_U1: Opcode = Opcode::new( + "conv.u1", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0xD2, Next, +); +pub const CONV_I: Opcode = Opcode::new( + "conv.i", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0xD3, Next, +); +pub const CONV_OVF_I: Opcode = Opcode::new( + "conv.ovf.i", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xD4, + Next, +); +pub const CONV_OVF_U: Opcode = Opcode::new( + "conv.ovf.u", + Pop1, + PushI, + InlineNone, + Primitive, + 1, + 0xFF, + 0xD5, + Next, +); +pub const ADD_OVF: Opcode = Opcode::new( + "add.ovf", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0xD6, Next, +); +pub const ADD_OVF_UN: Opcode = Opcode::new( + "add.ovf.un", + Pop1Pop1, + Push1, + InlineNone, + Primitive, + 1, + 0xFF, + 0xD7, + Next, +); +pub const MUL_OVF: Opcode = Opcode::new( + "mul.ovf", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0xD8, Next, +); +pub const MUL_OVF_UN: Opcode = Opcode::new( + "mul.ovf.un", + Pop1Pop1, + Push1, + InlineNone, + Primitive, + 1, + 0xFF, + 0xD9, + Next, +); +pub const SUB_OVF: Opcode = Opcode::new( + "sub.ovf", Pop1Pop1, Push1, InlineNone, Primitive, 1, 0xFF, 0xDA, Next, +); +pub const SUB_OVF_UN: Opcode = Opcode::new( + "sub.ovf.un", + Pop1Pop1, + Push1, + InlineNone, + Primitive, + 1, + 0xFF, + 0xDB, + Next, +); +pub const ENDFINALLY: Opcode = Opcode::new( + "endfinally", + Pop0, + Push0, + InlineNone, + Primitive, + 1, + 0xFF, + 0xDC, + Return, +); +pub const LEAVE: Opcode = Opcode::new( + "leave", + Pop0, + Push0, + InlineBrTarget, + Primitive, + 1, + 0xFF, + 0xDD, + Branch, +); +pub const LEAVE_S: Opcode = Opcode::new( + "leave.s", + Pop0, + Push0, + ShortInlineBrTarget, + Primitive, + 1, + 0xFF, + 0xDE, + Branch, +); +pub const STIND_I: Opcode = Opcode::new( + "stind.i", PopIPopI, Push0, InlineNone, Primitive, 1, 0xFF, 0xDF, Next, +); +pub const CONV_U: Opcode = Opcode::new( + "conv.u", Pop1, PushI, InlineNone, Primitive, 1, 0xFF, 0xE0, Next, +); +pub const UNUSED26: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE1, Next, +); +pub const UNUSED27: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE2, Next, +); +pub const UNUSED28: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE3, Next, +); +pub const UNUSED29: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE4, Next, +); +pub const UNUSED30: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE5, Next, +); +pub const UNUSED31: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE6, Next, +); +pub const UNUSED32: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE7, Next, +); +pub const UNUSED33: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE8, Next, +); +pub const UNUSED34: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xE9, Next, +); +pub const UNUSED35: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xEA, Next, +); +pub const UNUSED36: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xEB, Next, +); +pub const UNUSED37: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xEC, Next, +); +pub const UNUSED38: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xED, Next, +); +pub const UNUSED39: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xEE, Next, +); +pub const UNUSED40: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xEF, Next, +); +pub const UNUSED41: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF0, Next, +); +pub const UNUSED42: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF1, Next, +); +pub const UNUSED43: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF2, Next, +); +pub const UNUSED44: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF3, Next, +); +pub const UNUSED45: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF4, Next, +); +pub const UNUSED46: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF5, Next, +); +pub const UNUSED47: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF6, Next, +); +pub const UNUSED48: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 1, 0xFF, 0xF7, Next, +); +pub const PREFIX7: Opcode = Opcode::new( + "prefix7", Pop0, Push0, InlineNone, Internal, 1, 0xFF, 0xF8, Meta, +); +pub const PREFIX6: Opcode = Opcode::new( + "prefix6", Pop0, Push0, InlineNone, Internal, 1, 0xFF, 0xF9, Meta, +); +pub const PREFIX5: Opcode = Opcode::new( + "prefix5", Pop0, Push0, InlineNone, Internal, 1, 0xFF, 0xFA, Meta, +); +pub const PREFIX4: Opcode = Opcode::new( + "prefix4", Pop0, Push0, InlineNone, Internal, 1, 0xFF, 0xFB, Meta, +); +pub const PREFIX3: Opcode = Opcode::new( + "prefix3", Pop0, Push0, InlineNone, Internal, 1, 0xFF, 0xFC, Meta, +); +pub const PREFIX2: Opcode = Opcode::new( + "prefix2", Pop0, Push0, InlineNone, Internal, 1, 0xFF, 0xFD, Meta, +); +pub const PREFIX1: Opcode = Opcode::new( + "prefix1", Pop0, Push0, InlineNone, Internal, 1, 0xFF, 0xFE, Meta, +); +pub const PREFIXREF: Opcode = Opcode::new( + "prefixref", + Pop0, + Push0, + InlineNone, + Internal, + 1, + 0xFF, + 0xFF, + Meta, +); +pub const ARGLIST: Opcode = Opcode::new( + "arglist", Pop0, PushI, InlineNone, Primitive, 2, 0xFE, 0x00, Next, +); +pub const CEQ: Opcode = Opcode::new( + "ceq", Pop1Pop1, PushI, InlineNone, Primitive, 2, 0xFE, 0x01, Next, +); +pub const CGT: Opcode = Opcode::new( + "cgt", Pop1Pop1, PushI, InlineNone, Primitive, 2, 0xFE, 0x02, Next, +); +pub const CGT_UN: Opcode = Opcode::new( + "cgt.un", Pop1Pop1, PushI, InlineNone, Primitive, 2, 0xFE, 0x03, Next, +); +pub const CLT: Opcode = Opcode::new( + "clt", Pop1Pop1, PushI, InlineNone, Primitive, 2, 0xFE, 0x04, Next, +); +pub const CLT_UN: Opcode = Opcode::new( + "clt.un", Pop1Pop1, PushI, InlineNone, Primitive, 2, 0xFE, 0x05, Next, +); +pub const LDFTN: Opcode = Opcode::new( + "ldftn", + Pop0, + PushI, + InlineMethod, + Primitive, + 2, + 0xFE, + 0x06, + Next, +); +pub const LDVIRTFTN: Opcode = Opcode::new( + "ldvirtftn", + PopRef, + PushI, + InlineMethod, + Primitive, + 2, + 0xFE, + 0x07, + Next, +); +pub const UNUSED56: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x08, Next, +); +pub const LDARG: Opcode = Opcode::new( + "ldarg", Pop0, Push1, InlineVar, Primitive, 2, 0xFE, 0x09, Next, +); +pub const LDARGA: Opcode = Opcode::new( + "ldarga", Pop0, PushI, InlineVar, Primitive, 2, 0xFE, 0x0A, Next, +); +pub const STARG: Opcode = Opcode::new( + "starg", Pop1, Push0, InlineVar, Primitive, 2, 0xFE, 0x0B, Next, +); +pub const LDLOC: Opcode = Opcode::new( + "ldloc", Pop0, Push1, InlineVar, Primitive, 2, 0xFE, 0x0C, Next, +); +pub const LDLOCA: Opcode = Opcode::new( + "ldloca", Pop0, PushI, InlineVar, Primitive, 2, 0xFE, 0x0D, Next, +); +pub const STLOC: Opcode = Opcode::new( + "stloc", Pop1, Push0, InlineVar, Primitive, 2, 0xFE, 0x0E, Next, +); +pub const LOCALLOC: Opcode = Opcode::new( + "localloc", PopI, PushI, InlineNone, Primitive, 2, 0xFE, 0x0F, Next, +); +pub const UNUSED57: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x10, Next, +); +pub const ENDFILTER: Opcode = Opcode::new( + "endfilter", + PopI, + Push0, + InlineNone, + Primitive, + 2, + 0xFE, + 0x11, + Return, +); +pub const UNALIGNED: Opcode = Opcode::new( + "unaligned.", + Pop0, + Push0, + ShortInlineI, + Prefix, + 2, + 0xFE, + 0x12, + Meta, +); +pub const VOLATILE: Opcode = Opcode::new( + "volatile.", + Pop0, + Push0, + InlineNone, + Prefix, + 2, + 0xFE, + 0x13, + Meta, +); +pub const TAILCALL: Opcode = Opcode::new( + "tail.", Pop0, Push0, InlineNone, Prefix, 2, 0xFE, 0x14, Meta, +); +pub const INITOBJ: Opcode = Opcode::new( + "initobj", PopI, Push0, InlineType, ObjModel, 2, 0xFE, 0x15, Next, +); +pub const CONSTRAINED: Opcode = Opcode::new( + "constrained.", + Pop0, + Push0, + InlineType, + Prefix, + 2, + 0xFE, + 0x16, + Meta, +); +pub const CPBLK: Opcode = Opcode::new( + "cpblk", + PopIPopIPopI, + Push0, + InlineNone, + Primitive, + 2, + 0xFE, + 0x17, + Next, +); +pub const INITBLK: Opcode = Opcode::new( + "initblk", + PopIPopIPopI, + Push0, + InlineNone, + Primitive, + 2, + 0xFE, + 0x18, + Next, +); +pub const UNUSED69: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x19, Next, +); +pub const RETHROW: Opcode = Opcode::new( + "rethrow", Pop0, Push0, InlineNone, ObjModel, 2, 0xFE, 0x1A, Throw, +); +pub const UNUSED51: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x1B, Next, +); +pub const SIZEOF: Opcode = Opcode::new( + "sizeof", Pop0, PushI, InlineType, Primitive, 2, 0xFE, 0x1C, Next, +); +pub const REFANYTYPE: Opcode = Opcode::new( + "refanytype", + Pop1, + PushI, + InlineNone, + Primitive, + 2, + 0xFE, + 0x1D, + Next, +); +pub const READONLY: Opcode = Opcode::new( + "readonly.", + Pop0, + Push0, + InlineNone, + Prefix, + 2, + 0xFE, + 0x1E, + Meta, +); +pub const UNUSED53: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x1F, Next, +); +pub const UNUSED54: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x20, Next, +); +pub const UNUSED55: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x21, Next, +); +pub const UNUSED70: Opcode = Opcode::new( + "unused", Pop0, Push0, InlineNone, Primitive, 2, 0xFE, 0x22, Next, +); diff --git a/src/elastic_apm_profiler/src/cil/section.rs b/src/elastic_apm_profiler/src/cil/section.rs new file mode 100644 index 000000000..dd6d9c760 --- /dev/null +++ b/src/elastic_apm_profiler/src/cil/section.rs @@ -0,0 +1,285 @@ +#![allow(non_upper_case_globals)] + +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Copyright 2019 Camden Reslink +// MIT License +// https://github.com/camdenreslink/clr-profiler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +use crate::{ + cil::{check_flag, il_u16, il_u32, il_u8}, + error::Error, +}; + +bitflags! { + pub struct SectionHeaderFlags: u8 { + const CorILMethod_Sect_EHTable = 0x1; + const CorILMethod_Sect_OptILTable = 0x2; + const CorILMethod_Sect_FatFormat = 0x40; + const CorILMethod_Sect_MoreSects = 0x80; + } +} +bitflags! { + pub struct CorExceptionFlag: u8 { + const COR_ILEXCEPTION_CLAUSE_NONE = 0x0; + const COR_ILEXCEPTION_CLAUSE_FILTER = 0x1; + const COR_ILEXCEPTION_CLAUSE_FINALLY = 0x2; + const COR_ILEXCEPTION_CLAUSE_FAULT = 0x4; + const COR_ILEXCEPTION_CLAUSE_DUPLICATED = 0x8; + } +} +#[derive(Debug)] +pub struct FatSectionHeader { + pub is_eh_table: bool, + pub more_sects: bool, + /// Note that this really should be u24, but no such type exists. + /// Must take care when converting back to CIL bytes. + pub data_size: u32, +} + +impl FatSectionHeader { + pub const LENGTH: usize = 4; + pub fn into_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(4); + let mut flags = SectionHeaderFlags::CorILMethod_Sect_FatFormat.bits(); + if self.is_eh_table { + flags |= SectionHeaderFlags::CorILMethod_Sect_EHTable.bits(); + } + if self.more_sects { + flags |= SectionHeaderFlags::CorILMethod_Sect_MoreSects.bits(); + } + bytes.push(flags); + bytes.extend_from_slice(&self.data_size.to_le_bytes()[0..3]); + bytes + } +} + +#[derive(Debug)] +pub struct FatSectionClause { + pub flag: CorExceptionFlag, + pub try_offset: u32, + pub try_length: u32, + pub handler_offset: u32, + pub handler_length: u32, + pub class_token_or_filter_offset: u32, +} +impl FatSectionClause { + pub(crate) const LENGTH: usize = 24; + pub fn from_bytes(il: &[u8]) -> Result { + let flag = CorExceptionFlag::from_bits(il[0]).unwrap(); + let try_offset = il_u32(il, 4)?; + let try_length = il_u32(il, 8)?; + let handler_offset = il_u32(il, 12)?; + let handler_length = il_u32(il, 16)?; + let class_token_or_filter_offset = il_u32(il, 20)?; + Ok(FatSectionClause { + flag, + try_offset, + try_length, + handler_offset, + handler_length, + class_token_or_filter_offset, + }) + } + + pub fn into_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(Self::LENGTH); + let flags = self.flag.bits() as u32; + bytes.extend_from_slice(&flags.to_le_bytes()); + bytes.extend_from_slice(&self.try_offset.to_le_bytes()); + bytes.extend_from_slice(&self.try_length.to_le_bytes()); + bytes.extend_from_slice(&self.handler_offset.to_le_bytes()); + bytes.extend_from_slice(&self.handler_length.to_le_bytes()); + bytes.extend_from_slice(&self.class_token_or_filter_offset.to_le_bytes()); + bytes + } +} +#[derive(Debug)] +pub struct SmallSectionHeader { + pub is_eh_table: bool, + pub more_sects: bool, + pub data_size: u8, +} + +impl SmallSectionHeader { + pub fn into_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(4); + let mut flags = 0u8; + if self.is_eh_table { + flags |= SectionHeaderFlags::CorILMethod_Sect_EHTable.bits(); + } + if self.more_sects { + flags |= SectionHeaderFlags::CorILMethod_Sect_MoreSects.bits(); + } + bytes.push(flags); + bytes.push(self.data_size); + bytes.push(0u8); // Padding for DWORD alignment + bytes.push(0u8); // Padding for DWORD alignment + bytes + } +} + +#[derive(Debug)] +pub struct SmallSectionClause { + pub flag: CorExceptionFlag, + pub try_offset: u16, + pub try_length: u8, + pub handler_offset: u16, + pub handler_length: u8, + pub class_token_or_filter_offset: u32, +} +impl SmallSectionClause { + const LENGTH: usize = 12; + pub fn from_bytes(il: &[u8]) -> Result { + let flag = CorExceptionFlag::from_bits(il[0]).unwrap(); + let try_offset = il_u16(il, 2)?; + let try_length = il_u8(il, 4)?; + let handler_offset = il_u16(il, 5)?; + let handler_length = il_u8(il, 7)?; + let class_token_or_filter_offset = il_u32(il, 8)?; + Ok(SmallSectionClause { + flag, + try_offset, + try_length, + handler_offset, + handler_length, + class_token_or_filter_offset, + }) + } + + pub fn into_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(Self::LENGTH); + let flags = self.flag.bits() as u16; + bytes.extend_from_slice(&flags.to_le_bytes()); + bytes.extend_from_slice(&self.try_offset.to_le_bytes()); + bytes.push(self.try_length); + bytes.extend_from_slice(&self.handler_offset.to_le_bytes()); + bytes.push(self.handler_length); + bytes.extend_from_slice(&self.class_token_or_filter_offset.to_le_bytes()); + bytes + } +} +#[derive(Debug)] +pub enum Section { + FatSection(FatSectionHeader, Vec), + SmallSection(SmallSectionHeader, Vec), +} +impl Section { + pub fn from_bytes(il: &[u8]) -> Result { + let header_flags = il[0]; + let is_eh_table = Self::is_eh_table(header_flags); + let more_sects = Self::more_sects(header_flags); + if Self::is_small(header_flags) { + let data_size = il_u8(il, 1)?; + let small_header = SmallSectionHeader { + is_eh_table, + more_sects, + data_size, + }; + let clause_bytes = &il[4..(data_size as usize)]; + let clauses = Self::get_small_clauses(clause_bytes)?; + Ok(Section::SmallSection(small_header, clauses)) + } else if Self::is_fat(header_flags) { + let byte_1 = il_u8(il, 1)?; + let byte_2 = il_u8(il, 2)?; + let byte_3 = il_u8(il, 3)?; + let data_size = u32::from_le_bytes([byte_1, byte_2, byte_3, 0]); + let fat_header = FatSectionHeader { + is_eh_table, + more_sects, + data_size, + }; + let clause_bytes = &il[4..(data_size as usize)]; + let clauses = Self::get_fat_clauses(clause_bytes)?; + Ok(Section::FatSection(fat_header, clauses)) + } else { + Err(Error::InvalidSectionHeader) + } + } + + pub fn into_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + match &self { + Section::FatSection(header, clauses) => { + bytes.append(&mut header.into_bytes()); + for clause in clauses.iter() { + bytes.append(&mut clause.into_bytes()); + } + } + Section::SmallSection(header, clauses) => { + bytes.append(&mut header.into_bytes()); + for clause in clauses.iter() { + bytes.append(&mut clause.into_bytes()); + } + } + } + bytes + } + pub fn data_size(&self) -> usize { + match self { + Self::FatSection(header, _) => header.data_size as usize, + Self::SmallSection(header, _) => header.data_size as usize, + } + } + fn is_small(section_header_flags: u8) -> bool { + !Self::is_fat(section_header_flags) + } + fn is_fat(section_header_flags: u8) -> bool { + check_flag( + section_header_flags, + SectionHeaderFlags::CorILMethod_Sect_FatFormat.bits(), + ) + } + fn is_eh_table(section_header_flags: u8) -> bool { + check_flag( + section_header_flags, + SectionHeaderFlags::CorILMethod_Sect_EHTable.bits(), + ) + } + fn more_sects(section_header_flags: u8) -> bool { + check_flag( + section_header_flags, + SectionHeaderFlags::CorILMethod_Sect_MoreSects.bits(), + ) + } + fn get_fat_clauses(il: &[u8]) -> Result, Error> { + let mut index = 0; + let mut clauses = Vec::new(); + while index < il.len() { + let il = &il[index..]; + let clause = FatSectionClause::from_bytes(il)?; + index += FatSectionClause::LENGTH; + clauses.push(clause); + } + Ok(clauses) + } + fn get_small_clauses(il: &[u8]) -> Result, Error> { + let mut index = 0; + let mut clauses = Vec::new(); + while index < il.len() { + let il = &il[index..]; + let clause = SmallSectionClause::from_bytes(il)?; + index += SmallSectionClause::LENGTH; + clauses.push(clause); + } + Ok(clauses) + } +} diff --git a/src/elastic_apm_profiler/src/error.rs b/src/elastic_apm_profiler/src/error.rs new file mode 100644 index 000000000..06fe2156a --- /dev/null +++ b/src/elastic_apm_profiler/src/error.rs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + InvalidMethodHeader, + InvalidSectionHeader, + InvalidCil, + InvalidCilOpcode, + CodeSize, + StackSize, + InvalidVersion, + InvalidAssemblyReference, +} diff --git a/src/elastic_apm_profiler/src/ffi/mod.rs b/src/elastic_apm_profiler/src/ffi/mod.rs new file mode 100644 index 000000000..ca81233b8 --- /dev/null +++ b/src/elastic_apm_profiler/src/ffi/mod.rs @@ -0,0 +1,1183 @@ +#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +#![allow(overflowing_literals)] + +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// Copyright 2019 Camden Reslink +// MIT License +// https://github.com/camdenreslink/clr-profiler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// +// Contains LARGE_INTEGER and ULARGE_INTEGER implementations derived from winapi-rs. +// Licensed under Apache 2.0 +// https://github.com/retep998/winapi-rs/blob/0.3/LICENSE-APACHE + +pub mod types; + +use com::{ + sys::{GUID, HRESULT}, + AbiTransferable, CLSID, IID, +}; +use core::ffi::c_void; +use std::{intrinsics::transmute, ptr}; + +// numeric types +pub type c_int = i32; +pub type c_long = i32; +pub type c_uint = u32; +pub type c_ulong = u32; +pub type c_ushort = u16; +pub type c_uchar = u8; +pub type int = c_int; +pub type LONG = c_long; +pub type LONGLONG = i64; +pub type ULONGLONG = u64; +pub type BOOL = c_int; +pub type USHORT = c_ushort; +pub type UINT = c_uint; +pub type ULONG32 = c_uint; +pub type ULONG = c_ulong; +pub type DWORD = c_ulong; +pub type BYTE = c_uchar; +pub type COR_SIGNATURE = BYTE; + +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct LARGE_INTEGER([i64; 1]); +impl Copy for LARGE_INTEGER {} +impl Clone for LARGE_INTEGER { + #[inline] + fn clone(&self) -> LARGE_INTEGER { + *self + } +} +impl Default for LARGE_INTEGER { + #[inline] + fn default() -> LARGE_INTEGER { + unsafe { std::mem::zeroed() } + } +} +impl LARGE_INTEGER { + #[inline] + pub unsafe fn s(&self) -> &LARGE_INTEGER_s { + &*(self as *const _ as *const LARGE_INTEGER_s) + } + #[inline] + pub unsafe fn s_mut(&mut self) -> &mut LARGE_INTEGER_s { + &mut *(self as *mut _ as *mut LARGE_INTEGER_s) + } + #[inline] + pub unsafe fn u(&self) -> &LARGE_INTEGER_u { + &*(self as *const _ as *const LARGE_INTEGER_u) + } + #[inline] + pub unsafe fn u_mut(&mut self) -> &mut LARGE_INTEGER_u { + &mut *(self as *mut _ as *mut LARGE_INTEGER_u) + } + #[inline] + pub unsafe fn QuadPart(&self) -> &LONGLONG { + &*(self as *const _ as *const LONGLONG) + } + #[inline] + pub unsafe fn QuadPart_mut(&mut self) -> &mut LONGLONG { + &mut *(self as *mut _ as *mut LONGLONG) + } +} + +#[repr(C)] +#[derive(Copy)] +pub struct LARGE_INTEGER_s { + LowPart: ULONG, + HighPart: LONG, +} +impl Clone for LARGE_INTEGER_s { + #[inline] + fn clone(&self) -> LARGE_INTEGER_s { + *self + } +} +impl Default for LARGE_INTEGER_s { + #[inline] + fn default() -> LARGE_INTEGER_s { + unsafe { std::mem::zeroed() } + } +} + +#[repr(C)] +#[derive(Copy)] +pub struct LARGE_INTEGER_u { + LowPart: ULONG, + HighPart: LONG, +} +impl Clone for LARGE_INTEGER_u { + #[inline] + fn clone(&self) -> LARGE_INTEGER_u { + *self + } +} +impl Default for LARGE_INTEGER_u { + #[inline] + fn default() -> LARGE_INTEGER_u { + unsafe { std::mem::zeroed() } + } +} + +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct ULARGE_INTEGER([u64; 1]); +impl Copy for ULARGE_INTEGER {} +impl Clone for ULARGE_INTEGER { + #[inline] + fn clone(&self) -> ULARGE_INTEGER { + *self + } +} +impl Default for ULARGE_INTEGER { + #[inline] + fn default() -> ULARGE_INTEGER { + unsafe { std::mem::zeroed() } + } +} +impl ULARGE_INTEGER { + #[inline] + pub unsafe fn s(&self) -> &ULARGE_INTEGER_s { + &*(self as *const _ as *const ULARGE_INTEGER_s) + } + #[inline] + pub unsafe fn s_mut(&mut self) -> &mut ULARGE_INTEGER_s { + &mut *(self as *mut _ as *mut ULARGE_INTEGER_s) + } + #[inline] + pub unsafe fn u(&self) -> &ULARGE_INTEGER_s { + &*(self as *const _ as *const ULARGE_INTEGER_s) + } + #[inline] + pub unsafe fn u_mut(&mut self) -> &mut ULARGE_INTEGER_s { + &mut *(self as *mut _ as *mut ULARGE_INTEGER_s) + } + #[inline] + pub unsafe fn QuadPart(&self) -> &ULONGLONG { + &*(self as *const _ as *const ULONGLONG) + } + #[inline] + pub unsafe fn QuadPart_mut(&mut self) -> &mut ULONGLONG { + &mut *(self as *mut _ as *mut ULONGLONG) + } +} +#[repr(C)] +#[derive(Copy)] +pub struct ULARGE_INTEGER_s { + LowPart: ULONG, + HighPart: ULONG, +} +impl Clone for ULARGE_INTEGER_s { + #[inline] + fn clone(&self) -> ULARGE_INTEGER_s { + *self + } +} +impl Default for ULARGE_INTEGER_s { + #[inline] + fn default() -> ULARGE_INTEGER_s { + unsafe { std::mem::zeroed() } + } +} + +#[repr(C)] +#[derive(Copy)] +pub struct ULARGE_INTEGER_u { + LowPart: ULONG, + HighPart: ULONG, +} +impl Clone for ULARGE_INTEGER_u { + #[inline] + fn clone(&self) -> ULARGE_INTEGER_u { + *self + } +} +impl Default for ULARGE_INTEGER_u { + #[inline] + fn default() -> ULARGE_INTEGER_u { + unsafe { std::mem::zeroed() } + } +} + +// char types +pub type wchar_t = u16; +pub type WCHAR = wchar_t; +pub type LPCWSTR = *const WCHAR; +pub type MDUTF8CSTR = *const c_uchar; +pub type LPOLESTR = LPCWSTR; + +// pointer types +pub type UINT_PTR = usize; +pub type ULONG_PTR = usize; +pub type LPCBYTE = *const BYTE; +pub type SIZE_T = ULONG_PTR; +pub type LPVOID = *mut c_void; +pub type HANDLE = *mut c_void; +pub type UVCP_CONSTANT = *const c_void; +pub type LPCVOID = *const c_void; + +// guid types +pub type REFGUID = *const GUID; +pub type REFCLSID = *const IID; +pub type REFIID = *const IID; + +// profiler-specific pointers +pub type AppDomainID = UINT_PTR; +pub type AssemblyID = UINT_PTR; +pub type ClassID = UINT_PTR; +pub type ContextID = UINT_PTR; +pub type COR_PRF_ELT_INFO = UINT_PTR; +pub type COR_PRF_FRAME_INFO = UINT_PTR; +pub type FunctionID = UINT_PTR; +pub type GCHandleID = UINT_PTR; +pub type ModuleID = UINT_PTR; +pub type ObjectID = UINT_PTR; +pub type PCOR_SIGNATURE = *mut COR_SIGNATURE; +pub type PCCOR_SIGNATURE = *const COR_SIGNATURE; +pub type ProcessID = UINT_PTR; +pub type ReJITID = UINT_PTR; +pub type ThreadID = UINT_PTR; +pub type ClrInstanceID = USHORT; +pub type HCORENUM = *const c_void; +pub type RID = ULONG; + +#[repr(C)] +pub union FunctionIDOrClientID { + functionID: FunctionID, + clientID: UINT_PTR, +} + +// token types +pub type mdToken = ULONG32; +pub type mdModule = mdToken; +pub type mdTypeRef = mdToken; +pub type mdTypeDef = mdToken; +pub type mdFieldDef = mdToken; +pub type mdMethodDef = mdToken; +pub type mdParamDef = mdToken; +pub type mdInterfaceImpl = mdToken; +pub type mdMemberRef = mdToken; +pub type mdCustomAttribute = mdToken; +pub type mdPermission = mdToken; +pub type mdSignature = mdToken; +pub type mdEvent = mdToken; +pub type mdProperty = mdToken; +pub type mdModuleRef = mdToken; +pub type mdAssembly = mdToken; +pub type mdAssemblyRef = mdToken; +pub type mdFile = mdToken; +pub type mdExportedType = mdToken; +pub type mdManifestResource = mdToken; +pub type mdTypeSpec = mdToken; +pub type mdGenericParam = mdToken; +pub type mdMethodSpec = mdToken; +pub type mdGenericParamConstraint = mdToken; +pub type mdString = mdToken; +pub type mdCPToken = mdToken; + +// nil tokens +pub const mdTokenNil: mdToken = 0; +pub const mdModuleNil: mdModule = CorTokenType::mdtModule.bits(); +pub const mdTypeRefNil: mdTypeRef = CorTokenType::mdtTypeRef.bits(); +pub const mdTypeDefNil: mdTypeDef = CorTokenType::mdtTypeDef.bits(); +pub const mdFieldDefNil: mdFieldDef = CorTokenType::mdtFieldDef.bits(); +pub const mdMethodDefNil: mdMethodDef = CorTokenType::mdtMethodDef.bits(); +pub const mdParamDefNil: mdParamDef = CorTokenType::mdtParamDef.bits(); +pub const mdInterfaceImplNil: mdInterfaceImpl = CorTokenType::mdtInterfaceImpl.bits(); +pub const mdMemberRefNil: mdMemberRef = CorTokenType::mdtMemberRef.bits(); +pub const mdCustomAttributeNil: mdCustomAttribute = CorTokenType::mdtCustomAttribute.bits(); +pub const mdPermissionNil: mdPermission = CorTokenType::mdtPermission.bits(); +pub const mdSignatureNil: mdSignature = CorTokenType::mdtSignature.bits(); +pub const mdEventNil: mdEvent = CorTokenType::mdtEvent.bits(); +pub const mdPropertyNil: mdProperty = CorTokenType::mdtProperty.bits(); +pub const mdModuleRefNil: mdModuleRef = CorTokenType::mdtModuleRef.bits(); +pub const mdTypeSpecNil: mdTypeSpec = CorTokenType::mdtTypeSpec.bits(); +pub const mdAssemblyNil: mdAssembly = CorTokenType::mdtAssembly.bits(); +pub const mdAssemblyRefNil: mdAssemblyRef = CorTokenType::mdtAssemblyRef.bits(); +pub const mdFileNil: mdFile = CorTokenType::mdtFile.bits(); +pub const mdExportedTypeNil: mdExportedType = CorTokenType::mdtExportedType.bits(); +pub const mdManifestResourceNil: mdManifestResource = CorTokenType::mdtManifestResource.bits(); +pub const mdGenericParamNil: mdGenericParam = CorTokenType::mdtGenericParam.bits(); +pub const mdGenericParamConstraintNil: mdGenericParamConstraint = + CorTokenType::mdtGenericParamConstraint.bits(); +pub const mdMethodSpecNil: mdMethodSpec = CorTokenType::mdtMethodSpec.bits(); +pub const mdStringNil: mdString = CorTokenType::mdtString.bits(); + +// function pointer types +pub type FunctionEnter = unsafe extern "system" fn(funcID: FunctionID) -> (); +pub type FunctionLeave = unsafe extern "system" fn(funcID: FunctionID) -> (); +pub type FunctionTailcall = unsafe extern "system" fn(funcID: FunctionID) -> (); +pub type FunctionIDMapper = + unsafe extern "system" fn(funcId: FunctionID, pbHookFunction: *mut BOOL) -> UINT_PTR; +pub type FunctionEnter2 = unsafe extern "system" fn( + funcId: FunctionID, + clientData: UINT_PTR, + func: COR_PRF_FRAME_INFO, + argumentInfo: *const COR_PRF_FUNCTION_ARGUMENT_INFO, +) -> (); +pub type FunctionLeave2 = unsafe extern "system" fn( + funcId: FunctionID, + clientData: UINT_PTR, + func: COR_PRF_FRAME_INFO, + retvalRange: *const COR_PRF_FUNCTION_ARGUMENT_RANGE, +) -> (); +pub type FunctionTailcall2 = unsafe extern "system" fn( + funcId: FunctionID, + clientData: UINT_PTR, + func: COR_PRF_FRAME_INFO, +) -> (); +pub type FunctionIDMapper2 = unsafe extern "system" fn( + funcId: FunctionID, + clientData: *const c_void, + pbHookFunction: *mut BOOL, +) -> UINT_PTR; +pub type FunctionEnter3 = + unsafe extern "system" fn(functionIDOrClientID: FunctionIDOrClientID) -> (); +pub type FunctionLeave3 = + unsafe extern "system" fn(functionIDOrClientID: FunctionIDOrClientID) -> (); + +pub type FunctionTailcall3 = + unsafe extern "system" fn(functionIDOrClientID: FunctionIDOrClientID) -> (); + +pub type FunctionEnter3WithInfo = unsafe extern "system" fn( + functionIDOrClientID: FunctionIDOrClientID, + eltInfo: COR_PRF_ELT_INFO, +) -> (); + +pub type FunctionLeave3WithInfo = unsafe extern "system" fn( + functionIDOrClientID: FunctionIDOrClientID, + eltInfo: COR_PRF_ELT_INFO, +) -> (); + +pub type FunctionTailcall3WithInfo = unsafe extern "system" fn( + functionIDOrClientID: FunctionIDOrClientID, + eltInfo: COR_PRF_ELT_INFO, +) -> (); +pub type StackSnapshotCallback = unsafe extern "system" fn( + funcId: FunctionID, + ip: UINT_PTR, + frameInfo: COR_PRF_FRAME_INFO, + contextSize: ULONG32, + context: *const BYTE, + clientData: *const c_void, +) -> HRESULT; +pub type ObjectReferenceCallback = unsafe extern "system" fn( + root: ObjectID, + reference: *const ObjectID, + clientData: *const c_void, +) -> BOOL; + +// profiler types +#[repr(C)] +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum COR_PRF_JIT_CACHE { + COR_PRF_CACHED_FUNCTION_FOUND = 0, + COR_PRF_CACHED_FUNCTION_NOT_FOUND = 1, +} + +#[repr(C)] +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum COR_PRF_TRANSITION_REASON { + COR_PRF_TRANSITION_CALL = 0, + COR_PRF_TRANSITION_RETURN = 1, +} + +#[repr(C)] +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum COR_PRF_SUSPEND_REASON { + COR_PRF_SUSPEND_OTHER = 0, + COR_PRF_SUSPEND_FOR_GC = 1, + COR_PRF_SUSPEND_FOR_APPDOMAIN_SHUTDOWN = 2, + COR_PRF_SUSPEND_FOR_CODE_PITCHING = 3, + COR_PRF_SUSPEND_FOR_SHUTDOWN = 4, + COR_PRF_SUSPEND_FOR_INPROC_DEBUGGER = 6, + COR_PRF_SUSPEND_FOR_GC_PREP = 7, + COR_PRF_SUSPEND_FOR_REJIT = 8, +} +#[repr(C)] +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum COR_PRF_GC_REASON { + COR_PRF_GC_INDUCED = 1, + COR_PRF_GC_OTHER = 0, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum COR_PRF_GC_ROOT_KIND { + COR_PRF_GC_ROOT_STACK = 1, + COR_PRF_GC_ROOT_FINALIZER = 2, + COR_PRF_GC_ROOT_HANDLE = 3, + COR_PRF_GC_ROOT_OTHER = 0, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum COR_PRF_GC_ROOT_FLAGS { + COR_PRF_GC_ROOT_PINNING = 1, + COR_PRF_GC_ROOT_WEAKREF = 2, + COR_PRF_GC_ROOT_INTERIOR = 4, + COR_PRF_GC_ROOT_REFCOUNTED = 8, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_IL_MAP { + pub oldOffset: ULONG32, + pub newOffset: ULONG32, + pub fAccurate: BOOL, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_DEBUG_IL_TO_NATIVE_MAP { + pub ilOffset: ULONG32, + pub nativeStartOffset: ULONG32, + pub nativeEndOffset: ULONG32, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_FIELD_OFFSET { + pub ridOfField: mdFieldDef, + pub ulOffset: ULONG, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_CODE_INFO { + pub startAddress: UINT_PTR, + pub size: SIZE_T, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum COR_PRF_STATIC_TYPE { + COR_PRF_FIELD_NOT_A_STATIC = 0, + COR_PRF_FIELD_APP_DOMAIN_STATIC = 1, + COR_PRF_FIELD_THREAD_STATIC = 2, + COR_PRF_FIELD_CONTEXT_STATIC = 4, + COR_PRF_FIELD_RVA_STATIC = 8, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum COR_PRF_GC_GENERATION { + COR_PRF_GC_GEN_0 = 0, + COR_PRF_GC_GEN_1 = 1, + COR_PRF_GC_GEN_2 = 2, + COR_PRF_GC_LARGE_OBJECT_HEAP = 3, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_GC_GENERATION_RANGE { + pub generation: COR_PRF_GC_GENERATION, + pub rangeStart: ObjectID, + pub rangeLength: UINT_PTR, + pub rangeLengthReserved: UINT_PTR, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum COR_PRF_CLAUSE_TYPE { + COR_PRF_CLAUSE_NONE = 0, + COR_PRF_CLAUSE_FILTER = 1, + COR_PRF_CLAUSE_CATCH = 2, + COR_PRF_CLAUSE_FINALLY = 3, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_EX_CLAUSE_INFO { + pub clauseType: COR_PRF_CLAUSE_TYPE, + pub programCounter: UINT_PTR, + pub framePointer: UINT_PTR, + pub shadowStackPointer: UINT_PTR, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_FUNCTION_ARGUMENT_RANGE { + pub startAddress: UINT_PTR, + pub length: ULONG, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_FUNCTION_ARGUMENT_INFO { + pub numRanges: ULONG, + pub totalArgumentSize: ULONG, + pub ranges: [COR_PRF_FUNCTION_ARGUMENT_RANGE; 1], +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum COR_PRF_RUNTIME_TYPE { + COR_PRF_DESKTOP_CLR = 1, + COR_PRF_CORE_CLR = 2, +} +#[repr(C)] +#[derive(Debug, PartialEq, FromPrimitive)] +pub enum CorElementType { + ELEMENT_TYPE_END = 0x00, + ELEMENT_TYPE_VOID = 0x01, + ELEMENT_TYPE_BOOLEAN = 0x02, + ELEMENT_TYPE_CHAR = 0x03, + ELEMENT_TYPE_I1 = 0x04, + ELEMENT_TYPE_U1 = 0x05, + ELEMENT_TYPE_I2 = 0x06, + ELEMENT_TYPE_U2 = 0x07, + ELEMENT_TYPE_I4 = 0x08, + ELEMENT_TYPE_U4 = 0x09, + ELEMENT_TYPE_I8 = 0x0a, + ELEMENT_TYPE_U8 = 0x0b, + ELEMENT_TYPE_R4 = 0x0c, + ELEMENT_TYPE_R8 = 0x0d, + ELEMENT_TYPE_STRING = 0x0e, + + // every type above PTR will be simple type + ELEMENT_TYPE_PTR = 0x0f, // PTR + ELEMENT_TYPE_BYREF = 0x10, // BYREF + + // Please use ELEMENT_TYPE_VALUETYPE. ELEMENT_TYPE_VALUECLASS is deprecated. + ELEMENT_TYPE_VALUETYPE = 0x11, // VALUETYPE + ELEMENT_TYPE_CLASS = 0x12, // CLASS + ELEMENT_TYPE_VAR = 0x13, // a class type variable VAR + ELEMENT_TYPE_ARRAY = 0x14, // MDARRAY ... ... + ELEMENT_TYPE_GENERICINST = 0x15, // GENERICINST ... + ELEMENT_TYPE_TYPEDBYREF = 0x16, // TYPEDREF (it takes no args) a typed referece to some other type + + ELEMENT_TYPE_I = 0x18, // native integer size + ELEMENT_TYPE_U = 0x19, // native unsigned integer size + ELEMENT_TYPE_FNPTR = 0x1b, // FNPTR + ELEMENT_TYPE_OBJECT = 0x1c, // Shortcut for System.Object + ELEMENT_TYPE_SZARRAY = 0x1d, // Shortcut for single dimension zero lower bound array + // SZARRAY + ELEMENT_TYPE_MVAR = 0x1e, // a method type variable MVAR + + // This is only for binding + ELEMENT_TYPE_CMOD_REQD = 0x1f, // required C modifier : E_T_CMOD_REQD + ELEMENT_TYPE_CMOD_OPT = 0x20, // optional C modifier : E_T_CMOD_OPT + + // This is for signatures generated internally (which will not be persisted in any way). + ELEMENT_TYPE_INTERNAL = 0x21, // INTERNAL + + // Note that this is the max of base type excluding modifiers + ELEMENT_TYPE_MAX = 0x22, // first invalid element type + + ELEMENT_TYPE_MODIFIER = 0x40, + ELEMENT_TYPE_SENTINEL = 0x01 | CorElementType::ELEMENT_TYPE_MODIFIER as isize, // sentinel for varargs + ELEMENT_TYPE_PINNED = 0x05 | CorElementType::ELEMENT_TYPE_MODIFIER as isize, +} +impl From for CorElementType { + fn from(d: DWORD) -> Self { + unsafe { transmute(d as DWORD) } + } +} +#[repr(C)] +#[derive(Debug, PartialEq, Default)] +pub struct OSINFO { + dwOSPlatformId: DWORD, // Operating system platform. + dwOSMajorVersion: DWORD, // OS Major version. + dwOSMinorVersion: DWORD, // OS Minor version. +} +/// Managed assembly metadata +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] +pub struct ASSEMBLYMETADATA { + /// Major version + pub usMajorVersion: USHORT, + /// Minor version + pub usMinorVersion: USHORT, + /// Build number + pub usBuildNumber: USHORT, + /// Revision number + pub usRevisionNumber: USHORT, + /// Locale + pub szLocale: *mut WCHAR, + /// Locale buffer size in wide chars + pub cbLocale: ULONG, + /// Processor ID array + pub rProcessor: *mut DWORD, + /// Processor ID array size + pub ulProcessor: ULONG, + /// OS info array + pub rOS: *mut OSINFO, + /// OS info array size + pub ulOS: ULONG, +} +impl Default for ASSEMBLYMETADATA { + fn default() -> Self { + Self { + usMajorVersion: 0, + usMinorVersion: 0, + usBuildNumber: 0, + usRevisionNumber: 0, + szLocale: ptr::null_mut(), + cbLocale: 0, + rProcessor: ptr::null_mut(), + ulProcessor: 0, + rOS: ptr::null_mut(), + ulOS: 0, + } + } +} + +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct FILETIME { + dwLowDateTime: DWORD, + dwHighDateTime: DWORD, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct STATSTG { + pwcsName: LPOLESTR, + r#type: DWORD, + cbSize: ULARGE_INTEGER, + mtime: FILETIME, + ctime: FILETIME, + atime: FILETIME, + grfMode: DWORD, + grfLocksSupported: DWORD, + clsid: CLSID, + grfStateBits: DWORD, + reserved: DWORD, +} + +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_ASSEMBLY_REFERENCE_INFO { + pub pbPublicKeyOrToken: *const c_void, + pub cbPublicKeyOrToken: ULONG, + pub szName: LPCWSTR, + pub pMetaData: *const ASSEMBLYMETADATA, + pub pbHashValue: *const c_void, + pub cbHashValue: ULONG, + pub dwAssemblyRefFlags: DWORD, +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_FUNCTION { + functionId: FunctionID, + reJitId: ReJITID, +} +bitflags! { + pub struct COR_PRF_MONITOR: DWORD { + const COR_PRF_MONITOR_NONE = 0; + const COR_PRF_MONITOR_FUNCTION_UNLOADS = 0x1; + const COR_PRF_MONITOR_CLASS_LOADS = 0x2; + const COR_PRF_MONITOR_MODULE_LOADS = 0x4; + const COR_PRF_MONITOR_ASSEMBLY_LOADS = 0x8; + const COR_PRF_MONITOR_APPDOMAIN_LOADS = 0x10; + const COR_PRF_MONITOR_JIT_COMPILATION = 0x20; + const COR_PRF_MONITOR_EXCEPTIONS = 0x40; + const COR_PRF_MONITOR_GC = 0x80; + const COR_PRF_MONITOR_OBJECT_ALLOCATED = 0x100; + const COR_PRF_MONITOR_THREADS = 0x200; + const COR_PRF_MONITOR_REMOTING = 0x400; + const COR_PRF_MONITOR_CODE_TRANSITIONS = 0x800; + const COR_PRF_MONITOR_ENTERLEAVE = 0x1000; + const COR_PRF_MONITOR_CCW = 0x2000; + const COR_PRF_MONITOR_REMOTING_COOKIE = 0x4000 | Self::COR_PRF_MONITOR_REMOTING.bits; + const COR_PRF_MONITOR_REMOTING_ASYNC = 0x8000 | Self::COR_PRF_MONITOR_REMOTING.bits; + const COR_PRF_MONITOR_SUSPENDS = 0x10000; + const COR_PRF_MONITOR_CACHE_SEARCHES = 0x20000; + const COR_PRF_ENABLE_REJIT = 0x40000; + const COR_PRF_ENABLE_INPROC_DEBUGGING = 0x80000; + const COR_PRF_ENABLE_JIT_MAPS = 0x100000; + const COR_PRF_DISABLE_INLINING = 0x200000; + const COR_PRF_DISABLE_OPTIMIZATIONS = 0x400000; + const COR_PRF_ENABLE_OBJECT_ALLOCATED = 0x800000; + const COR_PRF_MONITOR_CLR_EXCEPTIONS = 0x1000000; + const COR_PRF_MONITOR_ALL = 0x107ffff; + const COR_PRF_ENABLE_FUNCTION_ARGS = 0x2000000; + const COR_PRF_ENABLE_FUNCTION_RETVAL = 0x4000000; + const COR_PRF_ENABLE_FRAME_INFO = 0x8000000; + const COR_PRF_ENABLE_STACK_SNAPSHOT = 0x10000000; + const COR_PRF_USE_PROFILE_IMAGES = 0x20000000; + const COR_PRF_DISABLE_TRANSPARENCY_CHECKS_UNDER_FULL_TRUST = 0x40000000; + const COR_PRF_DISABLE_ALL_NGEN_IMAGES = 0x80000000; + const COR_PRF_ALL = 0x8fffffff; + const COR_PRF_REQUIRE_PROFILE_IMAGE = Self::COR_PRF_USE_PROFILE_IMAGES.bits + | Self::COR_PRF_MONITOR_CODE_TRANSITIONS.bits + | Self::COR_PRF_MONITOR_ENTERLEAVE.bits; + const COR_PRF_ALLOWABLE_AFTER_ATTACH = Self::COR_PRF_MONITOR_THREADS.bits + | Self::COR_PRF_MONITOR_MODULE_LOADS.bits + | Self::COR_PRF_MONITOR_ASSEMBLY_LOADS.bits + | Self::COR_PRF_MONITOR_APPDOMAIN_LOADS.bits + | Self::COR_PRF_ENABLE_STACK_SNAPSHOT.bits + | Self::COR_PRF_MONITOR_GC.bits + | Self::COR_PRF_MONITOR_SUSPENDS.bits + | Self::COR_PRF_MONITOR_CLASS_LOADS.bits + | Self::COR_PRF_MONITOR_EXCEPTIONS.bits + | Self::COR_PRF_MONITOR_JIT_COMPILATION.bits + | Self::COR_PRF_ENABLE_REJIT.bits; + const COR_PRF_MONITOR_IMMUTABLE = Self::COR_PRF_MONITOR_CODE_TRANSITIONS.bits + | Self::COR_PRF_MONITOR_REMOTING.bits + | Self::COR_PRF_MONITOR_REMOTING_COOKIE.bits + | Self::COR_PRF_MONITOR_REMOTING_ASYNC.bits + | Self::COR_PRF_ENABLE_INPROC_DEBUGGING.bits + | Self::COR_PRF_ENABLE_JIT_MAPS.bits + | Self::COR_PRF_DISABLE_OPTIMIZATIONS.bits + | Self::COR_PRF_DISABLE_INLINING.bits + | Self::COR_PRF_ENABLE_OBJECT_ALLOCATED.bits + | Self::COR_PRF_ENABLE_FUNCTION_ARGS.bits + | Self::COR_PRF_ENABLE_FUNCTION_RETVAL.bits + | Self::COR_PRF_ENABLE_FRAME_INFO.bits + | Self::COR_PRF_USE_PROFILE_IMAGES.bits + | Self::COR_PRF_DISABLE_TRANSPARENCY_CHECKS_UNDER_FULL_TRUST.bits + | Self::COR_PRF_DISABLE_ALL_NGEN_IMAGES.bits; + } +} + +bitflags! { + pub struct COR_PRF_HIGH_MONITOR: DWORD { + const COR_PRF_HIGH_MONITOR_NONE = 0; + const COR_PRF_HIGH_ADD_ASSEMBLY_REFERENCES = 0x1; + const COR_PRF_HIGH_IN_MEMORY_SYMBOLS_UPDATED = 0x2; + const COR_PRF_HIGH_MONITOR_DYNAMIC_FUNCTION_UNLOADS = 0x4; + const COR_PRF_HIGH_DISABLE_TIERED_COMPILATION = 0x8; + const COR_PRF_HIGH_BASIC_GC = 0x10; + const COR_PRF_HIGH_MONITOR_GC_MOVED_OBJECTS = 0x20; + const COR_PRF_HIGH_REQUIRE_PROFILE_IMAGE = 0; + const COR_PRF_HIGH_MONITOR_LARGEOBJECT_ALLOCATED = 0x40; + const COR_PRF_HIGH_ALLOWABLE_AFTER_ATTACH = Self::COR_PRF_HIGH_IN_MEMORY_SYMBOLS_UPDATED.bits + | Self::COR_PRF_HIGH_MONITOR_DYNAMIC_FUNCTION_UNLOADS.bits + | Self::COR_PRF_HIGH_BASIC_GC.bits + | Self::COR_PRF_HIGH_MONITOR_GC_MOVED_OBJECTS.bits + | Self::COR_PRF_HIGH_MONITOR_LARGEOBJECT_ALLOCATED.bits; + const COR_PRF_HIGH_MONITOR_IMMUTABLE = COR_PRF_HIGH_MONITOR::COR_PRF_HIGH_DISABLE_TIERED_COMPILATION.bits; + } +} + +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_PRF_METHOD { + moduleId: ModuleID, + methodId: mdMethodDef, +} +bitflags! { + pub struct CorOpenFlags: DWORD { + const ofRead = 0x00000000; + const ofWrite = 0x00000001; + const ofReadWriteMask = 0x00000001; + const ofCopyMemory = 0x00000002; + const ofCacheImage = 0x00000004; + const ofManifestMetadata = 0x00000008; + const ofReadOnly = 0x00000010; + const ofTakeOwnership = 0x00000020; + const ofNoTypeLib = 0x00000080; + const ofNoTransform = 0x00001000; + const ofReserved1 = 0x00000100; + const ofReserved2 = 0x00000200; + const ofReserved = 0xffffff40; + } +} + +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum COR_PRF_SNAPSHOT_INFO { + COR_PRF_SNAPSHOT_DEFAULT = 0x0, + COR_PRF_SNAPSHOT_REGISTER_CONTEXT = 0x1, + COR_PRF_SNAPSHOT_X86_OPTIMIZED = 0x2, +} +bitflags! { + pub struct COR_PRF_MODULE_FLAGS: DWORD { + const COR_PRF_MODULE_DISK = 0x1; + const COR_PRF_MODULE_NGEN = 0x2; + const COR_PRF_MODULE_DYNAMIC = 0x4; + const COR_PRF_MODULE_COLLECTIBLE = 0x8; + const COR_PRF_MODULE_RESOURCE = 0x10; + const COR_PRF_MODULE_FLAT_LAYOUT = 0x20; + const COR_PRF_MODULE_WINDOWS_RUNTIME = 0x40; + } +} +bitflags! { + pub struct COR_PRF_REJIT_FLAGS: DWORD { + const COR_PRF_REJIT_BLOCK_INLINING = 0x1; + const COR_PRF_REJIT_INLINING_CALLBACKS = 0x2; + } +} +bitflags! { + pub struct COR_PRF_FINALIZER_FLAGS: DWORD { + const COR_PRF_FINALIZER_CRITICAL = 0x1; + } +} +#[repr(C)] +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum CorSaveSize { + cssAccurate = 0x0000, // Find exact save size, accurate but slower. + cssQuick = 0x0001, // Estimate save size, may pad estimate, but faster. + cssDiscardTransientCAs = 0x0002, // remove all of the CAs of discardable types +} +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct COR_SECATTR { + tkCtor: mdMemberRef, // Ref to constructor of security attribute. + pCustomAttribute: *const c_void, // Blob describing ctor args and field/property values. + cbCustomAttribute: ULONG, // Length of the above blob. +} +bitflags! { + pub struct CorFieldAttr: DWORD { + // member access mask - Use this mask to retrieve accessibility information. + const fdFieldAccessMask = 0x0007; + const fdPrivateScope = 0x0000; // Member not referenceable. + const fdPrivate = 0x0001; // Accessible only by the parent type. + const fdFamANDAssem = 0x0002; // Accessible by sub-types only in this Assembly. + const fdAssembly = 0x0003; // Accessibly by anyone in the Assembly. + const fdFamily = 0x0004; // Accessible only by type and sub-types. + const fdFamORAssem = 0x0005; // Accessibly by sub-types anywhere, plus anyone in assembly. + const fdPublic = 0x0006; // Accessibly by anyone who has visibility to this scope. + // end member access mask + + // field contract attributes. + const fdStatic = 0x0010; // Defined on type, else per instance. + const fdInitOnly = 0x0020; // Field may only be initialized, not written to after init. + const fdLiteral = 0x0040; // Value is compile time constant. + const fdNotSerialized = 0x0080; // Field does not have to be serialized when type is remoted. + + const fdSpecialName = 0x0200; // field is special. Name describes how. + + // interop attributes + const fdPinvokeImpl = 0x2000; // Implementation is forwarded through pinvoke. + + // Reserved flags for runtime use only. + const fdReservedMask = 0x9500; + const fdRTSpecialName = 0x0400; // Runtime(metadata internal APIs) should check name encoding. + const fdHasFieldMarshal = 0x1000; // Field has marshalling information. + const fdHasDefault = 0x8000; // Field has default. + const fdHasFieldRVA = 0x0100; // Field has RVA. + } +} +bitflags! { + pub struct CorMethodAttr: DWORD { + // member access mask - Use this mask to retrieve accessibility information. + const mdMemberAccessMask = 0x0007; + const mdPrivateScope = 0x0000; // Member not referenceable. + const mdPrivate = 0x0001; // Accessible only by the parent type. + const mdFamANDAssem = 0x0002; // Accessible by sub-types only in this Assembly. + const mdAssem = 0x0003; // Accessibly by anyone in the Assembly. + const mdFamily = 0x0004; // Accessible only by type and sub-types. + const mdFamORAssem = 0x0005; // Accessibly by sub-types anywhere, plus anyone in assembly. + const mdPublic = 0x0006; // Accessibly by anyone who has visibility to this scope. + // end member access mask + + // method contract attributes. + const mdStatic = 0x0010; // Defined on type, else per instance. + const mdFinal = 0x0020; // Method may not be overridden. + const mdVirtual = 0x0040; // Method virtual. + const mdHideBySig = 0x0080; // Method hides by name+sig, else just by name. + + // vtable layout mask - Use this mask to retrieve vtable attributes. + const mdVtableLayoutMask = 0x0100; + const mdReuseSlot = 0x0000; // The default. + const mdNewSlot = 0x0100; // Method always gets a new slot in the vtable. + // end vtable layout mask + + // method implementation attributes. + const mdCheckAccessOnOverride = 0x0200; // Overridability is the same as the visibility. + const mdAbstract = 0x0400; // Method does not provide an implementation. + const mdSpecialName = 0x0800; // Method is special. Name describes how. + + // interop attributes + const mdPinvokeImpl = 0x2000; // Implementation is forwarded through pinvoke. + const mdUnmanagedExport = 0x0008; // Managed method exported via thunk to unmanaged code. + + // Reserved flags for runtime use only. + const mdReservedMask = 0xd000; + const mdRTSpecialName = 0x1000; // Runtime should check name encoding. + const mdHasSecurity = 0x4000; // Method has security associate with it. + const mdRequireSecObject = 0x8000; // Method calls another method containing security code. + } +} +bitflags! { + pub struct CorMethodImpl: DWORD { + // code impl mask + const miCodeTypeMask = 0x0003; // Flags about code type. + const miIL = 0x0000; // Method impl is IL. + const miNative = 0x0001; // Method impl is native. + const miOPTIL = 0x0002; // Method impl is OPTIL + const miRuntime = 0x0003; // Method impl is provided by the runtime. + // end code impl mask + + // managed mask + const miManagedMask = 0x0004; // Flags specifying whether the code is managed or unmanaged. + const miUnmanaged = 0x0004; // Method impl is unmanaged, otherwise managed. + const miManaged = 0x0000; // Method impl is managed. + // end managed mask + + // implementation info and interop + const miForwardRef = 0x0010; // Indicates method is defined; used primarily in merge scenarios. + const miPreserveSig = 0x0080; // Indicates method sig is not to be mangled to do HRESULT conversion. + + const miInternalCall = 0x1000; // Reserved for internal use. + + const miSynchronized = 0x0020; // Method is single threaded through the body. + const miNoInlining = 0x0008; // Method may not be inlined. + const miAggressiveInlining = 0x0100; // Method should be inlined if possible. + const miNoOptimization = 0x0040; // Method may not be optimized. + const miAggressiveOptimization = 0x0200; // Method may contain hot code and should be aggressively optimized. + + // These are the flags that are allowed in MethodImplAttribute's Value + // property. This should include everything above except the code impl + // flags (which are used for MethodImplAttribute's MethodCodeType field). + const miUserMask = Self::miManagedMask.bits + | Self::miForwardRef.bits + | Self::miPreserveSig.bits + | Self::miInternalCall.bits + | Self::miSynchronized.bits + | Self::miNoInlining.bits + | Self::miAggressiveInlining.bits + | Self::miNoOptimization.bits + | Self::miAggressiveOptimization.bits; + + const miMaxMethodImplVal = 0xffff; // Range check value + } +} +bitflags! { + pub struct CorPinvokeMap: DWORD { + const pmNoMangle = 0x0001; // Pinvoke is to use the member name as specified. + + // Use this mask to retrieve the CharSet information. + const pmCharSetMask = 0x0006; + const pmCharSetNotSpec = 0x0000; + const pmCharSetAnsi = 0x0002; + const pmCharSetUnicode = 0x0004; + const pmCharSetAuto = 0x0006; + + const pmBestFitUseAssem = 0x0000; + const pmBestFitEnabled = 0x0010; + const pmBestFitDisabled = 0x0020; + const pmBestFitMask = 0x0030; + + const pmThrowOnUnmappableCharUseAssem = 0x0000; + const pmThrowOnUnmappableCharEnabled = 0x1000; + const pmThrowOnUnmappableCharDisabled = 0x2000; + const pmThrowOnUnmappableCharMask = 0x3000; + + const pmSupportsLastError = 0x0040; // Information about target function. Not relevant for fields. + + // None of the calling convention flags is relevant for fields. + const pmCallConvMask = 0x0700; + const pmCallConvWinapi = 0x0100; // Pinvoke will use native callconv appropriate to target windows platform. + const pmCallConvCdecl = 0x0200; + const pmCallConvStdcall = 0x0300; + const pmCallConvThiscall = 0x0400; // In M9, pinvoke will raise exception. + const pmCallConvFastcall = 0x0500; + + const pmMaxValue = 0xFFFF; + } +} + +pub const E_NOINTERFACE: HRESULT = 0x8000_4002; +pub const E_OUTOFMEMORY: HRESULT = 0x8007_000E; +pub const CLASS_E_NOAGGREGATION: HRESULT = 0x8004_0110; +pub const E_FAIL: HRESULT = 0x8000_4005; +pub const COR_E_INVALIDPROGRAM: HRESULT = 0x8013_153A; +pub const COR_E_INVALIDOPERATION: HRESULT = 0x8013_1509; +pub const COR_E_INDEXOUTOFRANGE: HRESULT = 0x8; +/// record not found on lookup +pub const CLDB_E_RECORD_NOTFOUND: HRESULT = 0x80131130; + +bitflags! { + pub struct CorAssemblyFlags: DWORD { + const afPublicKey = 0x0001; + const afPA_None = 0x0000; + const afPA_MSIL = 0x0010; + const afPA_x86 = 0x0020; + const afPA_IA64 = 0x0030; + const afPA_AMD64 = 0x0040; + const afPA_ARM = 0x0050; + const afPA_NoPlatform = 0x0070; + const afPA_Specified = 0x0080; + const afPA_Mask = 0x0070; + const afPA_FullMask = 0x00F0; + const afPA_Shift = 0x0004; + + const afEnableJITcompileTracking = 0x8000; + const afDisableJITcompileOptimizer= 0x4000; + + const afRetargetable = 0x0100; + const afContentType_Default = 0x0000; + const afContentType_WindowsRuntime = 0x0200; + const afContentType_Mask = 0x0E00; + } +} + +bitflags! { + /// Contains values that indicate type metadata. + pub struct CorTypeAttr: DWORD { + /// Used for type visibility information. + const tdVisibilityMask = 0x00000007; + /// Specifies that the type is not in public scope. + const tdNotPublic = 0x00000000; + /// Specifies that the type is in public scope. + const tdPublic = 0x00000001; + /// Specifies that the type is nested with public visibility. + const tdNestedPublic = 0x00000002; + /// Specifies that the type is nested with private visibility. + const tdNestedPrivate = 0x00000003; + const tdNestedFamily = 0x00000004; + const tdNestedAssembly = 0x00000005; + const tdNestedFamANDAssem = 0x00000006; + const tdNestedFamORAssem = 0x00000007; + + const tdLayoutMask = 0x00000018; + const tdAutoLayout = 0x00000000; + const tdSequentialLayout = 0x00000008; + const tdExplicitLayout = 0x00000010; + + const tdClassSemanticsMask = 0x00000020; + const tdClass = 0x00000000; + const tdInterface = 0x00000020; + + const tdAbstract = 0x00000080; + const tdSealed = 0x00000100; + const tdSpecialName = 0x00000400; + + const tdImport = 0x00001000; + const tdSerializable = 0x00002000; + const tdWindowsRuntime = 0x00004000; + + const tdStringFormatMask = 0x00030000; + const tdAnsiClass = 0x00000000; + const tdUnicodeClass = 0x00010000; + const tdAutoClass = 0x00020000; + const tdCustomFormatClass = 0x00030000; + const tdCustomFormatMask = 0x00C00000; + + const tdBeforeFieldInit = 0x00100000; + const tdForwarder = 0x00200000; + + const tdReservedMask = 0x00040800; + const tdRTSpecialName = 0x00000800; + const tdHasSecurity = 0x00040000; + } +} + +bitflags! { + /// Type of metadata token. + pub struct CorTokenType: DWORD { + const mdtModule = 0x00000000; + const mdtTypeRef = 0x01000000; + const mdtTypeDef = 0x02000000; + const mdtFieldDef = 0x04000000; + const mdtMethodDef = 0x06000000; + const mdtParamDef = 0x08000000; + const mdtInterfaceImpl = 0x09000000; + const mdtMemberRef = 0x0a000000; + const mdtCustomAttribute = 0x0c000000; + const mdtPermission = 0x0e000000; + const mdtSignature = 0x11000000; + const mdtEvent = 0x14000000; + const mdtProperty = 0x17000000; + const mdtModuleRef = 0x1a000000; + const mdtTypeSpec = 0x1b000000; + const mdtAssembly = 0x20000000; + const mdtAssemblyRef = 0x23000000; + const mdtFile = 0x26000000; + const mdtExportedType = 0x27000000; + const mdtManifestResource = 0x28000000; + const mdtGenericParam = 0x2a000000; + const mdtMethodSpec = 0x2b000000; + const mdtGenericParamConstraint = 0x2c000000; + const mdtString = 0x70000000; + const mdtName = 0x71000000; + const mdtBaseType = 0x72000000; + } +} + +bitflags! { + /// Method calling convention + pub struct CorCallingConvention : COR_SIGNATURE { + /// Indicates a default calling convention. + const IMAGE_CEE_CS_CALLCONV_DEFAULT = 0x0; + /// Indicates that the method takes a variable number of parameters. + const IMAGE_CEE_CS_CALLCONV_VARARG = 0x5; + /// Indicates that the call is to a field. + const IMAGE_CEE_CS_CALLCONV_FIELD = 0x6; + /// Indicates that the call is to a local method. + const IMAGE_CEE_CS_CALLCONV_LOCAL_SIG = 0x7; + /// Indicates that the call is to a property. + const IMAGE_CEE_CS_CALLCONV_PROPERTY = 0x8; + /// Indicates that the call is unmanaged. + const IMAGE_CEE_CS_CALLCONV_UNMANAGED = 0x9; + /// Indicates a generic method instantiation. + const IMAGE_CEE_CS_CALLCONV_GENERICINST = 0xa; + /// Indicates a 64-bit PInvoke call to a method that takes a variable number of parameters. + const IMAGE_CEE_CS_CALLCONV_NATIVEVARARG = 0xb; + /// Describes an invalid 4-bit value. + const IMAGE_CEE_CS_CALLCONV_MAX = 0xc; + /// Indicates that the calling convention is described by the bottom four bits. + const IMAGE_CEE_CS_CALLCONV_MASK = 0x0f; + /// Indicates that the top bit describes a 'this' parameter. + const IMAGE_CEE_CS_CALLCONV_HASTHIS = 0x20; + /// Indicates that a 'this' parameter is explicitly described in the signature. + const IMAGE_CEE_CS_CALLCONV_EXPLICITTHIS = 0x40; + /// Indicates a generic method signature with an explicit number of type arguments. + /// This precedes an ordinary parameter count. + const IMAGE_CEE_CS_CALLCONV_GENERIC = 0x10; + } +} + +impl CorCallingConvention { + /// Whether the calling convention is generic + pub fn is_generic(&self) -> bool { + self.contains(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC) + } +} + +/// Gets the type from the token +/// https://github.com/dotnet/coreclr/blob/a9f3fc16483eecfc47fb79c362811d870be02249/src/inc/corhdr.h#L1516 +pub fn type_from_token(token: mdToken) -> ULONG32 { + token & 0xff000000 +} + +pub fn rid_from_token(token: mdToken) -> RID { + token & 0x00ffffff +} + +pub fn token_from_rid(rid: RID, token_type: mdToken) -> mdToken { + rid | token_type +} + +pub fn rid_to_token(rid: RID, token_type: mdToken) -> mdToken { + rid | token_type +} + +pub fn is_nil_token(token: mdToken) -> bool { + rid_from_token(token) == 0 +} + +// allow types defined here to be passed across FFI boundary +macro_rules! primitive_transferable_type { + ($($t:ty),+) => { + $(unsafe impl AbiTransferable for $t { + type Abi = Self; + fn get_abi(&self) -> Self::Abi { + *self + } + fn set_abi(&mut self) -> *mut Self::Abi { + self as *mut Self::Abi + } + })* + }; +} + +primitive_transferable_type! { + COR_PRF_JIT_CACHE, + COR_PRF_TRANSITION_REASON, + COR_PRF_SUSPEND_REASON, + COR_PRF_GC_REASON, + CorSaveSize, + LARGE_INTEGER, + ULARGE_INTEGER +} diff --git a/src/elastic_apm_profiler/src/ffi/types.rs b/src/elastic_apm_profiler/src/ffi/types.rs new file mode 100644 index 000000000..a0b215621 --- /dev/null +++ b/src/elastic_apm_profiler/src/ffi/types.rs @@ -0,0 +1,218 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + ffi::{ + mdMethodDef, mdToken, mdTypeDef, AppDomainID, AssemblyID, ClassID, ClrInstanceID, + CorElementType, CorMethodAttr, CorMethodImpl, CorTypeAttr, FunctionID, ModuleID, ProcessID, + ReJITID, BYTE, COR_FIELD_OFFSET, COR_PRF_FRAME_INFO, COR_PRF_FUNCTION_ARGUMENT_INFO, + COR_PRF_FUNCTION_ARGUMENT_RANGE, COR_PRF_HIGH_MONITOR, COR_PRF_MODULE_FLAGS, + COR_PRF_MONITOR, COR_PRF_RUNTIME_TYPE, COR_SIGNATURE, DWORD, LPCBYTE, PCCOR_SIGNATURE, + ULONG, + }, + interfaces::ICorProfilerMethodEnum, +}; +use com::{sys::GUID, Interface}; +use std::{ + fmt, + fmt::{Display, Formatter}, +}; + +pub struct ArrayClassInfo { + pub element_type: CorElementType, + pub element_class_id: Option, + pub rank: u32, +} +pub struct ClassInfo { + pub module_id: ModuleID, + pub token: mdTypeDef, +} +#[derive(Debug)] +pub struct FunctionInfo { + pub class_id: ClassID, + pub module_id: ModuleID, + pub token: mdMethodDef, +} +pub struct FunctionTokenAndMetadata { + pub metadata: I, + pub token: mdMethodDef, +} +#[derive(Debug)] +pub struct ModuleInfo { + pub base_load_address: LPCBYTE, + pub file_name: String, + pub assembly_id: AssemblyID, +} +pub struct IlFunctionBody { + pub method_header: LPCBYTE, + pub method_size: u32, +} +impl From for &[u8] { + fn from(body: IlFunctionBody) -> Self { + unsafe { std::slice::from_raw_parts(body.method_header, body.method_size as usize) } + } +} +pub struct AppDomainInfo { + pub name: String, + pub process_id: ProcessID, +} +pub struct AssemblyInfo { + pub name: String, + pub module_id: ModuleID, + pub app_domain_id: AppDomainID, +} +pub struct FunctionInfo2 { + pub class_id: ClassID, + pub module_id: ModuleID, + pub token: mdMethodDef, + pub type_args: Vec, +} +pub struct ClassLayout { + pub field_offset: Vec, + pub class_size_bytes: u32, +} +pub struct ClassInfo2 { + pub module_id: ModuleID, + pub token: mdTypeDef, + pub parent_class_id: ClassID, + pub type_args: Vec, +} +pub struct ArrayObjectInfo { + pub dimension_sizes: Vec, + pub dimension_lower_bounds: Vec, + pub data: *mut BYTE, +} +pub struct StringLayout { + pub string_length_offset: u32, + pub buffer_offset: u32, +} +pub struct FunctionEnter3Info { + pub frame_info: COR_PRF_FRAME_INFO, + pub argument_info_length: u32, + pub argument_info: COR_PRF_FUNCTION_ARGUMENT_INFO, +} +pub struct FunctionLeave3Info { + pub frame_info: COR_PRF_FRAME_INFO, + pub retval_range: COR_PRF_FUNCTION_ARGUMENT_RANGE, +} +pub struct RuntimeInfo { + pub clr_instance_id: ClrInstanceID, + pub runtime_type: COR_PRF_RUNTIME_TYPE, + pub major_version: u16, + pub minor_version: u16, + pub build_number: u16, + pub qfe_version: u16, + pub version_string: String, +} +impl RuntimeInfo { + fn runtime_name(&self) -> &str { + match self.runtime_type { + COR_PRF_RUNTIME_TYPE::COR_PRF_DESKTOP_CLR => "Desktop CLR", + COR_PRF_RUNTIME_TYPE::COR_PRF_CORE_CLR => "Core CLR", + } + } + + pub fn is_desktop_clr(&self) -> bool { + self.runtime_type == COR_PRF_RUNTIME_TYPE::COR_PRF_DESKTOP_CLR + } + + pub fn is_core_clr(&self) -> bool { + self.runtime_type == COR_PRF_RUNTIME_TYPE::COR_PRF_CORE_CLR + } +} +impl Display for RuntimeInfo { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.runtime_name(), &self.version_string) + } +} + +pub struct ModuleInfo2 { + pub base_load_address: LPCBYTE, + pub file_name: String, + pub assembly_id: AssemblyID, + pub module_flags: COR_PRF_MODULE_FLAGS, +} +impl ModuleInfo2 { + pub fn is_windows_runtime(&self) -> bool { + self.module_flags + .contains(COR_PRF_MODULE_FLAGS::COR_PRF_MODULE_WINDOWS_RUNTIME) + } +} +pub struct FunctionAndRejit { + pub function_id: FunctionID, + pub rejit_id: ReJITID, +} +pub struct EventMask2 { + pub events_low: COR_PRF_MONITOR, + pub events_high: COR_PRF_HIGH_MONITOR, +} +pub struct EnumNgenModuleMethodsInliningThisMethod<'a> { + pub incomplete_data: bool, + pub method_enum: &'a mut ICorProfilerMethodEnum, +} +pub struct DynamicFunctionInfo { + pub module_id: ModuleID, + pub sig: PCCOR_SIGNATURE, + pub sig_length: u32, + pub name: String, +} +pub struct MethodProps { + pub class_token: mdTypeDef, + pub name: String, + pub attr_flags: CorMethodAttr, + pub sig: PCCOR_SIGNATURE, + pub sig_length: u32, + pub rva: u32, + pub impl_flags: CorMethodImpl, +} +pub struct ScopeProps { + pub name: String, + pub version: GUID, +} +pub struct TypeSpec { + /// The type spec signature + pub signature: Vec, +} +pub struct ModuleRefProps { + /// The name of the referenced module + pub name: String, +} +pub struct TypeDefProps { + /// The type name + pub name: String, + /// Flags that modify the type definition + pub cor_type_attr: CorTypeAttr, + /// A TypeDef or TypeRef metadata token that represents the base type of the requested type. + pub extends_td: mdToken, +} +pub struct TypeRefProps { + /// The type name + pub name: String, + /// A pointer to the scope in which the reference is made. This value is an AssemblyRef or ModuleRef metadata token. + pub parent_token: mdToken, +} +pub struct MemberRefProps { + /// The type name + pub name: String, + pub class_token: mdToken, + pub signature: Vec, +} +pub struct MemberProps { + pub name: String, + pub class_token: mdTypeDef, + pub member_flags: DWORD, + pub relative_virtual_address: ULONG, + pub method_impl_flags: DWORD, + pub element_type: CorElementType, + pub signature: Vec, + pub value: String, +} + +pub struct MethodSpecProps { + /// the MethodDef or MethodRef token that represents the method definition. + pub parent: mdToken, + /// the binary metadata signature of the method. + pub signature: Vec, +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_assembly_reference_provider.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_assembly_reference_provider.rs new file mode 100644 index 000000000..6453483d5 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_assembly_reference_provider.rs @@ -0,0 +1,41 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::COR_PRF_ASSEMBLY_REFERENCE_INFO; +use com::{ + interfaces::iunknown::IUnknown, + sys::{FAILED, HRESULT}, +}; + +interfaces! { + /// Enables the profiler to inform the common language runtime (CLR) of assembly references + /// that the profiler will add in the ICorProfilerCallback::ModuleLoadFinished callback. + /// + /// Supported in the .NET Framework 4.5.2 and later versions + #[uuid("66A78C24-2EEF-4F65-B45F-DD1D8038BF3C")] + pub unsafe interface ICorProfilerAssemblyReferenceProvider: IUnknown { + /// Informs the CLR of an assembly reference that the profiler plans to add in + /// the ModuleLoadFinished callback. + pub fn AddAssemblyReference( + &self, + pAssemblyRefInfo: *const COR_PRF_ASSEMBLY_REFERENCE_INFO, + ) -> HRESULT; + } +} + +impl ICorProfilerAssemblyReferenceProvider { + pub fn add_assembly_reference( + &self, + assembly_reference_info: &COR_PRF_ASSEMBLY_REFERENCE_INFO, + ) -> Result<(), HRESULT> { + let hr = unsafe { self.AddAssemblyReference(assembly_reference_info as *const _) }; + + if FAILED(hr) { + Err(hr) + } else { + Ok(()) + } + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_callback.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_callback.rs new file mode 100644 index 000000000..d706b1977 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_callback.rs @@ -0,0 +1,309 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + ffi::*, + interfaces::{ICorProfilerAssemblyReferenceProvider, ICorProfilerFunctionControl}, +}; +use com::{ + interfaces::IUnknown, + sys::{GUID, HRESULT}, +}; +use std::ffi::c_void; + +interfaces! { + #[uuid("176FBED1-A55C-4796-98CA-A9DA0EF883E7")] + pub unsafe interface ICorProfilerCallback: IUnknown { + pub fn Initialize( + &self, + pICorProfilerInfoUnk: IUnknown, + ) -> HRESULT; + pub fn Shutdown(&self) -> HRESULT; + pub fn AppDomainCreationStarted(&self, appDomainId: AppDomainID) -> HRESULT; + pub fn AppDomainCreationFinished( + &self, + appDomainId: AppDomainID, + hrStatus: HRESULT, + ) -> HRESULT; + pub fn AppDomainShutdownStarted(&self, appDomainId: AppDomainID) -> HRESULT; + pub fn AppDomainShutdownFinished( + &self, + appDomainId: AppDomainID, + hrStatus: HRESULT, + ) -> HRESULT; + pub fn AssemblyLoadStarted(&self, assemblyId: AssemblyID) -> HRESULT; + pub fn AssemblyLoadFinished( + &self, + assemblyId: AssemblyID, + hrStatus: HRESULT, + ) -> HRESULT; + pub fn AssemblyUnloadStarted(&self, assemblyId: AssemblyID) -> HRESULT; + pub fn AssemblyUnloadFinished( + &self, + assemblyId: AssemblyID, + hrStatus: HRESULT, + ) -> HRESULT; + pub fn ModuleLoadStarted(&self, moduleId: ModuleID) -> HRESULT; + pub fn ModuleLoadFinished(&self, moduleId: ModuleID, hrStatus: HRESULT) -> HRESULT; + pub fn ModuleUnloadStarted(&self, moduleId: ModuleID) -> HRESULT; + pub fn ModuleUnloadFinished(&self, moduleId: ModuleID, hrStatus: HRESULT) -> HRESULT; + pub fn ModuleAttachedToAssembly( + &self, + moduleId: ModuleID, + AssemblyId: AssemblyID, + ) -> HRESULT; + pub fn ClassLoadStarted(&self, classId: ClassID) -> HRESULT; + pub fn ClassLoadFinished(&self, classId: ClassID, hrStatus: HRESULT) -> HRESULT; + pub fn ClassUnloadStarted(&self, classId: ClassID) -> HRESULT; + pub fn ClassUnloadFinished(&self, classId: ClassID, hrStatus: HRESULT) -> HRESULT; + pub fn FunctionUnloadStarted(&self, functionId: FunctionID) -> HRESULT; + pub fn JITCompilationStarted( + &self, + functionId: FunctionID, + fIsSafeToBlock: BOOL, + ) -> HRESULT; + pub fn JITCompilationFinished( + &self, + functionId: FunctionID, + hrStatus: HRESULT, + fIsSafeToBlock: BOOL, + ) -> HRESULT; + pub fn JITCachedFunctionSearchStarted( + &self, + functionId: FunctionID, + pbUseCachedFunction: *mut BOOL, + ) -> HRESULT; + pub fn JITCachedFunctionSearchFinished( + &self, + functionId: FunctionID, + result: COR_PRF_JIT_CACHE, + ) -> HRESULT; + pub fn JITFunctionPitched(&self, functionId: FunctionID) -> HRESULT; + pub fn JITInlining( + &self, + callerId: FunctionID, + calleeId: FunctionID, + pfShouldInline: *mut BOOL, + ) -> HRESULT; + pub fn ThreadCreated(&self, threadId: ThreadID) -> HRESULT; + pub fn ThreadDestroyed(&self, threadId: ThreadID) -> HRESULT; + pub fn ThreadAssignedToOSThread( + &self, + managedThreadId: ThreadID, + osThreadId: DWORD, + ) -> HRESULT; + pub fn RemotingClientInvocationStarted(&self) -> HRESULT; + pub fn RemotingClientSendingMessage(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT; + pub fn RemotingClientReceivingReply(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT; + pub fn RemotingClientInvocationFinished(&self) -> HRESULT; + pub fn RemotingServerReceivingMessage(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT; + pub fn RemotingServerInvocationStarted(&self) -> HRESULT; + pub fn RemotingServerInvocationReturned(&self) -> HRESULT; + pub fn RemotingServerSendingReply(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT; + pub fn UnmanagedToManagedTransition( + &self, + functionId: FunctionID, + reason: COR_PRF_TRANSITION_REASON, + ) -> HRESULT; + pub fn ManagedToUnmanagedTransition( + &self, + functionId: FunctionID, + reason: COR_PRF_TRANSITION_REASON, + ) -> HRESULT; + pub fn RuntimeSuspendStarted(&self, suspendReason: COR_PRF_SUSPEND_REASON) -> HRESULT; + pub fn RuntimeSuspendFinished(&self) -> HRESULT; + pub fn RuntimeSuspendAborted(&self) -> HRESULT; + pub fn RuntimeResumeStarted(&self) -> HRESULT; + pub fn RuntimeResumeFinished(&self) -> HRESULT; + pub fn RuntimeThreadSuspended(&self, threadId: ThreadID) -> HRESULT; + pub fn RuntimeThreadResumed(&self, threadId: ThreadID) -> HRESULT; + pub fn MovedReferences( + &self, + cMovedObjectIDRanges: ULONG, + oldObjectIDRangeStart: *const ObjectID, + newObjectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const ULONG, + ) -> HRESULT; + pub fn ObjectAllocated(&self, objectId: ObjectID, classId: ClassID) -> HRESULT; + pub fn ObjectsAllocatedByClass( + &self, + cClassCount: ULONG, + classIds: *const ClassID, + cObjects: *const ULONG, + ) -> HRESULT; + pub fn ObjectReferences( + &self, + objectId: ObjectID, + classId: ClassID, + cObjectRefs: ULONG, + objectRefIds: *const ObjectID, + ) -> HRESULT; + pub fn RootReferences( + &self, + cRootRefs: ULONG, + rootRefIds: *const ObjectID, + ) -> HRESULT; + pub fn ExceptionThrown(&self, thrownObjectId: ObjectID) -> HRESULT; + pub fn ExceptionSearchFunctionEnter(&self, functionId: FunctionID) -> HRESULT; + pub fn ExceptionSearchFunctionLeave(&self) -> HRESULT; + pub fn ExceptionSearchFilterEnter(&self, functionId: FunctionID) -> HRESULT; + pub fn ExceptionSearchFilterLeave(&self) -> HRESULT; + pub fn ExceptionSearchCatcherFound(&self, functionId: FunctionID) -> HRESULT; + pub fn ExceptionOSHandlerEnter(&self, __unused: UINT_PTR) -> HRESULT; + pub fn ExceptionOSHandlerLeave(&self, __unused: UINT_PTR) -> HRESULT; + pub fn ExceptionUnwindFunctionEnter(&self, functionId: FunctionID) -> HRESULT; + pub fn ExceptionUnwindFunctionLeave(&self) -> HRESULT; + pub fn ExceptionUnwindFinallyEnter(&self, functionId: FunctionID) -> HRESULT; + pub fn ExceptionUnwindFinallyLeave(&self) -> HRESULT; + pub fn ExceptionCatcherEnter( + &self, + functionId: FunctionID, + objectId: ObjectID, + ) -> HRESULT; + pub fn ExceptionCatcherLeave(&self) -> HRESULT; + pub fn COMClassicVTableCreated( + &self, + wrappedClassId: ClassID, + implementedIID: REFGUID, + pVTable: *const c_void, + cSlots: ULONG, + ) -> HRESULT; + pub fn COMClassicVTableDestroyed( + &self, + wrappedClassId: ClassID, + implementedIID: REFGUID, + pVTable: *const c_void, + ) -> HRESULT; + pub fn ExceptionCLRCatcherFound(&self) -> HRESULT; + pub fn ExceptionCLRCatcherExecute(&self) -> HRESULT; + } + + #[uuid("8A8CC829-CCF2-49FE-BBAE-0F022228071A")] + pub unsafe interface ICorProfilerCallback2: ICorProfilerCallback { + pub fn ThreadNameChanged(&self, + threadId: ThreadID, + cchName: ULONG, + name: *const WCHAR, + ) -> HRESULT; + pub fn GarbageCollectionStarted(&self, + cGenerations: int, + generationCollected: *const BOOL, + reason: COR_PRF_GC_REASON, + ) -> HRESULT; + pub fn SurvivingReferences(&self, + cSurvivingObjectIDRanges: ULONG, + objectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const ULONG, + ) -> HRESULT; + pub fn GarbageCollectionFinished(&self) -> HRESULT; + pub fn FinalizeableObjectQueued(&self, + finalizerFlags: DWORD, + objectID: ObjectID, + ) -> HRESULT; + pub fn RootReferences2(&self, + cRootRefs: ULONG, + rootRefIds: *const ObjectID, + rootKinds: *const COR_PRF_GC_ROOT_KIND, + rootFlags: *const COR_PRF_GC_ROOT_FLAGS, + rootIds: *const UINT_PTR, + ) -> HRESULT; + pub fn HandleCreated(&self, + handleId: GCHandleID, + initialObjectId: ObjectID, + ) -> HRESULT; + pub fn HandleDestroyed(&self, handleId: GCHandleID) -> HRESULT; + } + + #[uuid("4FD2ED52-7731-4B8D-9469-03D2CC3086C5")] + pub unsafe interface ICorProfilerCallback3: ICorProfilerCallback2 { + pub fn InitializeForAttach(&self, + pCorProfilerInfoUnk: IUnknown, + pvClientData: *const c_void, + cbClientData: UINT, + ) -> HRESULT; + pub fn ProfilerAttachComplete(&self) -> HRESULT; + pub fn ProfilerDetachSucceeded(&self) -> HRESULT; + } + + #[uuid("7B63B2E3-107D-4D48-B2F6-F61E229470D2")] + pub unsafe interface ICorProfilerCallback4: ICorProfilerCallback3 { + pub fn ReJITCompilationStarted(&self, + functionId: FunctionID, + rejitId: ReJITID, + fIsSafeToBlock: BOOL, + ) -> HRESULT; + pub fn GetReJITParameters(&self, + moduleId: ModuleID, + methodId: mdMethodDef, + pFunctionControl: ICorProfilerFunctionControl, + ) -> HRESULT; + pub fn ReJITCompilationFinished(&self, + functionId: FunctionID, + rejitId: ReJITID, + hrStatus: HRESULT, + fIsSafeToBlock: BOOL, + ) -> HRESULT; + pub fn ReJITError(&self, + moduleId: ModuleID, + methodId: mdMethodDef, + functionId: FunctionID, + hrStatus: HRESULT, + ) -> HRESULT; + pub fn MovedReferences2(&self, + cMovedObjectIDRanges: ULONG, + oldObjectIDRangeStart: *const ObjectID, + newObjectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const SIZE_T, + ) -> HRESULT; + pub fn SurvivingReferences2(&self, + cSurvivingObjectIDRanges: ULONG, + objectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const SIZE_T, + ) -> HRESULT; + } + + #[uuid("8DFBA405-8C9F-45F8-BFFA-83B14CEF78B5")] + pub unsafe interface ICorProfilerCallback5: ICorProfilerCallback4 { + pub fn ConditionalWeakTableElementReferences(&self, + cRootRefs: ULONG, + keyRefIds: *const ObjectID, + valueRefIds: *const ObjectID, + rootIds: *const GCHandleID, + ) -> HRESULT; + } + + #[uuid("FC13DF4B-4448-4F4F-950C-BA8D19D00C36")] + pub unsafe interface ICorProfilerCallback6: ICorProfilerCallback5 { + pub fn GetAssemblyReferences(&self, + wszAssemblyPath: *const WCHAR, + pAsmRefProvider: ICorProfilerAssemblyReferenceProvider, + ) -> HRESULT; + } + + #[uuid("F76A2DBA-1D52-4539-866C-2AA518F9EFC3")] + pub unsafe interface ICorProfilerCallback7: ICorProfilerCallback6 { + pub fn ModuleInMemorySymbolsUpdated(&self, moduleId: ModuleID) -> HRESULT; + } + + #[uuid("5BED9B15-C079-4D47-BFE2-215A140C07E0")] + pub unsafe interface ICorProfilerCallback8: ICorProfilerCallback7 { + pub fn DynamicMethodJITCompilationStarted(&self, + functionId: FunctionID, + fIsSafeToBlock: BOOL, + pILHeader: LPCBYTE, + cbILHeader: ULONG, + ) -> HRESULT; + pub fn DynamicMethodJITCompilationFinished(&self, + functionId: FunctionID, + hrStatus: HRESULT, + fIsSafeToBlock: BOOL, + ) -> HRESULT; + } + + #[uuid("27583EC3-C8F5-482F-8052-194B8CE4705A")] + pub unsafe interface ICorProfilerCallback9: ICorProfilerCallback8 { + pub fn DynamicMethodUnloaded(&self, functionId: FunctionID) -> HRESULT; + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_function_control.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_function_control.rs new file mode 100644 index 000000000..0e4b6b381 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_function_control.rs @@ -0,0 +1,40 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{ + interfaces::iunknown::IUnknown, + sys::{FAILED, HRESULT}, +}; + +interfaces! { + #[uuid("F0963021-E1EA-4732-8581-E01B0BD3C0C6")] + pub unsafe interface ICorProfilerFunctionControl: IUnknown { + pub fn SetCodegenFlags(&self, flags: DWORD) -> HRESULT; + pub fn SetILFunctionBody( + &self, + cbNewILMethodHeader: ULONG, + pbNewILMethodHeader: LPCBYTE, + ) -> HRESULT; + pub fn SetILInstrumentedCodeMap( + &self, + cILMapEntries: ULONG, + rgILMapEntries: *const COR_IL_MAP, + ) -> HRESULT; + } +} + +impl ICorProfilerFunctionControl { + pub fn set_il_function_body(&self, new_method: &[u8]) -> Result<(), HRESULT> { + let len = new_method.len() as ULONG; + let ptr = new_method.as_ptr(); + let hr = unsafe { self.SetILFunctionBody(len, ptr) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(()) + } + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_function_enum.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_function_enum.rs new file mode 100644 index 000000000..324f85cfc --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_function_enum.rs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{interfaces::IUnknown, sys::HRESULT}; + +interfaces! { + #[uuid("FF71301A-B994-429D-A10B-B345A65280EF")] + pub unsafe interface ICorProfilerFunctionEnum: IUnknown { + pub unsafe fn Skip(&self, celt: ULONG) -> HRESULT; + pub unsafe fn Reset(&self) -> HRESULT; + pub unsafe fn Clone(&self, ppEnum: *mut *mut ICorProfilerFunctionEnum) -> HRESULT; + pub unsafe fn GetCount(&self, pcelt: *mut ULONG) -> HRESULT; + pub unsafe fn Next(&self, + celt: ULONG, + ids: *mut COR_PRF_FUNCTION, + pceltFetched: *mut ULONG, + ) -> HRESULT; + } + +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_info.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_info.rs new file mode 100644 index 000000000..f5df82c5b --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_info.rs @@ -0,0 +1,1228 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use std::{ffi::c_void, mem::MaybeUninit, ptr}; + +use com::{ + interfaces, + interfaces::IUnknown, + sys::{FAILED, HRESULT, S_OK}, + Interface, +}; +use widestring::U16CString; + +use crate::{ + cil::MAX_LENGTH, + ffi::{ + types::{ + AppDomainInfo, ArrayClassInfo, AssemblyInfo, ClassInfo, FunctionInfo, + FunctionTokenAndMetadata, IlFunctionBody, ModuleInfo, ModuleInfo2, RuntimeInfo, + }, + *, + }, + interfaces::{ + ICorProfilerFunctionEnum, ICorProfilerMethodEnum, ICorProfilerModuleEnum, + ICorProfilerObjectEnum, ICorProfilerThreadEnum, IMethodMalloc, + }, +}; + +interfaces! { + #[uuid("28B5557D-3F3F-48b4-90B2-5F9EEA2F6C48")] + pub unsafe interface ICorProfilerInfo: IUnknown { + pub fn GetClassFromObject(&self, objectId: ObjectID, pClassId: *mut ClassID) -> HRESULT; + pub fn GetClassFromToken(&self, + moduleId: ModuleID, + typeDef: mdTypeDef, + pClassId: *mut ClassID, + ) -> HRESULT; + pub fn GetCodeInfo(&self, + functionId: FunctionID, + pStart: *mut LPCBYTE, + pcSize: *mut ULONG, + ) -> HRESULT; + pub fn GetEventMask(&self, pdwEvents: *mut DWORD) -> HRESULT; + pub fn GetFunctionFromIP(&self, ip: LPCBYTE, pFunctionId: *mut FunctionID) -> HRESULT; + pub fn GetFunctionFromToken(&self, + moduleId: ModuleID, + token: mdToken, + pFunctionId: *mut FunctionID, + ) -> HRESULT; + pub fn GetHandleFromThread(&self, threadId: ThreadID, phThread: *mut HANDLE) -> HRESULT; + pub fn GetObjectSize(&self, objectId: ObjectID, pcSize: *mut ULONG) -> HRESULT; + pub fn IsArrayClass(&self, + classId: ClassID, + pBaseElemType: *mut CorElementType, + pBaseClassId: *mut ClassID, + pcRank: *mut ULONG, + ) -> HRESULT; + pub fn GetThreadInfo(&self, + threadId: ThreadID, + pdwWin32ThreadId: *mut DWORD, + ) -> HRESULT; + pub fn GetCurrentThreadID(&self, pThreadId: *mut ThreadID) -> HRESULT; + pub fn GetClassIDInfo(&self, + classId: ClassID, + pModuleId: *mut ModuleID, + pTypeDefToken: *mut mdTypeDef, + ) -> HRESULT; + pub fn GetFunctionInfo(&self, + functionId: FunctionID, + pClassId: *mut ClassID, + pModuleId: *mut ModuleID, + pToken: *mut mdToken, + ) -> HRESULT; + pub fn SetEventMask(&self, dwEvents: DWORD) -> HRESULT; + pub fn SetEnterLeaveFunctionHooks(&self, + pFuncEnter: *const FunctionEnter, + pFuncLeave: *const FunctionLeave, + pFuncTailcall: *const FunctionTailcall, + ) -> HRESULT; + pub fn SetFunctionIDMapper(&self, pFunc: *const FunctionIDMapper) -> HRESULT; + pub fn GetTokenAndMetaDataFromFunction(&self, + functionId: FunctionID, + riid: REFIID, + ppImport: *mut *mut IUnknown, + pToken: *mut mdToken, + ) -> HRESULT; + pub fn GetModuleInfo(&self, + moduleId: ModuleID, + ppBaseLoadAddress: *mut LPCBYTE, + cchName: ULONG, + pcchName: *mut ULONG, + szName: *mut WCHAR, + pAssemblyId: *mut AssemblyID, + ) -> HRESULT; + pub fn GetModuleMetaData(&self, + moduleId: ModuleID, + dwOpenFlags: DWORD, + riid: REFIID, + ppOut: *mut *mut IUnknown, + ) -> HRESULT; + /// Gets a pointer to the body of a method in Microsoft intermediate language (MSIL) code, + /// starting at its header. + pub fn GetILFunctionBody(&self, + moduleId: ModuleID, + methodId: mdMethodDef, + ppMethodHeader: *mut LPCBYTE, + pcbMethodSize: *mut ULONG, + ) -> HRESULT; + pub fn GetILFunctionBodyAllocator(&self, + moduleId: ModuleID, + ppMalloc: *mut *mut IMethodMalloc, + ) -> HRESULT; + pub fn SetILFunctionBody(&self, + moduleId: ModuleID, + methodid: mdMethodDef, + pbNewILMethodHeader: LPCBYTE, + ) -> HRESULT; + pub fn GetAppDomainInfo(&self, + appDomainId: AppDomainID, + cchName: ULONG, + pcchName: *mut ULONG, + szName: *mut WCHAR, + pProcessId: *mut ProcessID, + ) -> HRESULT; + pub fn GetAssemblyInfo(&self, + assemblyId: AssemblyID, + cchName: ULONG, + pcchName: *mut ULONG, + szName: *mut WCHAR, + pAppDomainId: *mut AppDomainID, + pModuleId: *mut ModuleID, + ) -> HRESULT; + pub fn SetFunctionReJIT(&self, functionId: FunctionID) -> HRESULT; + pub fn ForceGC(&self) -> HRESULT; + pub fn SetILInstrumentedCodeMap(&self, + functionId: FunctionID, + fStartJit: BOOL, + cILMapEntries: ULONG, + rgILMapEntries: *const COR_IL_MAP, + ) -> HRESULT; + pub fn GetInprocInspectionInterface(&self, + ppicd: *mut *mut IUnknown, + ) -> HRESULT; + pub fn GetInprocInspectionIThisThread(&self, + ppicd: *mut *mut IUnknown, + ) -> HRESULT; + pub fn GetThreadContext(&self, + threadId: ThreadID, + pContextId: *mut ContextID, + ) -> HRESULT; + pub fn BeginInprocDebugging(&self, + fThisThreadOnly: BOOL, + pdwProfilerContext: *mut DWORD, + ) -> HRESULT; + pub fn EndInprocDebugging(&self, dwProfilerContext: DWORD) -> HRESULT; + pub fn GetILToNativeMapping(&self, + functionId: FunctionID, + cMap: ULONG32, + pcMap: *mut ULONG32, + map: *mut COR_DEBUG_IL_TO_NATIVE_MAP, + ) -> HRESULT; + } + + #[uuid("CC0935CD-A518-487d-B0BB-A93214E65478")] + pub unsafe interface ICorProfilerInfo2: ICorProfilerInfo { + pub fn DoStackSnapshot(&self, + thread: ThreadID, + callback: *const StackSnapshotCallback, + infoFlags: ULONG32, + clientData: *const c_void, + context: *const BYTE, + contextSize: ULONG32, + ) -> HRESULT; + pub fn SetEnterLeaveFunctionHooks2(&self, + pFuncEnter: *const FunctionEnter2, + pFuncLeave: *const FunctionLeave2, + pFuncTailcall: *const FunctionTailcall2, + ) -> HRESULT; + pub fn GetFunctionInfo2(&self, + funcId: FunctionID, + frameInfo: COR_PRF_FRAME_INFO, + pClassId: *mut ClassID, + pModuleId: *mut ModuleID, + pToken: *mut mdToken, + cTypeArgs: ULONG32, + pcTypeArgs: *mut ULONG32, + typeArgs: *mut ClassID, + ) -> HRESULT; + pub fn GetStringLayout(&self, + pBufferLengthOffset: *mut ULONG, + pStringLengthOffset: *mut ULONG, + pBufferOffset: *mut ULONG, + ) -> HRESULT; + pub fn GetClassLayout(&self, + classID: ClassID, + rFieldOffset: *mut COR_FIELD_OFFSET, + cFieldOffset: ULONG, + pcFieldOffset: *mut ULONG, + pulClassSize: *mut ULONG, + ) -> HRESULT; + pub fn GetClassIDInfo2(&self, + classId: ClassID, + pModuleId: *mut ModuleID, + pTypeDefToken: *mut mdTypeDef, + pParentClassId: *mut ClassID, + cNumTypeArgs: ULONG32, + pcNumTypeArgs: *mut ULONG32, + typeArgs: *mut ClassID, + ) -> HRESULT; + pub fn GetCodeInfo2(&self, + functionID: FunctionID, + cCodeInfos: ULONG32, + pcCodeInfos: *mut ULONG32, + codeInfos: *mut COR_PRF_CODE_INFO, + ) -> HRESULT; + pub fn GetClassFromTokenAndTypeArgs(&self, + moduleID: ModuleID, + typeDef: mdTypeDef, + cTypeArgs: ULONG32, + typeArgs: *const ClassID, + pClassID: *mut ClassID, + ) -> HRESULT; + pub fn GetFunctionFromTokenAndTypeArgs(&self, + moduleID: ModuleID, + funcDef: mdMethodDef, + classId: ClassID, + cTypeArgs: ULONG32, + typeArgs: *const ClassID, + pFunctionID: *mut FunctionID, + ) -> HRESULT; + pub fn EnumModuleFrozenObjects(&self, + moduleID: ModuleID, + ppEnum: *mut *mut ICorProfilerObjectEnum, + ) -> HRESULT; + pub fn GetArrayObjectInfo(&self, + objectId: ObjectID, + cDimensions: ULONG32, + pDimensionSizes: *mut ULONG32, + pDimensionLowerBounds: *mut int, + ppData: *mut *mut BYTE, + ) -> HRESULT; + pub fn GetBoxClassLayout(&self, + classId: ClassID, + pBufferOffset: *mut ULONG32, + ) -> HRESULT; + pub fn GetThreadAppDomain(&self, + threadId: ThreadID, + pAppDomainId: *mut AppDomainID, + ) -> HRESULT; + pub fn GetRVAStaticAddress(&self, + classId: ClassID, + fieldToken: mdFieldDef, + ppAddress: *mut *mut c_void, + ) -> HRESULT; + pub fn GetAppDomainStaticAddress(&self, + classId: ClassID, + fieldToken: mdFieldDef, + appDomainId: AppDomainID, + ppAddress: *mut *mut c_void, + ) -> HRESULT; + pub fn GetThreadStaticAddress(&self, + classId: ClassID, + fieldToken: mdFieldDef, + threadId: ThreadID, + ppAddress: *mut *mut c_void, + ) -> HRESULT; + pub fn GetContextStaticAddress(&self, + classId: ClassID, + fieldToken: mdFieldDef, + contextId: ContextID, + ppAddress: *mut *mut c_void, + ) -> HRESULT; + pub fn GetStaticFieldInfo(&self, + classId: ClassID, + fieldToken: mdFieldDef, + pFieldInfo: *mut COR_PRF_STATIC_TYPE, + ) -> HRESULT; + pub fn GetGenerationBounds(&self, + cObjectRanges: ULONG, + pcObjectRanges: *mut ULONG, + ranges: *mut COR_PRF_GC_GENERATION_RANGE, + ) -> HRESULT; + pub fn GetObjectGeneration(&self, + objectId: ObjectID, + range: *mut COR_PRF_GC_GENERATION_RANGE, + ) -> HRESULT; + pub fn GetNotifiedExceptionClauseInfo(&self, pinfo: *mut COR_PRF_EX_CLAUSE_INFO) -> HRESULT; + } + + #[uuid("B555ED4F-452A-4E54-8B39-B5360BAD32A0")] + pub unsafe interface ICorProfilerInfo3: ICorProfilerInfo2 { + pub fn EnumJITedFunctions(&self, ppEnum: *mut *mut ICorProfilerFunctionEnum) -> HRESULT; + pub fn RequestProfilerDetach(&self, dwExpectedCompletionMilliseconds: DWORD) -> HRESULT; + pub fn SetFunctionIDMapper2(&self, + pFunc: *const FunctionIDMapper2, + clientData: *const c_void, + ) -> HRESULT; + pub fn GetStringLayout2(&self, + pStringLengthOffset: *mut ULONG, + pBufferOffset: *mut ULONG, + ) -> HRESULT; + pub fn SetEnterLeaveFunctionHooks3(&self, + pFuncEnter3: *const FunctionEnter3, + pFuncLeave3: *const FunctionLeave3, + pFuncTailcall3: *const FunctionTailcall3, + ) -> HRESULT; + pub fn SetEnterLeaveFunctionHooks3WithInfo(&self, + pFuncEnter3WithInfo: *const FunctionEnter3WithInfo, + pFuncLeave3WithInfo: *const FunctionLeave3WithInfo, + pFuncTailcall3WithInfo: *const FunctionTailcall3WithInfo, + ) -> HRESULT; + pub fn GetFunctionEnter3Info(&self, + functionId: FunctionID, + eltInfo: COR_PRF_ELT_INFO, + pFrameInfo: *mut COR_PRF_FRAME_INFO, + pcbArgumentInfo: *mut ULONG, + pArgumentInfo: *mut COR_PRF_FUNCTION_ARGUMENT_INFO, + ) -> HRESULT; + pub fn GetFunctionLeave3Info(&self, + functionId: FunctionID, + eltInfo: COR_PRF_ELT_INFO, + pFrameInfo: *mut COR_PRF_FRAME_INFO, + pRetvalRange: *mut COR_PRF_FUNCTION_ARGUMENT_RANGE, + ) -> HRESULT; + pub fn GetFunctionTailcall3Info(&self, + functionId: FunctionID, + eltInfo: COR_PRF_ELT_INFO, + pFrameInfo: *mut COR_PRF_FRAME_INFO, + ) -> HRESULT; + pub fn EnumModules(&self, ppEnum: *mut *mut ICorProfilerModuleEnum) -> HRESULT; + pub fn GetRuntimeInformation(&self, + pClrInstanceId: *mut USHORT, + pRuntimeType: *mut COR_PRF_RUNTIME_TYPE, + pMajorVersion: *mut USHORT, + pMinorVersion: *mut USHORT, + pBuildNumber: *mut USHORT, + pQFEVersion: *mut USHORT, + cchVersionString: ULONG, + pcchVersionString: *mut ULONG, + szVersionString: *mut WCHAR, + ) -> HRESULT; + pub fn GetThreadStaticAddress2(&self, + classId: ClassID, + fieldToken: mdFieldDef, + appDomainId: AppDomainID, + threadId: ThreadID, + ppAddress: *mut *mut c_void, + ) -> HRESULT; + pub fn GetAppDomainsContainingModule(&self, + moduleId: ModuleID, + cAppDomainIds: ULONG32, + pcAppDomainIds: *mut ULONG32, + appDomainIds: *mut AppDomainID, + ) -> HRESULT; + pub fn GetModuleInfo2(&self, + moduleId: ModuleID, + ppBaseLoadAddress: *mut LPCBYTE, + cchName: ULONG, + pcchName: *mut ULONG, + szName: *mut WCHAR, + pAssemblyId: *mut AssemblyID, + pdwModuleFlags: *mut DWORD, + ) -> HRESULT; + } + + #[uuid("0D8FDCAA-6257-47BF-B1BF-94DAC88466EE")] + pub unsafe interface ICorProfilerInfo4: ICorProfilerInfo3 { + pub fn EnumThreads(&self, ppEnum: *mut *mut ICorProfilerThreadEnum) -> HRESULT; + + pub fn InitializeCurrentThread(&self) -> HRESULT; + + pub fn RequestReJIT(&self, + cFunctions: ULONG, + moduleIds: *const ModuleID, + methodIds: *const mdMethodDef, + ) -> HRESULT; + + pub fn RequestRevert(&self, + cFunctions: ULONG, + moduleIds: *const ModuleID, + methodIds: *const mdMethodDef, + status: *mut HRESULT, + ) -> HRESULT; + + pub fn GetCodeInfo3(&self, + functionID: FunctionID, + reJitId: ReJITID, + cCodeInfos: ULONG32, + pcCodeInfos: *mut ULONG32, + codeInfos: *mut COR_PRF_CODE_INFO, + ) -> HRESULT; + + pub fn GetFunctionFromIP2(&self, + ip: LPCBYTE, + pFunctionId: *mut FunctionID, + pReJitId: *mut ReJITID, + ) -> HRESULT; + + pub fn GetReJITIDs(&self, + functionId: FunctionID, + cReJitIds: ULONG, + pcReJitIds: *mut ULONG, + reJitIds: *mut ReJITID, + ) -> HRESULT; + + pub fn GetILToNativeMapping2(&self, + functionId: FunctionID, + reJitId: ReJITID, + cMap: ULONG32, + pcMap: *mut ULONG32, + map: *mut COR_DEBUG_IL_TO_NATIVE_MAP, + ) -> HRESULT; + + pub fn EnumJITedFunctions2(&self, ppEnum: *mut *mut ICorProfilerFunctionEnum) -> HRESULT; + + pub fn GetObjectSize2(&self, objectId: ObjectID, pcSize: *mut SIZE_T) -> HRESULT; + } + + #[uuid("07602928-CE38-4B83-81E7-74ADAF781214")] + pub unsafe interface ICorProfilerInfo5: ICorProfilerInfo4 { + pub fn GetEventMask2(&self, pdwEventsLow: *mut DWORD, pdwEventsHigh: *mut DWORD) -> HRESULT; + pub fn SetEventMask2(&self, dwEventsLow: DWORD, dwEventsHigh: DWORD) -> HRESULT; + } + + #[uuid("F30A070D-BFFB-46A7-B1D8-8781EF7B698A")] + pub unsafe interface ICorProfilerInfo6: ICorProfilerInfo5 { + pub fn EnumNgenModuleMethodsInliningThisMethod(&self, + inlinersModuleId: ModuleID, + inlineeModuleId: ModuleID, + inlineeMethodId: mdMethodDef, + incompleteData: *mut BOOL, + ppEnum: *mut *mut ICorProfilerMethodEnum) -> HRESULT; + } + + #[uuid("9AEECC0D-63E0-4187-8C00-E312F503F663")] + pub unsafe interface ICorProfilerInfo7: ICorProfilerInfo6 { + pub fn ApplyMetaData(&self, moduleId: ModuleID) -> HRESULT; + pub fn GetInMemorySymbolsLength(&self, + moduleId: ModuleID, + pCountSymbolBytes: *mut DWORD) -> HRESULT; + pub fn ReadInMemorySymbols(&self, + moduleId: ModuleID, + symbolsReadOffset: DWORD, + pSymbolBytes: *mut BYTE, + countSymbolBytes: DWORD, + pCountSymbolBytesRead: *mut DWORD) -> HRESULT; + } + + #[uuid("C5AC80A6-782E-4716-8044-39598C60CFBF")] + pub unsafe interface ICorProfilerInfo8: ICorProfilerInfo7 { + /// Determines if a function has associated metadata. + /// + /// Certain methods like IL Stubs or LCG Methods do not have + /// associated metadata that can be retrieved using the IMetaDataImport APIs. + /// + /// Such methods can be encountered by profilers through instruction pointers + /// or by listening to ICorProfilerCallback::DynamicMethodJITCompilationStarted. + /// + /// This API can be used to determine whether a FunctionID is dynamic. + pub fn IsFunctionDynamic(&self, + functionId: FunctionID, + isDynamic: *mut BOOL) -> HRESULT; + + /// Maps a managed code instruction pointer to a FunctionID. + /// + /// GetFunctionFromIP2 fails for dynamic methods, this method works for + /// both dynamic and non-dynamic methods. It is a superset of GetFunctionFromIP2 + pub fn GetFunctionFromIP3(&self, + ip: LPCBYTE, + functionId: *mut FunctionID, + pReJitId: *mut ReJITID) -> HRESULT; + + /// Retrieves information about dynamic methods + /// + /// Certain methods like IL Stubs or LCG do not have + /// associated metadata that can be retrieved using the IMetaDataImport APIs. + /// + /// Such methods can be encountered by profilers through instruction pointers + /// or by listening to ICorProfilerCallback::DynamicMethodJITCompilationStarted + /// + /// This API can be used to retrieve information about dynamic methods + /// including a friendly name if available. + /// + pub fn GetDynamicFunctionInfo(&self, + functionId: FunctionID, + moduleId: *mut ModuleID, + ppvSig: *mut PCCOR_SIGNATURE, + pbSig: *mut ULONG, + cchName: ULONG, + pcchName: *mut ULONG, + wszName: *mut WCHAR) -> HRESULT; + } + + #[uuid("008170DB-F8CC-4796-9A51-DC8AA0B47012")] + pub unsafe interface ICorProfilerInfo9: ICorProfilerInfo8 { + /// Given functionId + rejitId, enumerate the native code start address of all + /// jitted versions of this code that currently exist + pub fn GetNativeCodeStartAddresses(&self, + functionID: FunctionID, + reJitId: ReJITID, + cCodeStartAddresses: ULONG32, + pcCodeStartAddresses: *mut ULONG32, + codeStartAddresses: *mut UINT_PTR) -> HRESULT; + + /// Given the native code start address, + /// return the native->IL mapping information for this jitted version of the code + pub fn GetILToNativeMapping3(&self, + pNativeCodeStartAddress: UINT_PTR, + cMap: ULONG32, + pcMap: *mut ULONG32, + map: *mut COR_DEBUG_IL_TO_NATIVE_MAP) -> HRESULT; + + /// Given the native code start address, return the the blocks of virtual memory + /// that store this code (method code is not necessarily stored in a single contiguous memory region) + pub fn GetCodeInfo4(&self, + pNativeCodeStartAddress: UINT_PTR, + cCodeInfos: ULONG32, + pcCodeInfos: *mut ULONG32, + codeInfos: *mut COR_PRF_CODE_INFO) -> HRESULT; + } + + #[uuid("2F1B5152-C869-40C9-AA5F-3ABE026BD720")] + pub unsafe interface ICorProfilerInfo10: ICorProfilerInfo9 { + /// Given an ObjectID, callback and clientData, enumerates each object reference (if any). + pub fn EnumerateObjectReferences(&self, + objectId: ObjectID, + callback: *const ObjectReferenceCallback, + clientData: *const c_void) -> HRESULT; + + /// Given an ObjectID, determines whether it is in a read only segment. + pub fn IsFrozenObject(&self, objectId: ObjectID, pbFrozen: *mut BOOL) -> HRESULT; + + /// Gets the value of the configured LOH Threshold. + pub fn GetLOHObjectSizeThreshold(&self, pThreshold: *mut DWORD) -> HRESULT; + + /// This method will ReJIT the methods requested, as well as any inliners + /// of the methods requested. + /// + /// RequestReJIT does not do any tracking of inlined methods. The profiler + /// was expected to track inlining and call RequestReJIT for all inliners + /// to make sure every instance of an inlined method was ReJITted. + /// This poses a problem with ReJIT on attach, since the profiler was + /// not present to monitor inlining. This method can be called to guarantee + /// that the full set of inliners will be ReJITted as well. + /// + pub fn RequestReJITWithInliners(&self, + dwRejitFlags: DWORD, + cFunctions: ULONG, + moduleIds: *const ModuleID, + methodIds: *const mdMethodDef) -> HRESULT; + + /// Suspend the runtime without performing a GC. + pub fn SuspendRuntime(&self) -> HRESULT; + + /// Restart the runtime from a previous suspension. + pub fn ResumeRuntime(&self) -> HRESULT; + } + + #[uuid("06398876-8987-4154-B621-40A00D6E4D04")] + pub unsafe interface ICorProfilerInfo11: ICorProfilerInfo10 { + /// Get environment variable for the running managed code. + pub fn GetEnvironmentVariable(&self, + szName: *const WCHAR, + cchValue: ULONG, + pcchValue: *mut ULONG, + szValue: *mut WCHAR) -> HRESULT; + + /// Set environment variable for the running managed code. + /// + /// The code profiler calls this function to modify environment variables of the + /// current managed process. For example, it can be used in the profiler's Initialize() + /// or InitializeForAttach() callbacks. + /// + /// szName is the name of the environment variable, should not be NULL. + /// + /// szValue is the contents of the environment variable, or NULL if the variable should be deleted. + pub fn SetEnvironmentVariable(&self, + szName: *const WCHAR, + szValue: *const WCHAR) -> HRESULT; + } +} + +/// Rust abstractions over COM functions +impl ICorProfilerInfo { + fn get_class_from_object(&self, object_id: ObjectID) -> Result { + let mut class_id = MaybeUninit::uninit(); + let hr = unsafe { self.GetClassFromObject(object_id, class_id.as_mut_ptr()) }; + match hr { + S_OK => { + let class_id = unsafe { class_id.assume_init() }; + Ok(class_id) + } + _ => Err(hr), + } + } + pub fn get_event_mask(&self) -> Result { + let mut events = MaybeUninit::uninit(); + let hr = unsafe { self.GetEventMask(events.as_mut_ptr()) }; + match hr { + S_OK => { + let events = unsafe { events.assume_init() }; + Ok(COR_PRF_MONITOR::from_bits(events).unwrap()) + } + _ => Err(hr), + } + } + pub fn get_function_from_ip(&self, ip: LPCBYTE) -> Result { + let mut function_id = MaybeUninit::uninit(); + let hr = unsafe { self.GetFunctionFromIP(ip, function_id.as_mut_ptr()) }; + match hr { + S_OK => { + let function_id = unsafe { function_id.assume_init() }; + Ok(function_id) + } + _ => Err(hr), + } + } + pub fn get_handle_from_thread(&self, thread_id: ThreadID) -> Result { + let mut handle = MaybeUninit::uninit(); + let hr = unsafe { self.GetHandleFromThread(thread_id, handle.as_mut_ptr()) }; + match hr { + S_OK => { + let handle = unsafe { handle.assume_init() }; + Ok(handle) + } + _ => Err(hr), + } + } + pub fn is_array_class(&self, class_id: ClassID) -> Result { + let mut element_type = MaybeUninit::uninit(); + let mut element_class_id = MaybeUninit::uninit(); + let mut rank = MaybeUninit::uninit(); + let hr = unsafe { + self.IsArrayClass( + class_id, + element_type.as_mut_ptr(), + element_class_id.as_mut_ptr(), + rank.as_mut_ptr(), + ) + }; + match hr { + S_OK => { + let element_type = unsafe { element_type.assume_init() }; + let element_class_id = unsafe { + if !element_class_id.as_ptr().is_null() { + Some(element_class_id.assume_init()) + } else { + None + } + }; + let rank = unsafe { rank.assume_init() }; + Ok(ArrayClassInfo { + element_type, + element_class_id, + rank, + }) + } + _ => Err(hr), + } + } + pub fn get_thread_info(&self, thread_id: ThreadID) -> Result { + let mut win_32_thread_id = MaybeUninit::uninit(); + let hr = unsafe { self.GetThreadInfo(thread_id, win_32_thread_id.as_mut_ptr()) }; + match hr { + S_OK => { + let win_32_thread_id = unsafe { win_32_thread_id.assume_init() }; + Ok(win_32_thread_id) + } + _ => Err(hr), + } + } + pub fn get_current_thread_id(&self) -> Result { + let mut thread_id = MaybeUninit::uninit(); + let hr = unsafe { self.GetCurrentThreadID(thread_id.as_mut_ptr()) }; + match hr { + S_OK => { + let thread_id = unsafe { thread_id.assume_init() }; + Ok(thread_id) + } + _ => Err(hr), + } + } + pub fn get_class_id_info(&self, class_id: ClassID) -> Result { + let mut module_id = MaybeUninit::uninit(); + let mut token = MaybeUninit::uninit(); + let hr = + unsafe { self.GetClassIDInfo(class_id, module_id.as_mut_ptr(), token.as_mut_ptr()) }; + match hr { + S_OK => { + let module_id = unsafe { module_id.assume_init() }; + let token = unsafe { token.assume_init() }; + Ok(ClassInfo { module_id, token }) + } + _ => Err(hr), + } + } + pub fn get_function_info(&self, function_id: FunctionID) -> Result { + let mut class_id = MaybeUninit::uninit(); + let mut module_id = MaybeUninit::uninit(); + let mut token = MaybeUninit::uninit(); + let hr = unsafe { + self.GetFunctionInfo( + function_id, + class_id.as_mut_ptr(), + module_id.as_mut_ptr(), + token.as_mut_ptr(), + ) + }; + match hr { + S_OK => { + let class_id = unsafe { class_id.assume_init() }; + let module_id = unsafe { module_id.assume_init() }; + let token = unsafe { token.assume_init() }; + Ok(FunctionInfo { + class_id, + module_id, + token, + }) + } + _ => Err(hr), + } + } + pub fn set_event_mask(&self, events: COR_PRF_MONITOR) -> Result<(), HRESULT> { + let events = events.bits(); + let hr = unsafe { self.SetEventMask(events) }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } + pub fn set_enter_leave_function_hooks( + &self, + func_enter: FunctionEnter, + func_leave: FunctionLeave, + func_tailcall: FunctionTailcall, + ) -> Result<(), HRESULT> { + let func_enter = func_enter as *const FunctionEnter; + let func_leave = func_leave as *const FunctionLeave; + let func_tailcall = func_tailcall as *const FunctionTailcall; + let hr = unsafe { self.SetEnterLeaveFunctionHooks(func_enter, func_leave, func_tailcall) }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } + pub fn set_function_id_mapper(&self, func: FunctionIDMapper) -> Result<(), HRESULT> { + let func = func as *const FunctionIDMapper; + let hr = unsafe { self.SetFunctionIDMapper(func) }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } + pub fn get_token_and_metadata_from_function( + &self, + function_id: FunctionID, + ) -> Result, HRESULT> { + let mut unknown = None; + let mut token = mdTokenNil; + let hr = unsafe { + self.GetTokenAndMetaDataFromFunction( + function_id, + &I::IID as REFIID, + &mut unknown as *mut _ as *mut *mut IUnknown, + &mut token, + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + Ok(FunctionTokenAndMetadata { + metadata: unknown.unwrap(), + token, + }) + } + pub fn get_module_info(&self, module_id: ModuleID) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + unsafe { + self.GetModuleInfo( + module_id, + ptr::null_mut(), + 0, + name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + let mut base_load_address = MaybeUninit::uninit(); + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + let mut name_length = MaybeUninit::uninit(); + let mut assembly_id = MaybeUninit::uninit(); + let hr = unsafe { + self.GetModuleInfo( + module_id, + base_load_address.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + name_buffer.as_mut_ptr(), + assembly_id.as_mut_ptr(), + ) + }; + match hr { + S_OK => { + let base_load_address = unsafe { base_load_address.assume_init() }; + let file_name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + let assembly_id = unsafe { assembly_id.assume_init() }; + Ok(ModuleInfo { + base_load_address, + file_name, + assembly_id, + }) + } + _ => Err(hr), + } + } + pub fn get_module_metadata( + &self, + module_id: ModuleID, + open_flags: CorOpenFlags, + ) -> Result { + let mut unknown = None; + let hr = unsafe { + self.GetModuleMetaData( + module_id, + open_flags.bits(), + &I::IID as REFIID, + &mut unknown as *mut _ as *mut *mut IUnknown, + ) + }; + + if FAILED(hr) { + log::error!( + "error fetching metadata for module_id {}, HRESULT: {:X}", + module_id, + hr + ); + return Err(hr); + } + Ok(unknown.unwrap()) + } + + /// Gets a pointer to the body of a method in Microsoft intermediate language (MSIL) code, + /// starting at its header. + pub fn get_il_function_body( + &self, + module_id: ModuleID, + method_id: mdMethodDef, + ) -> Result { + let mut method_header = MaybeUninit::uninit(); + let mut method_size = 0; + let hr = unsafe { + self.GetILFunctionBody( + module_id, + method_id, + method_header.as_mut_ptr(), + &mut method_size, + ) + }; + + match hr { + S_OK => { + let method_header = unsafe { method_header.assume_init() }; + Ok(IlFunctionBody { + method_header, + method_size, + }) + } + _ => Err(hr), + } + } + pub fn get_il_function_body_allocator( + &self, + module_id: ModuleID, + ) -> Result { + let mut malloc = None; + let hr = unsafe { + self.GetILFunctionBodyAllocator( + module_id, + &mut malloc as *mut _ as *mut *mut IMethodMalloc, + ) + }; + match hr { + S_OK => Ok(malloc.unwrap()), + _ => Err(hr), + } + } + pub fn set_il_function_body( + &self, + module_id: ModuleID, + method_id: mdMethodDef, + new_il_method_header: LPCBYTE, + ) -> Result<(), HRESULT> { + let hr = unsafe { self.SetILFunctionBody(module_id, method_id, new_il_method_header) }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } + pub fn get_app_domain_info( + &self, + app_domain_id: AppDomainID, + ) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + unsafe { + self.GetAppDomainInfo( + app_domain_id, + 0, + name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + let mut name_length = MaybeUninit::uninit(); + let mut process_id = MaybeUninit::uninit(); + let hr = unsafe { + self.GetAppDomainInfo( + app_domain_id, + name_buffer_length, + name_length.as_mut_ptr(), + name_buffer.as_mut_ptr(), + process_id.as_mut_ptr(), + ) + }; + match hr { + S_OK => { + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + let process_id = unsafe { process_id.assume_init() }; + Ok(AppDomainInfo { name, process_id }) + } + _ => Err(hr), + } + } + pub fn get_assembly_info(&self, assembly_id: AssemblyID) -> Result { + let mut name_buffer = Vec::::with_capacity(MAX_LENGTH as usize); + let mut name_length = 0; + let mut app_domain_id = MaybeUninit::uninit(); + let mut module_id = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetAssemblyInfo( + assembly_id, + MAX_LENGTH, + &mut name_length, + name_buffer.as_mut_ptr(), + app_domain_id.as_mut_ptr(), + module_id.as_mut_ptr(), + ) + }; + + match hr { + S_OK => { + unsafe { name_buffer.set_len(name_length as usize) }; + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + let app_domain_id = unsafe { app_domain_id.assume_init() }; + let module_id = unsafe { module_id.assume_init() }; + Ok(AssemblyInfo { + name, + module_id, + app_domain_id, + }) + } + _ => Err(hr), + } + } + pub fn force_gc(&self) -> Result<(), HRESULT> { + let hr = unsafe { self.ForceGC() }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } + pub fn set_il_instrumented_code_map( + &self, + function_id: FunctionID, + start_jit: bool, + il_map_entries: &[COR_IL_MAP], + ) -> Result<(), HRESULT> { + let start_jit: BOOL = if start_jit { 1 } else { 0 }; + let hr = unsafe { + self.SetILInstrumentedCodeMap( + function_id, + start_jit, + il_map_entries.len() as ULONG, + il_map_entries.as_ptr(), + ) + }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } + pub fn get_thread_context(&self, thread_id: ThreadID) -> Result { + let mut context_id = MaybeUninit::uninit(); + let hr = unsafe { self.GetThreadContext(thread_id, context_id.as_mut_ptr()) }; + + match hr { + S_OK => { + let context_id = unsafe { context_id.assume_init() }; + Ok(context_id) + } + _ => Err(hr), + } + } + pub fn get_il_to_native_mapping( + &self, + function_id: FunctionID, + ) -> Result, HRESULT> { + let mut map_buffer_length = MaybeUninit::uninit(); + unsafe { + self.GetILToNativeMapping( + function_id, + 0, + map_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ) + }; + + let map_buffer_length = unsafe { map_buffer_length.assume_init() }; + let mut map = Vec::::with_capacity(map_buffer_length as usize); + unsafe { map.set_len(map_buffer_length as usize) }; + let mut map_length = MaybeUninit::uninit(); + let hr = unsafe { + self.GetILToNativeMapping( + function_id, + map_buffer_length, + map_length.as_mut_ptr(), + map.as_mut_ptr(), + ) + }; + match hr { + S_OK => Ok(map), + _ => Err(hr), + } + } +} + +impl ICorProfilerInfo3 { + pub fn get_module_info_2(&self, module_id: ModuleID) -> Result { + let mut file_name_buffer_length = MaybeUninit::uninit(); + unsafe { + self.GetModuleInfo2( + module_id, + ptr::null_mut(), + 0, + file_name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + let file_name_buffer_length = unsafe { file_name_buffer_length.assume_init() }; + let mut file_name_buffer = Vec::::with_capacity(file_name_buffer_length as usize); + unsafe { file_name_buffer.set_len(file_name_buffer_length as usize) }; + + let mut base_load_address = MaybeUninit::uninit(); + let mut file_name_length = MaybeUninit::uninit(); + let mut assembly_id = MaybeUninit::uninit(); + let mut module_flags = MaybeUninit::uninit(); + let hr = unsafe { + self.GetModuleInfo2( + module_id, + base_load_address.as_mut_ptr(), + file_name_buffer_length, + file_name_length.as_mut_ptr(), + file_name_buffer.as_mut_ptr(), + assembly_id.as_mut_ptr(), + module_flags.as_mut_ptr(), + ) + }; + + match hr { + S_OK => { + let base_load_address = unsafe { base_load_address.assume_init() }; + let assembly_id = unsafe { assembly_id.assume_init() }; + let module_flags = unsafe { module_flags.assume_init() }; + let module_flags = COR_PRF_MODULE_FLAGS::from_bits(module_flags).unwrap(); + let file_name = U16CString::from_vec_with_nul(file_name_buffer) + .unwrap() + .to_string_lossy(); + Ok(ModuleInfo2 { + base_load_address, + file_name, + assembly_id, + module_flags, + }) + } + _ => Err(hr), + } + } + + pub fn get_runtime_information(&self) -> Result { + let mut version_string_buffer_length = MaybeUninit::uninit(); + unsafe { + self.GetRuntimeInformation( + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + 0, + version_string_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ) + }; + + let version_string_buffer_length = unsafe { version_string_buffer_length.assume_init() }; + let mut version_string_buffer = + Vec::::with_capacity(version_string_buffer_length as usize); + unsafe { version_string_buffer.set_len(version_string_buffer_length as usize) }; + + let mut clr_instance_id = MaybeUninit::uninit(); + let mut runtime_type = MaybeUninit::uninit(); + let mut major_version = MaybeUninit::uninit(); + let mut minor_version = MaybeUninit::uninit(); + let mut build_number = MaybeUninit::uninit(); + let mut qfe_version = MaybeUninit::uninit(); + let mut version_string_length = MaybeUninit::uninit(); + let hr = unsafe { + self.GetRuntimeInformation( + clr_instance_id.as_mut_ptr(), + runtime_type.as_mut_ptr(), + major_version.as_mut_ptr(), + minor_version.as_mut_ptr(), + build_number.as_mut_ptr(), + qfe_version.as_mut_ptr(), + version_string_buffer_length, + version_string_length.as_mut_ptr(), + version_string_buffer.as_mut_ptr(), + ) + }; + + match hr { + S_OK => { + let clr_instance_id = unsafe { clr_instance_id.assume_init() }; + let runtime_type = unsafe { runtime_type.assume_init() }; + let major_version = unsafe { major_version.assume_init() }; + let minor_version = unsafe { minor_version.assume_init() }; + let build_number = unsafe { build_number.assume_init() }; + let qfe_version = unsafe { qfe_version.assume_init() }; + let version_string = U16CString::from_vec_with_nul(version_string_buffer) + .unwrap() + .to_string_lossy(); + Ok(RuntimeInfo { + clr_instance_id, + runtime_type, + major_version, + minor_version, + build_number, + qfe_version, + version_string, + }) + } + _ => Err(hr), + } + } +} + +impl ICorProfilerInfo4 { + pub fn initialize_current_thread(&self) -> Result<(), HRESULT> { + let hr = unsafe { self.InitializeCurrentThread() }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } + + pub fn request_rejit( + &self, + module_ids: &[ModuleID], + method_ids: &[mdMethodDef], + ) -> Result<(), HRESULT> { + let len = method_ids.len() as ULONG; + let hr = unsafe { self.RequestReJIT(len, module_ids.as_ptr(), method_ids.as_ptr()) }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } +} + +// allow it to be moved to another thread for rejitting. +// We know this is safe to perform, but compiler doesn't +unsafe impl Send for ICorProfilerInfo4 {} + +impl ICorProfilerInfo5 { + pub fn set_event_mask2( + &self, + events_low: COR_PRF_MONITOR, + events_high: COR_PRF_HIGH_MONITOR, + ) -> Result<(), HRESULT> { + let events_low = events_low.bits(); + let events_high = events_high.bits(); + let hr = unsafe { self.SetEventMask2(events_low, events_high) }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } +} + +impl ICorProfilerInfo7 { + pub fn apply_metadata(&self, module_id: ModuleID) -> Result<(), HRESULT> { + let hr = unsafe { self.ApplyMetaData(module_id) }; + match hr { + S_OK => Ok(()), + _ => Err(hr), + } + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_method_enum.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_method_enum.rs new file mode 100644 index 000000000..ff60e8b5e --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_method_enum.rs @@ -0,0 +1,24 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{interfaces::iunknown::IUnknown, sys::HRESULT}; + +interfaces! { + /// Provides methods to sequentially iterate through a collection of modules loaded by + /// the application or the profiler. + #[uuid("FCCEE788-0088-454B-A811-C99F298D1942")] + pub unsafe interface ICorProfilerMethodEnum: IUnknown { + pub unsafe fn Skip(&self, celt: ULONG) -> HRESULT; + pub unsafe fn Reset(&self) -> HRESULT; + pub unsafe fn Clone(&self, ppEnum: *mut *mut ICorProfilerMethodEnum) -> HRESULT; + pub unsafe fn GetCount(&self, pcelt: *mut ULONG) -> HRESULT; + pub unsafe fn Next(&self, + celt: ULONG, + elements: *mut COR_PRF_METHOD, + pceltFetched: *mut ULONG, + ) -> HRESULT; + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_module_enum.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_module_enum.rs new file mode 100644 index 000000000..03e4f50d1 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_module_enum.rs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{interfaces, interfaces::IUnknown, sys::HRESULT}; + +interfaces! { + #[uuid("B0266D75-2081-4493-AF7F-028BA34DB891")] + pub unsafe interface ICorProfilerModuleEnum: IUnknown { + pub unsafe fn Skip(&self, celt: ULONG) -> HRESULT; + pub unsafe fn Reset(&self) -> HRESULT; + pub unsafe fn Clone(&self, ppEnum: *mut *mut ICorProfilerModuleEnum) -> HRESULT; + pub unsafe fn GetCount(&self, pcelt: *mut ULONG) -> HRESULT; + pub unsafe fn Next(&self, + celt: ULONG, + ids: *mut ModuleID, + pceltFetched: *mut ULONG, + ) -> HRESULT; + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_object_enum.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_object_enum.rs new file mode 100644 index 000000000..0042fedcb --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_object_enum.rs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{interfaces, interfaces::IUnknown, sys::HRESULT}; + +interfaces! { + #[uuid("2C6269BD-2D13-4321-AE12-6686365FD6AF")] + pub unsafe interface ICorProfilerObjectEnum: IUnknown { + pub fn Skip(&self, celt: ULONG) -> HRESULT; + pub fn Reset(&self) -> HRESULT; + pub fn Clone(&self, ppEnum: *mut *mut ICorProfilerObjectEnum) -> HRESULT; + pub fn GetCount(&self, pcelt: *mut ULONG) -> HRESULT; + pub fn Next(&self, + celt: ULONG, + objects: *mut ObjectID, + pceltFetched: *mut ULONG, + ) -> HRESULT; + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/icor_profiler_thread_enum.rs b/src/elastic_apm_profiler/src/interfaces/icor_profiler_thread_enum.rs new file mode 100644 index 000000000..464a9e2a5 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/icor_profiler_thread_enum.rs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{interfaces::IUnknown, sys::HRESULT}; + +com::interfaces! { + #[uuid("571194F7-25ED-419F-AA8B-7016B3159701")] + pub unsafe interface ICorProfilerThreadEnum: IUnknown { + pub unsafe fn Skip(&self, celt: ULONG) -> HRESULT; + pub unsafe fn Reset(&self) -> HRESULT; + pub unsafe fn Clone(&self, ppEnum: *mut *mut ICorProfilerThreadEnum) -> HRESULT; + pub unsafe fn GetCount(&self, pcelt: *mut ULONG) -> HRESULT; + pub unsafe fn Next(&self, + celt: ULONG, + ids: *mut ThreadID, + pceltFetched: *mut ULONG, + ) -> HRESULT; + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/imap_token.rs b/src/elastic_apm_profiler/src/interfaces/imap_token.rs new file mode 100644 index 000000000..e0df894ae --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/imap_token.rs @@ -0,0 +1,14 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::mdToken; +use com::{interfaces::iunknown::IUnknown, sys::HRESULT}; + +interfaces! { + #[uuid("06A3EA8B-0225-11d1-BF72-00C04FC31E12")] + pub unsafe interface IMapToken: IUnknown { + fn Map(&self, tkImp: mdToken, tkEmit: mdToken) -> HRESULT; + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/imetadata_assembly_emit.rs b/src/elastic_apm_profiler/src/interfaces/imetadata_assembly_emit.rs new file mode 100644 index 000000000..00fe75ea9 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/imetadata_assembly_emit.rs @@ -0,0 +1,135 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{ + interfaces::iunknown::IUnknown, + sys::{FAILED, HRESULT}, +}; + +use std::{ffi::c_void, ptr}; +use widestring::U16CString; + +interfaces! { + #[uuid("211EF15B-5317-4438-B196-DEC87B887693")] + pub unsafe interface IMetaDataAssemblyEmit: IUnknown { + pub unsafe fn DefineAssembly(&self, + pbPublicKey: *const c_void, + cbPublicKey: ULONG, + ulHashAlgId: ULONG, + szName: LPCWSTR, + pMetaData: *const ASSEMBLYMETADATA, + dwAssemblyFlags: DWORD, + pmda: *mut mdAssembly, + ) -> HRESULT; + pub unsafe fn DefineAssemblyRef(&self, + pbPublicKeyOrToken: *const c_void, + cbPublicKeyOrToken: ULONG, + szName: LPCWSTR, + pMetaData: *const ASSEMBLYMETADATA, + pbHashValue: *const c_void, + cbHashValue: ULONG, + dwAssemblyRefFlags: DWORD, + pmdar: *mut mdAssemblyRef, + ) -> HRESULT; + pub unsafe fn DefineExportedType(&self, + szName: LPCWSTR, + tkImplementation: mdToken, + tkTypeDef: mdTypeDef, + dwExportedTypeFlags: DWORD, + pmdct: *mut mdExportedType, + ) -> HRESULT; + pub unsafe fn DefineManifestResource(&self, + szName: LPCWSTR, + tkImplementation: mdToken, + dwOffset: DWORD, + dwResourceFlags: DWORD, + pmdmr: *mut mdManifestResource, + ) -> HRESULT; + pub unsafe fn SetAssemblyProps(&self, + pma: mdAssembly, + pbPublicKey: *const c_void, + cbPublicKey: ULONG, + ulHashAlgId: ULONG, + szName: LPCWSTR, + pMetaData: *const ASSEMBLYMETADATA, + dwAssemblyFlags: DWORD, + ) -> HRESULT; + pub unsafe fn SetAssemblyRefProps(&self, + ar: mdAssemblyRef, + pbPublicKeyOrToken: *const c_void, + cbPublicKeyOrToken: ULONG, + szName: LPCWSTR, + pMetaData: *const ASSEMBLYMETADATA, + pbHashValue: *const c_void, + cbHashValue: ULONG, + dwAssemblyRefFlags: DWORD, + ) -> HRESULT; + pub unsafe fn SetFileProps(&self, + file: mdFile, + pbHashValue: *const c_void, + cbHashValue: ULONG, + dwFileFlags: DWORD, + ) -> HRESULT; + pub unsafe fn SetExportedTypeProps(&self, + ct: mdExportedType, + tkImplementation: mdToken, + tkTypeDef: mdTypeDef, + dwExportedTypeFlags: DWORD, + ) -> HRESULT; + pub unsafe fn SetManifestResourceProps(&self, + mr: mdManifestResource, + tkImplementation: mdToken, + dwOffset: DWORD, + dwResourceFlags: DWORD, + ) -> HRESULT; + } +} + +impl IMetaDataAssemblyEmit { + /// Creates an AssemblyRef structure containing metadata for the assembly that this + /// assembly references, and returns the associated metadata token. + pub fn define_assembly_ref( + &self, + public_key: &[u8], + name: &str, + assembly_metadata: &ASSEMBLYMETADATA, + hash: &[u8], + assembly_ref_flags: CorAssemblyFlags, + ) -> Result { + let key_ptr = public_key.as_ptr(); + let key_len = public_key.len() as ULONG; + let wstr = U16CString::from_str(name).unwrap(); + let hash_ptr = if hash.is_empty() { + ptr::null() + } else { + hash.as_ptr() + }; + let hash_len = hash.len() as ULONG; + let mut assembly_ref = mdAssemblyRefNil; + let hr = unsafe { + self.DefineAssemblyRef( + key_ptr as *const c_void, + key_len, + wstr.as_ptr(), + assembly_metadata as *const _, + hash_ptr as *const c_void, + hash_len, + assembly_ref_flags.bits(), + &mut assembly_ref, + ) + }; + if FAILED(hr) { + log::error!( + "define assembly ref '{}' failed. HRESULT: {} {:X}", + name, + hr, + hr + ); + return Err(hr); + } + Ok(assembly_ref) + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/imetadata_assembly_import.rs b/src/elastic_apm_profiler/src/interfaces/imetadata_assembly_import.rs new file mode 100644 index 000000000..f5eabf6c2 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/imetadata_assembly_import.rs @@ -0,0 +1,386 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use core::slice; +use std::{ffi::c_void, mem::MaybeUninit, ptr}; + +use com::{ + interfaces::iunknown::IUnknown, + sys::{FAILED, HRESULT, S_OK}, +}; +use widestring::U16CString; + +use crate::{ + cil::MAX_LENGTH, + ffi::*, + profiler::types::{AssemblyMetaData, PublicKey, Version}, +}; + +interfaces! { + #[uuid("EE62470B-E94B-424E-9B7C-2F00C9249F93")] + pub unsafe interface IMetaDataAssemblyImport: IUnknown { + pub unsafe fn GetAssemblyProps(&self, + mda: mdAssembly, + ppbPublicKey: *mut *mut c_void, + pcbPublicKey: *mut ULONG, + pulHashAlgId: *mut ULONG, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + pMetaData: *mut ASSEMBLYMETADATA, + pdwAssemblyFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetAssemblyRefProps(&self, + mdar: mdAssemblyRef, + ppbPublicKeyOrToken: *mut *mut c_void, + pcbPublicKeyOrToken: *mut ULONG, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + pMetaData: *mut ASSEMBLYMETADATA, + ppbHashValue: *mut *mut c_void, + pcbHashValue: *mut ULONG, + pdwAssemblyRefFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetFileProps(&self, + mdf: mdFile, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + ppbHashValue: *mut *mut c_void, + pcbHashValue: *mut ULONG, + pdwFileFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetExportedTypeProps(&self, + mdct: mdExportedType, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + ptkImplementation: *mut mdToken, + ptkTypeDef: *mut mdTypeDef, + pdwExportedTypeFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetManifestResourceProps(&self, + mdmr: mdManifestResource, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + ptkImplementation: *mut mdToken, + pdwOffset: *mut DWORD, + pdwResourceFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn EnumAssemblyRefs(&self, + phEnum: *mut HCORENUM, + rAssemblyRefs: *mut mdAssemblyRef, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumFiles(&self, + phEnum: *mut HCORENUM, + rFiles: *mut mdFile, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumExportedTypes(&self, + phEnum: *mut HCORENUM, + rExportedTypes: *mut mdExportedType, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumManifestResources(&self, + phEnum: *mut HCORENUM, + rManifestResources: *mut mdManifestResource, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetAssemblyFromScope(&self, ptkAssembly: *mut mdAssembly) -> HRESULT; + pub unsafe fn FindExportedTypeByName(&self, + szName: LPCWSTR, + mdtExportedType: mdToken, + ptkExportedType: *mut mdExportedType, + ) -> HRESULT; + pub unsafe fn FindManifestResourceByName(&self, + szName: LPCWSTR, + ptkManifestResource: *mut mdManifestResource, + ) -> HRESULT; + pub unsafe fn CloseEnum(&self, hEnum: HCORENUM); + pub unsafe fn FindAssembliesByName(&self, + szAppBase: LPCWSTR, + szPrivateBin: LPCWSTR, + szAssemblyName: LPCWSTR, + ppIUnk: *mut *mut IUnknown, + cMax: ULONG, + pcAssemblies: *mut ULONG, + ) -> HRESULT; + } +} + +impl IMetaDataAssemblyImport { + pub fn get_assembly_metadata(&self) -> Result { + let mut assembly_token = mdAssemblyNil; + let hr = unsafe { self.GetAssemblyFromScope(&mut assembly_token) }; + + if FAILED(hr) { + log::error!("error calling assembly from scope"); + return Err(hr); + } + + let mut name_buffer_length = MaybeUninit::uninit(); + let hr = unsafe { + self.GetAssemblyProps( + assembly_token, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + 0, + name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + + let mut name_length = MaybeUninit::uninit(); + // NOTE: null_mut() default values on ASSEMBLYMETADATA will not populated. + // This is not an issue now, but would be if AssemblyMetaData were to expose these values + let mut assembly_metadata = ASSEMBLYMETADATA::default(); + + let mut sz_locale: *mut WCHAR = ptr::null_mut(); + assembly_metadata.szLocale = (&mut sz_locale as *mut *mut WCHAR) as *mut WCHAR; + + let mut assembly_flags = MaybeUninit::uninit(); + let mut hash_algorithm = MaybeUninit::uninit(); + let mut public_key = MaybeUninit::uninit(); + let mut public_key_length = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetAssemblyProps( + assembly_token, + public_key.as_mut_ptr(), + public_key_length.as_mut_ptr(), + hash_algorithm.as_mut_ptr(), + name_buffer.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + &mut assembly_metadata, + assembly_flags.as_mut_ptr(), + ) + }; + + match hr { + S_OK => { + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + + let public_key = unsafe { + let l = public_key_length.assume_init(); + let p = public_key.assume_init(); + if l == 0 { + Vec::new() + } else { + slice::from_raw_parts(p as *const u8, l as usize).to_vec() + } + }; + + let hash_algorithm = unsafe { hash_algorithm.assume_init() }; + let assembly_flags = unsafe { + let a = assembly_flags.assume_init(); + CorAssemblyFlags::from_bits(a).unwrap() + }; + + let locale = unsafe { + U16CString::from_ptr( + assembly_metadata.szLocale, + assembly_metadata.cbLocale as usize, + ) + .unwrap() + .to_string_lossy() + }; + + Ok(AssemblyMetaData { + name, + locale, + assembly_token, + public_key: PublicKey::new(public_key, hash_algorithm), + version: Version::new( + assembly_metadata.usMajorVersion, + assembly_metadata.usMinorVersion, + assembly_metadata.usBuildNumber, + assembly_metadata.usRevisionNumber, + ), + assembly_flags, + }) + } + _ => Err(hr), + } + } + + pub fn enum_assembly_refs(&self) -> Result, HRESULT> { + let mut en = ptr::null_mut() as HCORENUM; + let max = 256; + let mut assembly_refs = Vec::with_capacity(max as usize); + let mut assembly_len = MaybeUninit::uninit(); + + let hr = unsafe { + self.EnumAssemblyRefs( + &mut en, + assembly_refs.as_mut_ptr(), + max, + assembly_len.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let len = unsafe { + let len = assembly_len.assume_init(); + assembly_refs.set_len(len as usize); + len + }; + + // no more assembly refs + if len < max { + unsafe { + self.CloseEnum(en); + } + return Ok(assembly_refs); + } + + let mut all_assembly_refs = assembly_refs; + loop { + assembly_refs = Vec::with_capacity(max as usize); + assembly_len = MaybeUninit::uninit(); + let hr = unsafe { + self.EnumAssemblyRefs( + &mut en, + assembly_refs.as_mut_ptr(), + max, + assembly_len.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let len = unsafe { + let len = assembly_len.assume_init(); + assembly_refs.set_len(len as usize); + len + }; + all_assembly_refs.append(&mut assembly_refs); + if len < max { + break; + } + } + + unsafe { + self.CloseEnum(en); + } + Ok(all_assembly_refs) + } + + pub fn get_referenced_assembly_metadata( + &self, + assembly_ref: mdAssemblyRef, + ) -> Result { + let mut name_buffer = Vec::::with_capacity(MAX_LENGTH as usize); + let mut name_length = 0; + let mut public_key = MaybeUninit::uninit(); + // NOTE: null_mut() default values on ASSEMBLYMETADATA will not populated. + // This is not an issue now, but would be if AssemblyMetaData were to expose these values + let mut assembly_metadata = ASSEMBLYMETADATA::default(); + let mut sz_locale: *mut WCHAR = ptr::null_mut(); + assembly_metadata.szLocale = (&mut sz_locale as *mut *mut WCHAR) as *mut WCHAR; + + let mut public_key_length = 0; + let mut assembly_flags = 0; + + let hr = unsafe { + self.GetAssemblyRefProps( + assembly_ref, + public_key.as_mut_ptr(), + &mut public_key_length, + name_buffer.as_mut_ptr(), + MAX_LENGTH, + &mut name_length, + &mut assembly_metadata, + ptr::null_mut(), + ptr::null_mut(), + &mut assembly_flags, + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + unsafe { name_buffer.set_len(name_length as usize) }; + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + + let public_key = unsafe { + let p = public_key.assume_init(); + if public_key_length == 0 { + Vec::new() + } else { + slice::from_raw_parts(p as *const u8, public_key_length as usize).to_vec() + } + }; + + let assembly_flags = CorAssemblyFlags::from_bits(assembly_flags).unwrap(); + + let locale = unsafe { + U16CString::from_ptr( + assembly_metadata.szLocale, + assembly_metadata.cbLocale as usize, + ) + .unwrap() + .to_string_lossy() + }; + + Ok(AssemblyMetaData { + name, + locale, + assembly_token: assembly_ref, + public_key: PublicKey::new(public_key, 32772), + version: Version::new( + assembly_metadata.usMajorVersion, + assembly_metadata.usMinorVersion, + assembly_metadata.usBuildNumber, + assembly_metadata.usRevisionNumber, + ), + assembly_flags, + }) + } + + // Other Rust abstractions + + pub fn find_assembly_ref(&self, name: &str) -> Option { + if let Ok(assembly_refs) = self.enum_assembly_refs() { + for assembly_ref in assembly_refs.into_iter() { + if let Ok(assembly_metadata) = self.get_referenced_assembly_metadata(assembly_ref) { + if assembly_metadata.name == name { + return Some(assembly_ref); + } + } + } + } + + None + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/imetadata_dispenser.rs b/src/elastic_apm_profiler/src/interfaces/imetadata_dispenser.rs new file mode 100644 index 000000000..ee9f9295f --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/imetadata_dispenser.rs @@ -0,0 +1,39 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::{DWORD, LPCVOID, LPCWSTR, REFCLSID, REFIID, ULONG}; +use com::{interfaces::iunknown::IUnknown, sys::HRESULT}; + +interfaces! { + /// Provides methods to create a new metadata scope, or open an existing one. + #[uuid("809C652E-7396-11D2-9771-00A0C9B4D50C")] + pub unsafe interface IMetaDataDispenser: IUnknown { + /// Creates a new area in memory in which you can create new metadata. + fn DefineScope( + &self, + rclsid: REFCLSID, // [in] What version to create. + dwCreateFlags: DWORD, // [in] Flags on the create. + riid: REFIID, // [in] The interface desired. + ppIUnk: *mut *mut IUnknown) -> HRESULT; // [out] Return interface on success. + + /// Opens an existing, on-disk file and maps its metadata into memory. + fn OpenScope( + &self, + szScope: LPCWSTR, // [in] The scope to open. + dwOpenFlags: DWORD, // [in] Open mode flags. + riid: REFIID, // [in] The interface desired. + ppIUnk: *mut *mut IUnknown) -> HRESULT; // [out] Return interface on success. + + /// Opens an area of memory that contains existing metadata. + /// That is, this method opens a specified area of memory in which the existing data is treated as metadata. + fn OpenScopeOnMemory( + &self, + pData: LPCVOID, // [in] Location of scope data. + cbData: ULONG, // [in] Size of the data pointed to by pData. + dwOpenFlags: DWORD, // [in] Open mode flags. + riid: REFIID, // [in] The interface desired. + ppIUnk: *mut *mut IUnknown) -> HRESULT; // [out] Return interface on success. + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/imetadata_emit.rs b/src/elastic_apm_profiler/src/interfaces/imetadata_emit.rs new file mode 100644 index 000000000..5833b5b13 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/imetadata_emit.rs @@ -0,0 +1,623 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + ffi::*, + interfaces::{ + IMapToken, IMetaDataAssemblyEmit, IMetaDataAssemblyImport, IMetaDataImport, IStream, + }, +}; +use com::{ + interfaces::iunknown::IUnknown, + sys::{FAILED, HRESULT}, +}; +use std::ffi::c_void; +use widestring::U16CString; + +interfaces! { + #[uuid("BA3FEE4C-ECB9-4E41-83B7-183FA41CD859")] + pub unsafe interface IMetaDataEmit: IUnknown { + pub unsafe fn SetModuleProps(&self, szName: LPCWSTR) -> HRESULT; + pub unsafe fn Save(&self, szName: LPCWSTR, dwSaveFlags: DWORD) -> HRESULT; + pub unsafe fn SaveToStream(&self, + pIStream: *const IStream, + dwSaveFlags: DWORD, + ) -> HRESULT; + pub unsafe fn GetSaveSize(&self, fSave: CorSaveSize, pdwSaveSize: *mut DWORD) -> HRESULT; + pub unsafe fn DefineTypeDef(&self, + szTypeDef: LPCWSTR, + dwTypeDefFlags: DWORD, + tkExtends: mdToken, + rtkImplements: *const mdToken, + ptd: *mut mdTypeDef, + ) -> HRESULT; + pub unsafe fn DefineNestedType(&self, + szTypeDef: LPCWSTR, + dwTypeDefFlags: DWORD, + tkExtends: mdToken, + rtkImplements: *const mdToken, + tdEncloser: mdTypeDef, + ptd: *mut mdTypeDef, + ) -> HRESULT; + pub unsafe fn SetHandler(&self, pUnk: *const IUnknown) -> HRESULT; + pub unsafe fn DefineMethod(&self, + td: mdTypeDef, + szName: LPCWSTR, + dwMethodFlags: DWORD, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + ulCodeRVA: ULONG, + dwImplFlags: DWORD, + pmd: *mut mdMethodDef, + ) -> HRESULT; + pub unsafe fn DefineMethodImpl(&self, + td: mdTypeDef, + tkBody: mdToken, + tkDecl: mdToken, + ) -> HRESULT; + pub unsafe fn DefineTypeRefByName(&self, + tkResolutionScope: mdToken, + szName: LPCWSTR, + ptr: *mut mdTypeRef, + ) -> HRESULT; + pub unsafe fn DefineImportType(&self, + pAssemImport: *const IMetaDataAssemblyImport, + pbHashValue: *const c_void, + cbHashValue: ULONG, + pImport: *const IMetaDataImport, + tdImport: mdTypeDef, + pAssemEmit: *const IMetaDataAssemblyEmit, + ptr: *mut mdTypeRef, + ) -> HRESULT; + pub unsafe fn DefineMemberRef(&self, + tkImport: mdToken, + szName: LPCWSTR, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + pmr: *mut mdMemberRef, + ) -> HRESULT; + pub unsafe fn DefineImportMember(&self, + pAssemImport: *const IMetaDataAssemblyImport, + pbHashValue: *const c_void, + cbHashValue: ULONG, + pImport: *const IMetaDataImport, + mbMember: mdToken, + pAssemEmit: *const IMetaDataAssemblyEmit, + tkParent: mdToken, + pmr: *mut mdMemberRef, + ) -> HRESULT; + pub unsafe fn DefineEvent(&self, + td: mdTypeDef, + szEvent: LPCWSTR, + dwEventFlags: DWORD, + tkEventType: mdToken, + mdAddOn: mdMethodDef, + mdRemoveOn: mdMethodDef, + mdFire: mdMethodDef, + rmdOtherMethods: *const mdMethodDef, + pmdEvent: *mut mdEvent, + ) -> HRESULT; + pub unsafe fn SetClassLayout(&self, + td: mdTypeDef, + dwPackSize: DWORD, + rFieldOffsets: *const COR_FIELD_OFFSET, + ulClassSize: ULONG, + ) -> HRESULT; + pub unsafe fn DeleteClassLayout(&self, td: mdTypeDef) -> HRESULT; + pub unsafe fn SetFieldMarshal(&self, + tk: mdToken, + pvNativeType: PCCOR_SIGNATURE, + cbNativeType: ULONG) -> HRESULT; + pub unsafe fn DeleteFieldMarshal(&self, tk: mdToken) -> HRESULT; + pub unsafe fn DefinePermissionSet(&self, + tk: mdToken, + dwAction: DWORD, + pvPermission: *const c_void, + cbPermission: ULONG, + ppm: *mut mdPermission, + ) -> HRESULT; + pub unsafe fn SetRVA(&self, md: mdMethodDef, ulRVA: ULONG) -> HRESULT; + pub unsafe fn GetTokenFromSig(&self, + pvSig: PCCOR_SIGNATURE, + cbSig: ULONG, + pmsig: *mut mdSignature, + ) -> HRESULT; + pub unsafe fn DefineModuleRef(&self, szName: LPCWSTR, pmur: *mut mdModuleRef) -> HRESULT; + pub unsafe fn SetParent(&self, mr: mdMemberRef, tk: mdToken) -> HRESULT; + pub unsafe fn GetTokenFromTypeSpec(&self, + pvSig: PCCOR_SIGNATURE, + cbSig: ULONG, + ptypespec: *mut mdTypeSpec, + ) -> HRESULT; + pub unsafe fn SaveToMemory(&self, pbData: *mut c_void, cbData: ULONG) -> HRESULT; + /// Gets a metadata token for the specified literal string. + pub unsafe fn DefineUserString(&self, + szString: LPCWSTR, + cchString: ULONG, + pstk: *mut mdString, + ) -> HRESULT; + pub unsafe fn DeleteToken(&self, tkObj: mdToken) -> HRESULT; + pub unsafe fn SetMethodProps(&self, + md: mdMethodDef, + dwMethodFlags: DWORD, + ulCodeRVA: ULONG, + dwImplFlags: DWORD) -> HRESULT; + pub unsafe fn SetTypeDefProps(&self, + td: mdTypeDef, + dwTypeDefFlags: DWORD, + tkExtends: mdToken, + rtkImplements: *const mdToken, + ) -> HRESULT; + pub unsafe fn SetEventProps(&self, + ev: mdEvent, + dwEventFlags: DWORD, + tkEventType: mdToken, + mdAddOn: mdMethodDef, + mdRemoveOn: mdMethodDef, + mdFire: mdMethodDef, + rmdOtherMethods: *const mdMethodDef, + ) -> HRESULT; + pub unsafe fn SetPermissionSetProps(&self, + tk: mdToken, + dwAction: DWORD, + pvPermission: *const c_void, + cbPermission: ULONG, + ppm: *mut mdPermission, + ) -> HRESULT; + pub unsafe fn DefinePinvokeMap(&self, + tk: mdToken, + dwMappingFlags: DWORD, + szImportName: LPCWSTR, + mrImportDLL: mdModuleRef, + ) -> HRESULT; + pub unsafe fn SetPinvokeMap(&self, + tk: mdToken, + dwMappingFlags: DWORD, + szImportName: LPCWSTR, + mrImportDLL: mdModuleRef, + ) -> HRESULT; + pub unsafe fn DeletePinvokeMap(&self, tk: mdToken) -> HRESULT; + pub unsafe fn DefineCustomAttribute(&self, + tkOwner: mdToken, + tkCtor: mdToken, + pCustomAttribute: *const c_void, + cbCustomAttribute: ULONG, + pcv: *mut mdCustomAttribute, + ) -> HRESULT; + pub unsafe fn SetCustomAttributeValue(&self, + pcv: mdCustomAttribute, + pCustomAttribute: *const c_void, + cbCustomAttribute: ULONG, + ) -> HRESULT; + pub unsafe fn DefineField(&self, + td: mdTypeDef, + szName: LPCWSTR, + dwFieldFlags: DWORD, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + dwCPlusTypeFlag: DWORD, + pValue: *const c_void, + cchValue: ULONG, + pmd: *mut mdFieldDef, + ) -> HRESULT; + pub unsafe fn DefineProperty(&self, + td: mdTypeDef, + szProperty: LPCWSTR, + dwPropFlags: DWORD, + pvSig: PCCOR_SIGNATURE, + cbSig: ULONG, + dwCPlusTypeFlag: DWORD, + pValue: *const c_void, + cchValue: ULONG, + mdSetter: mdMethodDef, + mdGetter: mdMethodDef, + rmdOtherMethods: *const mdMethodDef, + pmdProp: *mut mdProperty, + ) -> HRESULT; + pub unsafe fn DefineParam(&self, + md: mdMethodDef, + ulParamSeq: ULONG, + szName: LPCWSTR, + dwParamFlags: DWORD, + dwCPlusTypeFlag: DWORD, + pValue: *const c_void, + cchValue: ULONG, + ppd: *mut mdParamDef, + ) -> HRESULT; + pub unsafe fn SetFieldProps(&self, + fd: mdFieldDef, + dwFieldFlags: DWORD, + dwCPlusTypeFlag: DWORD, + pValue: *const c_void, + cchValue: ULONG, + ) -> HRESULT; + pub unsafe fn SetPropertyProps(&self, + pr: mdProperty, + dwPropFlags: DWORD, + dwCPlusTypeFlag: DWORD, + pValue: *const c_void, + cchValue: ULONG, + mdSetter: mdMethodDef, + mdGetter: mdMethodDef, + rmdOtherMethods: *const mdMethodDef, + ) -> HRESULT; + pub unsafe fn SetParamProps(&self, + pd: mdParamDef, + szName: LPCWSTR, + dwParamFlags: DWORD, + dwCPlusTypeFlag: DWORD, + pValue: *mut c_void, + cchValue: ULONG, + ) -> HRESULT; + pub unsafe fn DefineSecurityAttributeSet(&self, + tkObj: mdToken, + rSecAttrs: *const COR_SECATTR, + cSecAttrs: ULONG, + pulErrorAttr: *mut ULONG, + ) -> HRESULT; + pub unsafe fn ApplyEditAndContinue(&self, + pImport: *const IUnknown, + ) -> HRESULT; + pub unsafe fn TranslateSigWithScope(&self, + pAssemImport: *const IMetaDataAssemblyImport, + pbHashValue: *const c_void, + cbHashValue: ULONG, + import: *const IMetaDataImport, + pbSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + pAssemEmit: *const IMetaDataAssemblyEmit, + emit: *const IMetaDataEmit, + pvTranslatedSig: PCOR_SIGNATURE, + cbTranslatedSigMax: ULONG, + pcbTranslatedSig: *mut ULONG, + ) -> HRESULT; + pub unsafe fn SetMethodImplFlags(&self, md: mdMethodDef, dwImplFlags: DWORD) -> HRESULT; + pub unsafe fn SetFieldRVA(&self, fd: mdFieldDef, ulRVA: ULONG) -> HRESULT; + pub unsafe fn Merge(&self, + pImport: *const IMetaDataImport, + pHostMapToken: *const IMapToken, + pHandler: *const IUnknown, + ) -> HRESULT; + pub unsafe fn MergeEnd(&self) -> HRESULT; + } + + #[uuid("F5DD9950-F693-42e6-830E-7B833E8146A9")] + pub unsafe interface IMetaDataEmit2: IMetaDataEmit { + pub unsafe fn DefineMethodSpec(&self, + tkParent: mdToken, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + pmi: *mut mdMethodSpec, + ) -> HRESULT; + pub unsafe fn GetDeltaSaveSize(&self, fSave: CorSaveSize, pdwSaveSize: *mut DWORD) -> HRESULT; + pub unsafe fn SaveDelta(&self, szFile: LPCWSTR, dwSaveFlags: DWORD) -> HRESULT; + pub unsafe fn SaveDeltaToStream(&self, + pIStream: *const IStream, + dwSaveFlags: DWORD, + ) -> HRESULT; + pub unsafe fn SaveDeltaToMemory(&self, pbData: *mut c_void, cbData: ULONG) -> HRESULT; + pub unsafe fn DefineGenericParam(&self, + tk: mdToken, + ulParamSeq: ULONG, + dwParamFlags: DWORD, + szname: LPCWSTR, + reserved: DWORD, + rtkConstraints: *const mdToken, + pgp: *mut mdGenericParam, + ) -> HRESULT; + pub unsafe fn SetGenericParamProps(&self, + gp: mdGenericParam, + dwParamFlags: DWORD, + szName: LPCWSTR, + reserved: DWORD, + rtkConstraints: *const mdToken, + ) -> HRESULT; + pub unsafe fn ResetENCLog(&self) -> HRESULT; + } +} + +impl IMetaDataEmit { + pub fn define_field( + &self, + type_def: mdTypeDef, + name: &str, + flags: CorFieldAttr, + sig: &[COR_SIGNATURE], + constant_flag: CorElementType, + constant_value: Option, + constant_value_len: ULONG, + ) -> Result { + let wstr = U16CString::from_str(name).unwrap(); + let ptr = sig.as_ptr(); + let len = sig.len() as ULONG; + + let value = match constant_value { + Some(v) => &v, + None => std::ptr::null(), + }; + let mut field_def = mdFieldDefNil; + let hr = unsafe { + self.DefineField( + type_def, + wstr.as_ptr(), + flags.bits(), + ptr, + len, + constant_flag as u32, + value as *const _, + constant_value_len, + &mut field_def, + ) + }; + if FAILED(hr) { + Err(hr) + } else { + Ok(field_def) + } + } + + pub fn get_token_from_type_spec(&self, signature: &[u8]) -> Result { + let mut type_spec = mdTypeSpecNil; + let hr = unsafe { + self.GetTokenFromTypeSpec(signature.as_ptr(), signature.len() as ULONG, &mut type_spec) + }; + if FAILED(hr) { + Err(hr) + } else { + Ok(type_spec) + } + } + + /// Updates references to a module defined by a prior call to + /// [crate::interfaces::imetadata_emit::IMetaDataEmit::DefineModuleRef]. + pub fn set_module_props(&self, name: &str) -> Result<(), HRESULT> { + let wstr = U16CString::from_str(name).unwrap(); + let hr = unsafe { self.SetModuleProps(wstr.as_ptr()) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(()) + } + } + + /// Defines a reference to a member of a module outside the current scope, + /// and gets a token to that reference definition. + /// - token + /// + /// Token for the target member's class or interface, if the member is not global; + /// if the member is global, the mdModuleRef token for that other file. + /// - name + /// + /// The name of the target member + /// - sig + /// + /// The signature of the target member. + pub fn define_member_ref( + &self, + token: mdToken, + name: &str, + sig: &[COR_SIGNATURE], + ) -> Result { + let wstr = U16CString::from_str(name).unwrap(); + let mut member_ref = mdMemberRefNil; + let ptr = sig.as_ptr(); + let len = sig.len() as ULONG; + let hr = unsafe { self.DefineMemberRef(token, wstr.as_ptr(), ptr, len, &mut member_ref) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(member_ref) + } + } + + /// Creates a definition for a method or global function with the specified signature, + /// and returns a token to that method definition. + /// - token + /// + /// The token of the parent class or parent interface of the method. + /// Set to mdTokenNil, if defining a global function. + /// - name + /// + /// The member name + /// - attributes + /// + /// The attributes of the method or global function + /// - sig + /// + /// The method signature + /// - address + /// + /// The address of the code + /// - implementation + /// + /// The implementation features of the method + pub fn define_method( + &self, + token: mdTypeDef, + name: &str, + attributes: CorMethodAttr, + sig: &[COR_SIGNATURE], + address: ULONG, + implementation: CorMethodImpl, + ) -> Result { + let wstr = U16CString::from_str(name).unwrap(); + let ptr = sig.as_ptr(); + let len = sig.len() as ULONG; + let mut method_def = mdMethodDefNil; + let hr = unsafe { + self.DefineMethod( + token, + wstr.as_ptr(), + attributes.bits(), + ptr, + len, + address, + implementation.bits(), + &mut method_def, + ) + }; + if FAILED(hr) { + Err(hr) + } else { + Ok(method_def) + } + } + + /// Creates the metadata signature for a module with the specified name. + /// - name + /// + /// The name of the other metadata file, typically a DLL. This is the file name only. + /// Do not use a full path name. + pub fn define_module_ref(&self, name: &str) -> Result { + let wstr = U16CString::from_str(name).unwrap(); + let mut module_ref = mdModuleRefNil; + let hr = unsafe { self.DefineModuleRef(wstr.as_ptr(), &mut module_ref) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(module_ref) + } + } + + /// Sets features of the PInvoke signature of the method referenced by the specified token. + pub fn define_pinvoke_map( + &self, + token: mdToken, + flags: CorPinvokeMap, + import_name: &str, + import_dll: mdModuleRef, + ) -> Result<(), HRESULT> { + let wstr = U16CString::from_str(import_name).unwrap(); + let hr = unsafe { self.DefinePinvokeMap(token, flags.bits(), wstr.as_ptr(), import_dll) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(()) + } + } + + pub fn define_type_def( + &self, + name: &str, + flags: CorTypeAttr, + extends: mdToken, + implements: Option, + ) -> Result { + let wstr = U16CString::from_str(name).unwrap(); + let implements: *const mdTypeDef = match implements { + Some(v) => &v, + None => std::ptr::null(), + }; + let mut type_def = mdTypeDefNil; + let hr = unsafe { + self.DefineTypeDef( + wstr.as_ptr(), + flags.bits(), + extends, + implements, + &mut type_def, + ) + }; + + if FAILED(hr) { + Err(hr) + } else { + Ok(type_def) + } + } + + /// Gets a metadata token for a type that is defined in the specified scope, + /// which is outside the current scope. + /// - token + /// The token specifying the resolution scope. The following token types are valid: + /// - mdModuleRef + /// if the type is defined in the same assembly in which the caller is defined. + /// - mdAssemblyRef + /// if the type is defined in an assembly other than the one in which the caller is defined. + /// - mdTypeRef + /// if the type is a nested type. + /// - mdModule + /// if the type is defined in the same module in which the caller is defined. + /// - mdTokenNil + /// if the type is defined globally. + /// - name + /// The name of the target type + pub fn define_type_ref_by_name( + &self, + token: mdToken, + name: &str, + ) -> Result { + let wstr = U16CString::from_str(name).unwrap(); + let mut type_ref = mdTypeRefNil; + let hr = unsafe { self.DefineTypeRefByName(token, wstr.as_ptr(), &mut type_ref) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(type_ref) + } + } + + /// Gets a metadata token for the specified literal string. + pub fn define_user_string(&self, str: &str) -> Result { + let mut md_string = mdStringNil; + let wstr = U16CString::from_str(str).unwrap(); + let hr = + unsafe { self.DefineUserString(wstr.as_ptr(), wstr.len() as ULONG, &mut md_string) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(md_string) + } + } + + /// Gets a token for the specified metadata signature. + pub fn get_token_from_sig(&self, sig: &[u8]) -> Result { + let mut sig_token = mdSignatureNil; + let hr = unsafe { self.GetTokenFromSig(sig.as_ptr(), sig.len() as ULONG, &mut sig_token) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(sig_token) + } + } + + /// Sets or updates the metadata signature of the inherited method implementation + /// that is referenced by the specified token. + pub fn set_method_impl_flags( + &self, + method: mdMethodDef, + implementation: CorMethodImpl, + ) -> Result<(), HRESULT> { + let hr = unsafe { self.SetMethodImplFlags(method, implementation.bits()) }; + if FAILED(hr) { + Err(hr) + } else { + Ok(()) + } + } +} + +impl IMetaDataEmit2 { + pub fn define_method_spec( + &self, + token: mdToken, + signature: &[COR_SIGNATURE], + ) -> Result { + let mut method_spec = mdMethodSpecNil; + let hr = unsafe { + self.DefineMethodSpec( + token, + signature.as_ptr(), + signature.len() as ULONG, + &mut method_spec, + ) + }; + if FAILED(hr) { + Err(hr) + } else { + Ok(method_spec) + } + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/imetadata_import.rs b/src/elastic_apm_profiler/src/interfaces/imetadata_import.rs new file mode 100644 index 000000000..aab7208ee --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/imetadata_import.rs @@ -0,0 +1,1261 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use core::{ptr, slice}; +use std::{ffi::c_void, mem::MaybeUninit}; + +use com::{ + interfaces::iunknown::IUnknown, + sys::{FAILED, GUID, HRESULT, S_FALSE, S_OK}, +}; +use widestring::U16CString; + +use crate::{ + cil::uncompress_token, + ffi::{types::*, *}, + profiler::types::{FunctionInfo, FunctionMethodSignature, MethodSignature, TypeInfo}, +}; + +interfaces! { + /// Provides methods for importing and manipulating existing metadata from a portable + /// executable (PE) file or other source, such as a type library or a stand-alone, + /// run-time metadata binary. + #[uuid("7DAC8207-D3AE-4c75-9B67-92801A497D44")] + pub unsafe interface IMetaDataImport: IUnknown { + pub unsafe fn CloseEnum(&self, hEnum: HCORENUM); + pub unsafe fn CountEnum(&self, hEnum: HCORENUM, pulCount: *mut ULONG) -> HRESULT; + pub unsafe fn ResetEnum(&self, hEnum: HCORENUM, ulPos: *const ULONG) -> HRESULT; + pub unsafe fn EnumTypeDefs(&self, + phEnum: *mut HCORENUM, + rTypeDefs: *const mdTypeDef, + cMax: ULONG, + pcTypeDefs: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumInterfaceImpls(&self, + phEnum: *mut HCORENUM, + td: mdTypeDef, + rImpls: *mut mdInterfaceImpl, + cMax: ULONG, + pcImpls: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumTypeRefs(&self, + phEnum: *mut HCORENUM, + rTypeRefs: *mut mdTypeRef, + cMax: ULONG, + pcTypeRefs: *mut ULONG, + ) -> HRESULT; + pub unsafe fn FindTypeDefByName(&self, + szTypeDef: LPCWSTR, + tkEnclosingClass: mdToken, + ptd: *mut mdTypeDef, + ) -> HRESULT; + pub unsafe fn GetScopeProps(&self, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + pmvid: *mut GUID, + ) -> HRESULT; + pub unsafe fn GetModuleFromScope(&self, pmd: *mut mdModule) -> HRESULT; + pub unsafe fn GetTypeDefProps(&self, + td: mdTypeDef, + szTypeDef: *mut WCHAR, + cchTypeDef: ULONG, + pchTypeDef: *mut ULONG, + pdwTypeDefFlags: *mut DWORD, + ptkExtends: *mut mdToken, + ) -> HRESULT; + pub unsafe fn GetInterfaceImplProps(&self, + iiImpl: mdInterfaceImpl, + pClass: *mut mdTypeDef, + ptkIface: *mut mdToken, + ) -> HRESULT; + pub unsafe fn GetTypeRefProps(&self, + tr: mdTypeRef, + ptkResolutionScope: *mut mdToken, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + ) -> HRESULT; + pub unsafe fn ResolveTypeRef(&self, + tr: mdTypeRef, + riid: REFIID, + ppIScope: *mut *mut IUnknown, + ptd: *mut mdTypeDef, + ) -> HRESULT; + pub unsafe fn EnumMembers(&self, + phEnum: *mut HCORENUM, + cl: mdTypeDef, + rMembers: *mut mdToken, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumMembersWithName(&self, + phEnum: *mut HCORENUM, + cl: mdTypeDef, + szName: LPCWSTR, + rMembers: *mut mdToken, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumMethods(&self, + phEnum: *mut HCORENUM, + cl: mdTypeDef, + rMethods: *mut mdMethodDef, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumMethodsWithName(&self, + phEnum: *mut HCORENUM, + cl: mdTypeDef, + szName: LPCWSTR, + rMethods: *mut mdMethodDef, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumFields(&self, + phEnum: *mut HCORENUM, + cl: mdTypeDef, + rFields: *mut mdFieldDef, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumFieldsWithName(&self, + phEnum: *mut HCORENUM, + cl: mdTypeDef, + szName: LPCWSTR, + rFields: *mut mdFieldDef, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumParams(&self, + phEnum: *mut HCORENUM, + mb: mdMethodDef, + rParams: *mut mdParamDef, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumMemberRefs(&self, + phEnum: *mut HCORENUM, + tkParent: mdToken, + rMemberRefs: *mut mdMemberRef, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumMethodImpls(&self, + phEnum: *mut HCORENUM, + td: mdTypeDef, + rMethodBody: *mut mdToken, + rMethodDecl: *mut mdToken, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumPermissionSets(&self, + phEnum: *mut HCORENUM, + tk: mdToken, + dwActions: DWORD, + rPermission: *mut mdPermission, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn FindMember(&self, + td: mdTypeDef, + szName: LPCWSTR, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + pmb: *mut mdToken, + ) -> HRESULT; + pub unsafe fn FindMethod(&self, + td: mdTypeDef, + szName: LPCWSTR, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + pmb: *mut mdMethodDef, + ) -> HRESULT; + pub unsafe fn FindField(&self, + td: mdTypeDef, + szName: LPCWSTR, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + pmb: *mut mdFieldDef, + ) -> HRESULT; + pub unsafe fn FindMemberRef(&self, + td: mdTypeRef, + szName: LPCWSTR, + pvSigBlob: PCCOR_SIGNATURE, + cbSigBlob: ULONG, + pmr: *mut mdMemberRef, + ) -> HRESULT; + pub unsafe fn GetMethodProps(&self, + mb: mdMethodDef, + pClass: *mut mdTypeDef, + szMethod: *mut WCHAR, + cchMethod: ULONG, + pchMethod: *mut ULONG, + pdwAttr: *mut DWORD, + ppvSigBlob: *mut PCCOR_SIGNATURE, + pcbSigBlob: *mut ULONG, + pulCodeRVA: *mut ULONG, + pdwImplFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetMemberRefProps(&self, + mr: mdMemberRef, + ptk: *mut mdToken, + szMember: *mut WCHAR, + cchMember: ULONG, + pchMember: *mut ULONG, + ppvSigBlob: *mut PCCOR_SIGNATURE, + pbSig: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumProperties(&self, + phEnum: *mut HCORENUM, + td: mdTypeDef, + rProperties: *mut mdProperty, + cMax: ULONG, + pcProperties: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumEvents(&self, + phEnum: *mut HCORENUM, + td: mdTypeDef, + rEvents: *mut mdEvent, + cMax: ULONG, + pcEvents: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetEventProps(&self, + ev: mdEvent, + pClass: *mut mdTypeDef, + szEvent: *mut WCHAR, + cchEvent: ULONG, + pchEvent: *mut ULONG, + pdwEventFlags: *mut DWORD, + ptkEventType: *mut mdToken, + pmdAddOn: *mut mdMethodDef, + pmdRemoveOn: *mut mdMethodDef, + pmdFire: *mut mdMethodDef, + rmdOtherMethod: *mut mdMethodDef, + cMax: ULONG, + pcOtherMethod: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumMethodSemantics(&self, + phEnum: *mut HCORENUM, + mb: mdMethodDef, + rEventProp: *mut mdToken, + cMax: ULONG, + pcEventProp: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetMethodSemantics(&self, + mb: mdMethodDef, + tkEventProp: mdToken, + pdwSemanticsFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetClassLayout(&self, + td: mdTypeDef, + pdwPackSize: *mut DWORD, + rFieldOffset: *mut COR_FIELD_OFFSET, + cMax: ULONG, + pcFieldOffset: *mut ULONG, + pulClassSize: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetFieldMarshal(&self, + tk: mdToken, + ppvNativeType: *mut PCCOR_SIGNATURE, + pcbNativeType: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetRVA(&self, + tk: mdToken, + pulCodeRVA: *mut ULONG, + pdwImplFlags: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetPermissionSetProps(&self, + pm: mdPermission, + pdwAction: *mut DWORD, + ppvPermission: *mut *mut c_void, + pcbPermission: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetSigFromToken(&self, + mdSig: mdSignature, + ppvSig: *mut PCCOR_SIGNATURE, + pcbSig: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetModuleRefProps(&self, + mur: mdModuleRef, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumModuleRefs(&self, + phEnum: *mut HCORENUM, + rModuleRefs: *mut mdModuleRef, + cmax: ULONG, + pcModuleRefs: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetTypeSpecFromToken(&self, + typespec: mdTypeSpec, + ppvSig: *mut PCCOR_SIGNATURE, + pcbSig: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetNameFromToken(&self, + tk: mdToken, + pszUtf8NamePtr: *mut MDUTF8CSTR, + ) -> HRESULT; + pub unsafe fn EnumUnresolvedMethods(&self, + phEnum: *mut HCORENUM, + rMethods: *mut mdToken, + cMax: ULONG, + pcTokens: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetUserString(&self, + stk: mdString, + szString: *mut WCHAR, + cchString: ULONG, + pchString: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetPinvokeMap(&self, + tk: mdToken, + pdwMappingFlags: *mut DWORD, + szImportName: *mut WCHAR, + cchImportName: ULONG, + pchImportName: *mut ULONG, + pmrImportDLL: *mut mdModuleRef, + ) -> HRESULT; + pub unsafe fn EnumSignatures(&self, + phEnum: *mut HCORENUM, + rSignatures: *mut mdSignature, + cMax: ULONG, + pcSignatures: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumTypeSpecs(&self, + phEnum: *mut HCORENUM, + rTypeSpecs: *mut mdTypeSpec, + cMax: ULONG, + pcTypeSpecs: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumUserStrings(&self, + phEnum: *mut HCORENUM, + rStrings: *mut mdString, + cMax: ULONG, + pcStrings: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetParamForMethodIndex(&self, + md: mdMethodDef, + ulParamSeq: ULONG, + ppd: *mut mdParamDef, + ) -> HRESULT; + pub unsafe fn EnumCustomAttributes(&self, + phEnum: *mut HCORENUM, + tk: mdToken, + tkType: mdToken, + rCustomAttributes: *mut mdCustomAttribute, + cMax: ULONG, + pcCustomAttributes: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetCustomAttributeProps(&self, + cv: mdCustomAttribute, + ptkObj: *mut mdToken, + ptkType: *mut mdToken, + ppBlob: *mut *mut c_void, + pcbSize: *mut ULONG, + ) -> HRESULT; + pub unsafe fn FindTypeRef(&self, + tkResolutionScope: mdToken, + szName: LPCWSTR, + ptr: *mut mdTypeRef, + ) -> HRESULT; + pub unsafe fn GetMemberProps(&self, + mb: mdToken, + pClass: *mut mdTypeDef, + szMember: *mut WCHAR, + cchMember: ULONG, + pchMember: *mut ULONG, + pdwAttr: *mut DWORD, + ppvSigBlob: *mut PCCOR_SIGNATURE, + pcbSigBlob: *mut ULONG, + pulCodeRVA: *mut ULONG, + pdwImplFlags: *mut DWORD, + pdwCPlusTypeFlag: *mut DWORD, + ppValue: *mut UVCP_CONSTANT, + pcchValue: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetFieldProps(&self, + mb: mdToken, + pClass: *mut mdTypeDef, + szField: *mut WCHAR, + cchField: ULONG, + pchField: *mut ULONG, + pdwAttr: *mut DWORD, + ppvSigBlob: *mut PCCOR_SIGNATURE, + pcbSigBlob: *mut ULONG, + pdwCPlusTypeFlag: *mut DWORD, + ppValue: *mut UVCP_CONSTANT, + pcchValue: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetPropertyProps(&self, + prop: mdProperty, + pClass: *mut mdTypeDef, + szProperty: *mut WCHAR, + cchProperty: ULONG, + pchProperty: *mut ULONG, + pdwPropFlags: *mut DWORD, + ppvSig: *mut PCCOR_SIGNATURE, + pbSig: *mut ULONG, + pdwCPlusTypeFlag: *mut DWORD, + ppDefaultValue: *mut UVCP_CONSTANT, + pcchDefaultValue: *mut ULONG, + pmdSetter: *mut mdMethodDef, + pmdGetter: *mut mdMethodDef, + rmdOtherMethod: *mut mdMethodDef, + cMax: ULONG, + pcOtherMethod: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetParamProps(&self, + tk: mdParamDef, + pmd: *mut mdMethodDef, + pulSequence: *mut ULONG, + szName: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + pdwAttr: *mut DWORD, + pdwCPlusTypeFlag: *mut DWORD, + ppValue: *mut UVCP_CONSTANT, + pcchValue: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetCustomAttributeByName(&self, + tkObj: mdToken, + szName: LPCWSTR, + ppData: *mut *mut c_void, + pcbData: *mut ULONG, + ) -> HRESULT; + pub unsafe fn IsValidToken(&self, tk: mdToken) -> BOOL; + pub unsafe fn GetNestedClassProps(&self, + tdNestedClass: mdTypeDef, + ptdEnclosingClass: *mut mdTypeDef, + ) -> HRESULT; + pub unsafe fn GetNativeCallConvFromSig(&self, + pvSig: *const c_void, + cbSig: ULONG, + pCallConv: *mut ULONG, + ) -> HRESULT; + pub unsafe fn IsGlobal(&self, pd: mdToken, pbGlobal: *mut int) -> HRESULT; + } + + /// Extends the IMetaDataImport interface to provide the capability of working + /// with generic types. + #[uuid("FCE5EFA0-8BBA-4F8E-A036-8F2022B08466")] + pub unsafe interface IMetaDataImport2: IMetaDataImport { + pub unsafe fn EnumGenericParams(&self, + phEnum: *mut HCORENUM, + tk: mdToken, + rGenericParams: *mut mdGenericParam, + cMax: ULONG, + pcGenericParams: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetGenericParamProps(&self, + gp: mdGenericParam, + pulParamSeq: *mut ULONG, + pdwParamFlags: *mut DWORD, + ptOwner: *mut mdToken, + reserved: *mut DWORD, + wzname: *mut WCHAR, + cchName: ULONG, + pchName: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetMethodSpecProps(&self, + mi: mdMethodSpec, + tkParent: *mut mdToken, + ppvSigBlob: *mut PCCOR_SIGNATURE, + pcbSigBlob: *mut ULONG, + ) -> HRESULT; + pub unsafe fn EnumGenericParamConstraints(&self, + phEnum: *mut HCORENUM, + tk: mdGenericParam, + rGenericParamConstraints: *mut mdGenericParamConstraint, + cMax: ULONG, + pcGenericParamConstraints: *mut ULONG, + ) -> HRESULT; + pub unsafe fn GetGenericParamConstraintProps(&self, + gpc: mdGenericParamConstraint, + ptGenericParam: *mut mdGenericParam, + ptkConstraintType: *mut mdToken, + ) -> HRESULT; + pub unsafe fn GetPEKind(&self, + pdwPEKind: *mut DWORD, + pdwMAchine: *mut DWORD, + ) -> HRESULT; + pub unsafe fn GetVersionString(&self, + pwzBuf: *mut WCHAR, + ccBufSize: DWORD, + pccBufSize: *mut DWORD, + ) -> HRESULT; + pub unsafe fn EnumMethodSpecs(&self, + phEnum: *mut HCORENUM, + tk: mdToken, + rMethodSpecs: *mut mdMethodSpec, + cMax: ULONG, + pcMethodSpecs: *mut ULONG, + ) -> HRESULT; + } +} + +impl IMetaDataImport { + pub fn find_member_ref( + &self, + type_ref: mdTypeRef, + name: &str, + signature: &[u8], + ) -> Result { + let wide_name = U16CString::from_str(name).unwrap(); + let mut member_ref = mdMemberRefNil; + let hr = unsafe { + self.FindMemberRef( + type_ref, + wide_name.as_ptr(), + signature.as_ptr(), + signature.len() as ULONG, + &mut member_ref, + ) + }; + match hr { + S_OK => Ok(member_ref), + _ => Err(hr), + } + } + + /// Gets a pointer to the TypeDef metadata token for the Type with the specified name. + pub fn find_type_def_by_name( + &self, + name: &str, + enclosing_class: Option, + ) -> Result { + let wide_name = U16CString::from_str(name).unwrap(); + let mut type_def = mdTypeDefNil; + let hr = unsafe { + self.FindTypeDefByName( + wide_name.as_ptr(), + enclosing_class.unwrap_or(mdTokenNil), + &mut type_def, + ) + }; + match hr { + S_OK => Ok(type_def), + _ => Err(hr), + } + } + + pub fn find_type_ref(&self, scope: mdToken, name: &str) -> Result { + let wide_name = U16CString::from_str(name).unwrap(); + let mut type_ref = mdTypeRefNil; + + let hr = unsafe { self.FindTypeRef(scope, wide_name.as_ptr(), &mut type_ref) }; + + match hr { + S_OK => Ok(type_ref), + _ => Err(hr), + } + } + + /// Enumerates methods that have the specified name and that are defined by the + /// type referenced by the specified TypeDef token. + pub fn enum_methods_with_name( + &self, + type_def: mdTypeDef, + name: &str, + ) -> Result, HRESULT> { + let mut en = ptr::null_mut() as HCORENUM; + let wide_name = U16CString::from_str(name).unwrap(); + let max = 256; + let mut method_defs = Vec::with_capacity(max as usize); + let mut method_len = MaybeUninit::uninit(); + let hr = unsafe { + self.EnumMethodsWithName( + &mut en, + type_def, + wide_name.as_ptr(), + method_defs.as_mut_ptr(), + max, + method_len.as_mut_ptr(), + ) + }; + + match hr { + S_OK => { + unsafe { + let len = method_len.assume_init(); + method_defs.set_len(len as usize); + self.CloseEnum(en); + } + Ok(method_defs) + } + S_FALSE => Ok(Vec::new()), + _ => Err(hr), + } + } + + /// Gets metadata associated with the member referenced by the specified token. + pub fn get_member_ref_props(&self, mr: mdMemberRef) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + let hr = unsafe { + self.GetMemberRefProps( + mr, + ptr::null_mut(), + ptr::null_mut(), + 0, + name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + let mut name_length = MaybeUninit::uninit(); + let mut class_token = MaybeUninit::uninit(); + let mut pb_sig_blob = MaybeUninit::uninit(); + let mut pb_sig = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetMemberRefProps( + mr, + class_token.as_mut_ptr(), + name_buffer.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + pb_sig_blob.as_mut_ptr(), + pb_sig.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let class_token = unsafe { class_token.assume_init() }; + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + let signature = unsafe { + let pb_sig_blob = pb_sig_blob.assume_init(); + let pb_sig = pb_sig.assume_init(); + std::slice::from_raw_parts(pb_sig_blob, pb_sig as usize).to_vec() + }; + + Ok(MemberRefProps { + name, + class_token, + signature, + }) + } + + /// Gets information stored in the metadata for a specified member definition, + /// including the name, binary signature, and relative virtual address, of the Type member + /// referenced by the specified metadata token. This is a simple helper method: if mb is a + /// MethodDef, then GetMethodProps is called; if mb is a FieldDef, then GetFieldProps is + /// called. See these other methods for details. + pub fn get_member_props(&self, mb: mdToken) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + let hr = unsafe { + self.GetMemberProps( + mb, + ptr::null_mut(), + ptr::null_mut(), + 0, + name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + let mut name_length = MaybeUninit::uninit(); + let mut class_token = MaybeUninit::uninit(); + let mut pdw_attr = MaybeUninit::uninit(); + let mut impl_flags = MaybeUninit::uninit(); + let mut element_type = MaybeUninit::uninit(); + let mut ppv_sig_blob = MaybeUninit::uninit(); + let mut pcb_sig_blob = MaybeUninit::uninit(); + let mut pul_code_rva = MaybeUninit::uninit(); + let mut value = MaybeUninit::uninit(); + let mut value_len = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetMemberProps( + mb, + class_token.as_mut_ptr(), + name_buffer.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + pdw_attr.as_mut_ptr(), + ppv_sig_blob.as_mut_ptr(), + pcb_sig_blob.as_mut_ptr(), + pul_code_rva.as_mut_ptr(), + impl_flags.as_mut_ptr(), + element_type.as_mut_ptr(), + value.as_mut_ptr(), + value_len.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + let class_token = unsafe { class_token.assume_init() }; + let pdw_attr = unsafe { pdw_attr.assume_init() }; + let impl_flags = unsafe { impl_flags.assume_init() }; + let element_type = unsafe { element_type.assume_init() }; + let signature = unsafe { + let ppv_sig_blob = ppv_sig_blob.assume_init(); + let pcb_sig_blob = pcb_sig_blob.assume_init(); + std::slice::from_raw_parts(ppv_sig_blob, pcb_sig_blob as usize).to_vec() + }; + let pul_code_rva = unsafe { pul_code_rva.assume_init() }; + let value_len = unsafe { value_len.assume_init() }; + let value = if value_len > 0 { + unsafe { + let _value = value.assume_init(); + // TODO: get string from value + //slice_from_raw_parts(value, value_len as usize) + String::new() + } + } else { + String::new() + }; + + Ok(MemberProps { + name, + class_token, + member_flags: pdw_attr, + relative_virtual_address: pul_code_rva, + method_impl_flags: impl_flags, + element_type: CorElementType::from(element_type), + signature, + value, + }) + } + + /// Gets the metadata associated with the method referenced by the specified MethodDef token. + pub fn get_method_props(&self, mb: mdMethodDef) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + unsafe { + self.GetMethodProps( + mb, + ptr::null_mut(), + ptr::null_mut(), + 0, + name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + let mut class_token = MaybeUninit::uninit(); + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + let mut name_length = MaybeUninit::uninit(); + let mut attr_flags = MaybeUninit::uninit(); + let mut sig = MaybeUninit::uninit(); + let mut sig_length = MaybeUninit::uninit(); + let mut rva = MaybeUninit::uninit(); + let mut impl_flags = MaybeUninit::uninit(); + let hr = unsafe { + self.GetMethodProps( + mb, + class_token.as_mut_ptr(), + name_buffer.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + attr_flags.as_mut_ptr(), + sig.as_mut_ptr(), + sig_length.as_mut_ptr(), + rva.as_mut_ptr(), + impl_flags.as_mut_ptr(), + ) + }; + match hr { + S_OK => { + let class_token = unsafe { class_token.assume_init() }; + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + let attr_flags = unsafe { attr_flags.assume_init() }; + let attr_flags = CorMethodAttr::from_bits(attr_flags).unwrap(); + let sig = unsafe { sig.assume_init() }; + let sig_length = unsafe { sig_length.assume_init() }; + let rva = unsafe { rva.assume_init() }; + let impl_flags = unsafe { impl_flags.assume_init() }; + let impl_flags = CorMethodImpl::from_bits(impl_flags).unwrap(); + Ok(MethodProps { + class_token, + name, + attr_flags, + sig, + sig_length, + rva, + impl_flags, + }) + } + _ => Err(hr), + } + } + + pub fn get_module_from_scope(&self) -> Result { + let mut module = mdModuleNil; + let hr = unsafe { self.GetModuleFromScope(&mut module) }; + match hr { + S_OK => Ok(module), + _ => Err(hr), + } + } + + /// Gets the name of the module referenced by the specified metadata token. + pub fn get_module_ref_props(&self, token: mdModuleRef) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + let hr = unsafe { + self.GetModuleRefProps(token, ptr::null_mut(), 0, name_buffer_length.as_mut_ptr()) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + let mut name_length = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetModuleRefProps( + token, + name_buffer.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + + Ok(ModuleRefProps { name }) + } + + pub fn get_nested_class_props(&self, token: mdTypeDef) -> Result { + let mut parent_token = mdTypeDefNil; + let hr = unsafe { self.GetNestedClassProps(token, &mut parent_token) }; + match hr { + S_OK => Ok(parent_token), + _ => Err(hr), + } + } + + pub fn get_sig_from_token(&self, token: mdSignature) -> Result, HRESULT> { + let mut sig = MaybeUninit::uninit(); + let mut len = 0; + let hr = unsafe { self.GetSigFromToken(token, sig.as_mut_ptr(), &mut len) }; + match hr { + S_OK => { + let signature = unsafe { + let s = sig.assume_init(); + slice::from_raw_parts(s as *const u8, len as usize).to_vec() + }; + Ok(signature) + } + _ => Err(hr), + } + } + + /// Gets metadata information for the Type represented by the specified metadata token. + pub fn get_type_def_props(&self, token: mdTypeDef) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + let hr = unsafe { + self.GetTypeDefProps( + token, + ptr::null_mut(), + 0, + name_buffer_length.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + + let mut name_length = MaybeUninit::uninit(); + let mut cor_type_attr = MaybeUninit::uninit(); + let mut extends_td = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetTypeDefProps( + token, + name_buffer.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + cor_type_attr.as_mut_ptr(), + extends_td.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + let cor_type_attr = { + let c = unsafe { cor_type_attr.assume_init() }; + CorTypeAttr::from_bits(c).unwrap() + }; + let extends_td = unsafe { extends_td.assume_init() }; + Ok(TypeDefProps { + name, + cor_type_attr, + extends_td, + }) + } + + /// Gets the metadata associated with the Type referenced by the specified TypeRef token. + pub fn get_type_ref_props(&self, token: mdTypeRef) -> Result { + let mut name_buffer_length = MaybeUninit::uninit(); + let mut parent_token = mdTokenNil; + + let hr = unsafe { + self.GetTypeRefProps( + token, + &mut parent_token, + ptr::null_mut(), + 0, + name_buffer_length.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name_buffer_length = unsafe { name_buffer_length.assume_init() }; + let mut name_buffer = Vec::::with_capacity(name_buffer_length as usize); + unsafe { name_buffer.set_len(name_buffer_length as usize) }; + let mut name_length = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetTypeRefProps( + token, + &mut parent_token, + name_buffer.as_mut_ptr(), + name_buffer_length, + name_length.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let name = U16CString::from_vec_with_nul(name_buffer) + .unwrap() + .to_string_lossy(); + + Ok(TypeRefProps { name, parent_token }) + } + + /// Gets the binary metadata signature of the type specification represented by + /// the specified token. + pub fn get_type_spec_from_token(&self, token: mdTypeSpec) -> Result { + let mut signature = MaybeUninit::uninit(); + let mut signature_len = MaybeUninit::uninit(); + let hr = unsafe { + self.GetTypeSpecFromToken(token, signature.as_mut_ptr(), signature_len.as_mut_ptr()) + }; + + if FAILED(hr) { + return Err(hr); + } + + let signature = unsafe { + let s = signature.assume_init(); + let l = signature_len.assume_init(); + std::slice::from_raw_parts(s, l as usize).to_vec() + }; + + Ok(TypeSpec { signature }) + } + + /// Gets the literal string represented by the specified metadata token. + pub fn get_user_string(&self, stk: mdString) -> Result { + let mut len = MaybeUninit::uninit(); + let hr = unsafe { self.GetUserString(stk, ptr::null_mut(), 0, len.as_mut_ptr()) }; + + if FAILED(hr) { + return Err(hr); + } + + let len = unsafe { len.assume_init() }; + let mut str_buffer = Vec::::with_capacity(len as usize); + unsafe { str_buffer.set_len(len as usize) }; + let hr = unsafe { self.GetUserString(stk, str_buffer.as_mut_ptr(), len, ptr::null_mut()) }; + + if FAILED(hr) { + return Err(hr); + } + + // NOTE: the user string is not null terminated + let str = String::from_utf16(&str_buffer).unwrap(); + Ok(str) + } +} + +impl IMetaDataImport2 { + /// Gets the metadata signature of the method referenced by the specified MethodSpec token. + pub fn get_method_spec_props(&self, token: mdMethodSpec) -> Result { + let mut parent = MaybeUninit::uninit(); + let mut signature = MaybeUninit::uninit(); + let mut signature_len = MaybeUninit::uninit(); + let hr = unsafe { + self.GetMethodSpecProps( + token, + parent.as_mut_ptr(), + signature.as_mut_ptr(), + signature_len.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let parent = unsafe { parent.assume_init() }; + let signature = unsafe { + let s = signature.assume_init(); + let l = signature_len.assume_init(); + std::slice::from_raw_parts(s, l as usize).to_vec() + }; + + Ok(MethodSpecProps { parent, signature }) + } + + // other methods, not direct Rust abstractions over COM + + /// Gets the function information from the specified metadata token + pub fn get_function_info(&self, token: mdToken) -> Result { + let cor_token_type = { + let t = type_from_token(token); + CorTokenType::from_bits(t).unwrap() + }; + + let mut is_generic = false; + let function_name; + let parent_token; + let mut method_def_id = mdTokenNil; + let final_signature; + let method_signature; + let mut method_spec_signature = None; + + match cor_token_type { + CorTokenType::mdtMemberRef => { + let member_ref_props = self.get_member_ref_props(token)?; + function_name = member_ref_props.name; + parent_token = member_ref_props.class_token; + final_signature = MethodSignature::new(member_ref_props.signature.clone()); + method_signature = FunctionMethodSignature::new(member_ref_props.signature); + } + CorTokenType::mdtMethodDef => { + let member_props = self.get_member_props(token)?; + function_name = member_props.name; + parent_token = member_props.class_token; + final_signature = MethodSignature::new(member_props.signature.clone()); + method_signature = FunctionMethodSignature::new(member_props.signature); + } + CorTokenType::mdtMethodSpec => { + let method_spec = self.get_method_spec_props(token)?; + parent_token = method_spec.parent; + + is_generic = true; + let generic_info = self.get_function_info(parent_token)?; + + function_name = generic_info.name; + final_signature = generic_info.signature; + method_signature = FunctionMethodSignature::new(method_spec.signature.clone()); + method_spec_signature = Some(MethodSignature::new(method_spec.signature)); + method_def_id = generic_info.id; + } + _ => { + log::warn!("get_function_info: unknown token type {}", token); + return Err(E_FAIL); + } + }; + + let type_info = self.get_type_info(parent_token)?; + + Ok(FunctionInfo::new( + token, + function_name, + is_generic, + type_info, + final_signature, + method_spec_signature, + method_def_id, + method_signature, + )) + } + + /// Gets the type information for the specified metadata token + pub fn get_type_info(&self, token: mdToken) -> Result, HRESULT> { + let token_type = { + let t = type_from_token(token); + CorTokenType::from_bits(t).unwrap() + }; + + let mut parent_type = None; + let mut extends_from = None; + let mut name: String = String::new(); + let mut is_value_type = false; + let mut is_generic = false; + + match token_type { + CorTokenType::mdtTypeDef => { + let type_def_props = self.get_type_def_props(token)?; + name = type_def_props.name; + + let mut parent_type_token = mdTokenNil; + + // try to get the parent type if type is nested + let hr = unsafe { self.GetNestedClassProps(token, &mut parent_type_token) }; + if parent_type_token != mdTokenNil { + parent_type = Some(Box::new(self.get_type_info(parent_type_token)?.unwrap())); + } + + // get the base type + if type_def_props.extends_td != mdTokenNil { + if let Some(extends_type_info) = + self.get_type_info(type_def_props.extends_td)? + { + is_value_type = &extends_type_info.name == "System.ValueType" + || &extends_type_info.name == "System.Enum"; + extends_from = Some(Box::new(extends_type_info)); + } + } + } + CorTokenType::mdtTypeRef => { + let type_ref_props = self.get_type_ref_props(token)?; + name = type_ref_props.name; + } + CorTokenType::mdtTypeSpec => { + let type_spec = self.get_type_spec_from_token(token)?; + + if type_spec.signature.len() < 3 { + return Ok(None); + } + + if type_spec.signature[0] == CorElementType::ELEMENT_TYPE_GENERICINST as u8 { + let (base_token, base_len) = uncompress_token(&type_spec.signature[2..]); + return if let Some(base_type) = self.get_type_info(base_token)? { + Ok(Some(TypeInfo { + id: base_type.id, + name: base_type.name, + type_spec: token, + token_type: base_type.token_type, + extends_from: base_type.extends_from, + is_value_type: base_type.is_value_type, + is_generic: base_type.is_generic, + parent_type: base_type.parent_type, + })) + } else { + Ok(None) + }; + } + } + CorTokenType::mdtModuleRef => { + let module_ref_props = self.get_module_ref_props(token)?; + name = module_ref_props.name; + } + CorTokenType::mdtMemberRef => { + let function_info = self.get_function_info(token)?; + return Ok(function_info.type_info); + } + CorTokenType::mdtMethodDef => { + let function_info = self.get_function_info(token)?; + return Ok(function_info.type_info); + } + _ => return Ok(None), + }; + + // check the type name for generic arity + if let Some(index) = name.rfind('`') { + let from_right = name.len() - index - 1; + is_generic = from_right == 1 || from_right == 2; + } + + Ok(Some(TypeInfo { + id: token, + name, + type_spec: mdTypeSpecNil, + token_type, + extends_from, + is_value_type, + is_generic, + parent_type, + })) + } + + pub fn get_module_version_id(&self) -> Result { + let mut module = mdModuleNil; + let hr = unsafe { self.GetModuleFromScope(&mut module) }; + + if FAILED(hr) { + return Err(hr); + } + + let mut module_version_id = MaybeUninit::uninit(); + + let hr = unsafe { + self.GetScopeProps( + ptr::null_mut(), + 0, + ptr::null_mut(), + module_version_id.as_mut_ptr(), + ) + }; + + if FAILED(hr) { + return Err(hr); + } + + let module_version_id = unsafe { module_version_id.assume_init() }; + Ok(module_version_id) + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/imethod_malloc.rs b/src/elastic_apm_profiler/src/interfaces/imethod_malloc.rs new file mode 100644 index 000000000..9e2327dec --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/imethod_malloc.rs @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use c_vec::CVec; +use com::{interfaces, interfaces::IUnknown, sys::HRESULT}; + +interfaces! { + /// /// Provides a method to allocate memory for a new Microsoft intermediate language (MSIL) function body. + #[uuid("A0EFB28B-6EE2-4D7B-B983-A75EF7BEEDB8")] + pub unsafe interface IMethodMalloc: IUnknown { + /// Attempts to allocate a specified amount of memory for a new MSIL function body. + pub fn Alloc(&self, cb: ULONG) -> LPVOID; + } +} + +impl IMethodMalloc { + /// Attempts to allocate a specified amount of memory for a new MSIL function body. + /// - cb: the number of bytes to allocate + pub fn alloc(&self, cb: ULONG) -> Result, HRESULT> { + unsafe { + let p = self.Alloc(cb); + if p.is_null() { + log::error!("failed to allocate {} bytes", cb); + Err(E_FAIL) + } else { + let address = p as *mut u8; + Ok(CVec::new(address, cb as usize)) + } + } + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/istream.rs b/src/elastic_apm_profiler/src/interfaces/istream.rs new file mode 100644 index 000000000..a6c22e54d --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/istream.rs @@ -0,0 +1,55 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::ffi::*; +use com::{interfaces::iunknown::IUnknown, sys::HRESULT}; +use std::ffi::c_void; + +interfaces! { + #[uuid("0c733a30-2a1c-11ce-ade5-00aa0044773d")] + pub unsafe interface ISequentialStream: IUnknown { + pub unsafe fn Read(&self, pv: *mut c_void, cb: ULONG, pcbRead: *mut ULONG) -> HRESULT; + pub unsafe fn Write(&self, pv: *const c_void, cb: ULONG, pcbWritten: *mut ULONG) -> HRESULT; + } + + #[uuid("0000000c-0000-0000-C000-000000000046")] + pub unsafe interface IStream: ISequentialStream { + /// The Seek method changes the seek pointer to a new location. The new location is + /// relative to either the beginning of the stream, the end of the stream, or the current seek pointer. + pub unsafe fn Seek(&self, + dlibMove: LARGE_INTEGER, + dwOrigin: DWORD, + plibNewPosition: *const ULARGE_INTEGER) -> HRESULT; + + pub unsafe fn SetSize(&self, libNewSize: ULARGE_INTEGER) -> HRESULT; + + pub unsafe fn CopyTo(&self, + pstm: *const IStream, + cb: ULARGE_INTEGER, + pcbRead: *const ULARGE_INTEGER, + pcbWritten: *const ULARGE_INTEGER) -> HRESULT; + + pub unsafe fn Commit(&self, grfCommitFlags: DWORD ) -> HRESULT; + + pub unsafe fn Revert(&self) -> HRESULT; + + pub unsafe fn LockRegion(&self, + libOffset: ULARGE_INTEGER, + cb: ULARGE_INTEGER, + dwLockType: DWORD) -> HRESULT; + + pub unsafe fn UnlockRegion(&self, + libOffset: ULARGE_INTEGER, + cb: ULARGE_INTEGER, + dwLockType: DWORD) -> HRESULT; + + /// The Stat method retrieves the STATSTG structure for this stream. + pub unsafe fn Stat(&self, pstatstg: *const STATSTG, grfStatFlag: DWORD) -> HRESULT; + + /// The Clone method creates a new stream object with its own seek pointer that + /// references the same bytes as the original stream. + pub unsafe fn Clone(&self, ppstm: *const *const IStream) -> HRESULT; + } +} diff --git a/src/elastic_apm_profiler/src/interfaces/mod.rs b/src/elastic_apm_profiler/src/interfaces/mod.rs new file mode 100644 index 000000000..74550f7f8 --- /dev/null +++ b/src/elastic_apm_profiler/src/interfaces/mod.rs @@ -0,0 +1,40 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +mod icor_profiler_assembly_reference_provider; +mod icor_profiler_callback; +mod icor_profiler_function_control; +mod icor_profiler_function_enum; +mod icor_profiler_info; +mod icor_profiler_method_enum; +mod icor_profiler_module_enum; +mod icor_profiler_object_enum; +mod icor_profiler_thread_enum; +mod imap_token; +mod imetadata_assembly_emit; +mod imetadata_assembly_import; +mod imetadata_dispenser; +mod imetadata_emit; +mod imetadata_import; +mod imethod_malloc; +mod istream; + +pub use icor_profiler_assembly_reference_provider::*; +pub use icor_profiler_callback::*; +pub use icor_profiler_function_control::*; +pub use icor_profiler_function_enum::*; +pub use icor_profiler_info::*; +pub use icor_profiler_method_enum::*; +pub use icor_profiler_module_enum::*; +pub use icor_profiler_object_enum::*; +pub use icor_profiler_thread_enum::*; +pub use imap_token::*; +pub use imetadata_assembly_emit::*; +pub use imetadata_assembly_import::*; +pub use imetadata_dispenser::*; +pub use imetadata_emit::*; +pub use imetadata_import::*; +pub use imethod_malloc::*; +pub use istream::*; diff --git a/src/elastic_apm_profiler/src/lib.rs b/src/elastic_apm_profiler/src/lib.rs new file mode 100644 index 000000000..aff557b01 --- /dev/null +++ b/src/elastic_apm_profiler/src/lib.rs @@ -0,0 +1,47 @@ +#![allow(dead_code, unused_variables)] +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate com; +#[macro_use] +extern crate num_derive; + +mod error; +mod ffi; +mod profiler; + +pub mod cil; +pub mod interfaces; + +use com::CLSID; +use profiler::Profiler; + +/// The CLSID of the profiler +/// {FA65FE15-F085-4681-9B20-95E04F6C03CC} +const CLSID_PROFILER: CLSID = CLSID { + data1: 0xFA65FE15, + data2: 0xF085, + data3: 0x4681, + data4: [0x9B, 0x20, 0x95, 0xE0, 0x4F, 0x6C, 0x03, 0xCC], +}; + +/// Called by the runtime to get an instance of the profiler +#[no_mangle] +unsafe extern "system" fn DllGetClassObject( + class_id: *const ::com::sys::CLSID, + iid: *const ::com::sys::IID, + result: *mut *mut ::core::ffi::c_void, +) -> ::com::sys::HRESULT { + let class_id = &*class_id; + if class_id == &CLSID_PROFILER { + let instance = ::Factory::allocate(); + instance.QueryInterface(&*iid, result) + } else { + ::com::sys::CLASS_E_CLASSNOTAVAILABLE + } +} diff --git a/src/elastic_apm_profiler/src/profiler/calltarget_tokens.rs b/src/elastic_apm_profiler/src/profiler/calltarget_tokens.rs new file mode 100644 index 000000000..adc1623c5 --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/calltarget_tokens.rs @@ -0,0 +1,1156 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + cil::{compress_data, compress_token, uncompress_data, Instruction, Method}, + ffi::{ + mdAssemblyRef, mdAssemblyRefNil, mdMemberRef, mdMemberRefNil, mdMethodSpec, mdToken, + mdTokenNil, mdTypeRef, mdTypeRefNil, mdTypeSpec, mdTypeSpecNil, CorAssemblyFlags, + CorCallingConvention, CorElementType, ASSEMBLYMETADATA, COR_SIGNATURE, E_FAIL, ULONG, + WCHAR, + }, + profiler::{ + managed, + types::{ + FunctionInfo, FunctionMethodArgument, MethodArgumentTypeFlag, ModuleMetadata, TypeInfo, + }, + }, +}; +use com::sys::HRESULT; +use std::ops::Deref; +use widestring::U16CString; + +/// Metadata tokens to modify call targets +pub struct CallTargetTokens { + cor_lib_assembly_ref: mdAssemblyRef, + object_type_ref: mdTypeRef, + ex_type_ref: mdTypeRef, + type_ref: mdTypeRef, + runtime_type_handle_ref: mdTypeRef, + get_type_from_handle_token: mdToken, + runtime_method_handle_ref: mdTypeRef, + profiler_assembly_ref: mdAssemblyRef, + call_target_type_ref: mdTypeRef, + call_target_state_type_ref: mdTypeRef, + call_target_return_void_type_ref: mdTypeRef, + call_target_return_type_ref: mdTypeRef, + begin_array_member_ref: mdMemberRef, + begin_method_fast_path_refs: Vec, + end_void_member_ref: mdMemberRef, + log_exception_ref: mdMemberRef, + call_target_state_type_get_default: mdMemberRef, + call_target_return_void_type_get_default: mdMemberRef, + get_default_member_ref: mdMemberRef, +} + +impl CallTargetTokens { + pub const FAST_PATH_COUNT: usize = 9; + pub fn new() -> Self { + Self { + cor_lib_assembly_ref: mdAssemblyRefNil, + object_type_ref: mdTypeRefNil, + ex_type_ref: mdTypeRefNil, + type_ref: mdTypeRefNil, + runtime_type_handle_ref: mdTypeRefNil, + get_type_from_handle_token: mdTokenNil, + runtime_method_handle_ref: mdTypeRefNil, + profiler_assembly_ref: mdAssemblyRefNil, + call_target_type_ref: mdTypeRefNil, + call_target_state_type_ref: mdTypeRefNil, + call_target_return_void_type_ref: mdTypeRefNil, + call_target_return_type_ref: mdTypeRefNil, + begin_array_member_ref: mdMemberRefNil, + begin_method_fast_path_refs: vec![mdMemberRefNil; Self::FAST_PATH_COUNT], + end_void_member_ref: mdMemberRefNil, + log_exception_ref: mdMemberRefNil, + call_target_state_type_get_default: mdMemberRefNil, + call_target_return_void_type_get_default: mdMemberRefNil, + get_default_member_ref: mdMemberRefNil, + } + } + + pub fn ensure_cor_lib_tokens( + &mut self, + module_metadata: &ModuleMetadata, + ) -> Result<(), HRESULT> { + if self.cor_lib_assembly_ref == mdAssemblyRefNil { + let cor_assembly_property = &module_metadata.cor_assembly_property; + let assembly_metadata = ASSEMBLYMETADATA { + usMajorVersion: cor_assembly_property.version.major, + usMinorVersion: cor_assembly_property.version.minor, + usBuildNumber: cor_assembly_property.version.build, + usRevisionNumber: cor_assembly_property.version.revision, + szLocale: std::ptr::null_mut(), + cbLocale: 0, + rProcessor: std::ptr::null_mut(), + ulProcessor: 0, + rOS: std::ptr::null_mut(), + ulOS: 0, + }; + + self.cor_lib_assembly_ref = module_metadata.assembly_emit.define_assembly_ref( + cor_assembly_property.public_key.bytes(), + &cor_assembly_property.name, + &assembly_metadata, + &(cor_assembly_property.public_key.hash_algorithm().unwrap() as u32).to_le_bytes(), + cor_assembly_property.assembly_flags, + )?; + } + + if self.object_type_ref == mdTypeRefNil { + self.object_type_ref = module_metadata + .emit + .define_type_ref_by_name(self.cor_lib_assembly_ref, "System.Object") + .map_err(|e| { + log::warn!("Could not define type_ref for System.Object"); + e + })?; + } + + if self.ex_type_ref == mdTypeRefNil { + self.ex_type_ref = module_metadata + .emit + .define_type_ref_by_name(self.cor_lib_assembly_ref, "System.Exception") + .map_err(|e| { + log::warn!("Could not define type_ref for System.Exception"); + e + })?; + } + + if self.type_ref == mdTypeRefNil { + self.type_ref = module_metadata + .emit + .define_type_ref_by_name(self.cor_lib_assembly_ref, "System.Type") + .map_err(|e| { + log::warn!("Could not define type_ref for System.Type"); + e + })?; + } + + if self.runtime_type_handle_ref == mdTypeRefNil { + self.runtime_type_handle_ref = module_metadata + .emit + .define_type_ref_by_name(self.cor_lib_assembly_ref, "System.RuntimeTypeHandle") + .map_err(|e| { + log::warn!("Could not define type_ref for System.RuntimeTypeHandle"); + e + })?; + } + + if self.get_type_from_handle_token == mdTokenNil { + let mut runtime_type_handle_compressed = + compress_token(self.runtime_type_handle_ref).unwrap(); + let mut type_ref_compressed = compress_token(self.type_ref).unwrap(); + + let mut signature = Vec::with_capacity( + 4 + runtime_type_handle_compressed.len() + type_ref_compressed.len(), + ); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits()); + signature.push(1); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.append(&mut type_ref_compressed); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.append(&mut runtime_type_handle_compressed); + + self.get_type_from_handle_token = module_metadata + .emit + .define_member_ref(self.type_ref, "GetTypeFromHandle", &signature) + .map_err(|e| { + log::warn!("Could not define get_type_from_handle_token"); + e + })?; + } + + if self.runtime_method_handle_ref == mdTypeRefNil { + self.runtime_method_handle_ref = module_metadata + .emit + .define_type_ref_by_name(self.cor_lib_assembly_ref, "System.RuntimeMethodHandle") + .map_err(|e| { + log::warn!("Could not define type_ref for System.RuntimeMethodHandle"); + e + })?; + } + + Ok(()) + } + + pub fn ensure_base_calltarget_tokens( + &mut self, + module_metadata: &ModuleMetadata, + ) -> Result<(), HRESULT> { + self.ensure_cor_lib_tokens(module_metadata)?; + + if self.profiler_assembly_ref == mdAssemblyRefNil { + let assembly_reference = + crate::profiler::managed::MANAGED_PROFILER_FULL_ASSEMBLY_VERSION.deref(); + + let (sz_locale, cb_locale) = if &assembly_reference.locale == "neutral" { + (std::ptr::null_mut() as *mut WCHAR, 0) + } else { + let wstr = U16CString::from_str(&assembly_reference.locale).unwrap(); + let len = wstr.len() as ULONG; + (wstr.into_vec().as_mut_ptr(), len) + }; + + let assembly_metadata = ASSEMBLYMETADATA { + usMajorVersion: assembly_reference.version.major, + usMinorVersion: assembly_reference.version.minor, + usBuildNumber: assembly_reference.version.build, + usRevisionNumber: assembly_reference.version.revision, + szLocale: sz_locale, + cbLocale: cb_locale, + rProcessor: std::ptr::null_mut(), + ulProcessor: 0, + rOS: std::ptr::null_mut(), + ulOS: 0, + }; + + let public_key_bytes = assembly_reference.public_key.into_bytes(); + self.profiler_assembly_ref = module_metadata + .assembly_emit + .define_assembly_ref( + &public_key_bytes, + &assembly_reference.name, + &assembly_metadata, + &[], + CorAssemblyFlags::empty(), + ) + .map_err(|e| { + log::warn!("Could not define profiler_assembly_ref"); + e + })?; + } + + if self.call_target_type_ref == mdTypeRefNil { + self.call_target_type_ref = module_metadata + .emit + .define_type_ref_by_name( + self.profiler_assembly_ref, + managed::MANAGED_PROFILER_CALLTARGET_TYPE, + ) + .map_err(|e| { + log::warn!( + "Could not define type_ref for {}", + managed::MANAGED_PROFILER_CALLTARGET_TYPE + ); + e + })?; + } + + if self.call_target_state_type_ref == mdTypeRefNil { + self.call_target_state_type_ref = module_metadata + .emit + .define_type_ref_by_name( + self.profiler_assembly_ref, + managed::MANAGED_PROFILER_CALLTARGET_STATETYPE, + ) + .map_err(|e| { + log::warn!( + "Could not define type_ref for {}", + managed::MANAGED_PROFILER_CALLTARGET_STATETYPE + ); + e + })?; + } + + if self.call_target_state_type_get_default == mdMemberRefNil { + let mut call_target_state_type_compressed = + compress_token(self.call_target_state_type_ref).unwrap(); + + let mut signature = Vec::with_capacity(3 + call_target_state_type_compressed.len()); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits()); + signature.push(0); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.append(&mut call_target_state_type_compressed); + + self.call_target_state_type_get_default = module_metadata + .emit + .define_member_ref( + self.call_target_state_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_STATETYPE_GETDEFAULT_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_STATETYPE_GETDEFAULT_NAME + ); + e + })?; + } + + Ok(()) + } + + pub fn get_cor_lib_assembly_ref(&self) -> mdAssemblyRef { + self.cor_lib_assembly_ref + } + + pub fn get_object_type_ref(&self) -> mdTypeRef { + self.object_type_ref + } + + pub fn get_ex_type_ref(&self) -> mdTypeRef { + self.ex_type_ref + } + + pub fn get_target_state_type_ref( + &mut self, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + Ok(self.call_target_state_type_ref) + } + + pub fn get_target_void_return_type_ref( + &mut self, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + if self.call_target_return_void_type_ref == mdTypeRefNil { + self.call_target_return_void_type_ref = module_metadata + .emit + .define_type_ref_by_name( + self.profiler_assembly_ref, + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE, + ) + .map_err(|e| { + log::warn!( + "Could not define type_ref for {}", + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE + ); + e + })?; + } + + Ok(self.call_target_return_void_type_ref) + } + + pub fn get_target_return_value_type_ref( + &mut self, + return_argument: &FunctionMethodArgument, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + if self.call_target_return_type_ref == mdTypeRefNil { + self.call_target_return_type_ref = module_metadata + .emit + .define_type_ref_by_name( + self.profiler_assembly_ref, + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GENERICS, + ) + .map_err(|e| { + log::warn!( + "Could not define type_ref for {}", + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GENERICS + ); + e + })?; + } + + let return_signature = return_argument.signature(); + let mut call_target_return_type_ref_compressed = + compress_token(self.call_target_return_type_ref).unwrap(); + + let mut signature = Vec::with_capacity( + 3 + return_signature.len() + call_target_return_type_ref_compressed.len(), + ); + signature.push(CorElementType::ELEMENT_TYPE_GENERICINST as COR_SIGNATURE); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.append(&mut call_target_return_type_ref_compressed); + signature.push(1); + signature.extend_from_slice(return_signature); + + let return_value_type_spec = module_metadata.emit.get_token_from_type_spec(&signature)?; + + Ok(return_value_type_spec) + } + + pub fn get_call_target_state_default_member_ref( + &mut self, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + Ok(self.call_target_state_type_get_default) + } + + pub fn get_call_target_return_void_default_member_ref( + &mut self, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + if self.call_target_return_void_type_get_default == mdMemberRefNil { + let call_target_return_void_type_compressed = + compress_token(self.call_target_return_void_type_ref).unwrap(); + + let mut signature = + Vec::with_capacity(3 + call_target_return_void_type_compressed.len()); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits()); + signature.push(0); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.extend_from_slice(&call_target_return_void_type_compressed); + + self.call_target_return_void_type_get_default = module_metadata + .emit + .define_member_ref( + self.call_target_return_void_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETDEFAULT_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETDEFAULT_NAME + ); + e + })?; + } + + Ok(self.call_target_return_void_type_get_default) + } + + pub fn get_call_target_return_value_default_member_ref( + &mut self, + call_target_return_type_spec: mdTypeSpec, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + if self.call_target_return_type_ref == mdTypeRefNil { + log::warn!("Could not define call_target_return_type_get_default because call_target_return_type_ref is null"); + return Err(E_FAIL); + } + + let mut call_target_return_type_compressed = + compress_token(self.call_target_return_type_ref).unwrap(); + + let mut signature = Vec::with_capacity(7 + call_target_return_type_compressed.len()); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits()); + signature.push(0); + signature.push(CorElementType::ELEMENT_TYPE_GENERICINST as COR_SIGNATURE); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.append(&mut call_target_return_type_compressed); + signature.push(1); + signature.push(CorElementType::ELEMENT_TYPE_VAR as COR_SIGNATURE); + signature.push(0); + + module_metadata + .emit + .define_member_ref( + call_target_return_type_spec, + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETDEFAULT_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}, {}", + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETDEFAULT_NAME, + e + ); + e + }) + } + + pub fn get_call_target_default_value_method_spec( + &mut self, + method_argument: &FunctionMethodArgument, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + if self.get_default_member_ref == mdMemberRefNil { + let signature = vec![ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC.bits(), + 1, + 0, + CorElementType::ELEMENT_TYPE_MVAR as COR_SIGNATURE, + 0, + ]; + + self.get_default_member_ref = module_metadata + .emit + .define_member_ref( + self.call_target_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_GETDEFAULTVALUE_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_GETDEFAULTVALUE_NAME + ); + e + })?; + } + + let method_argument_signature = method_argument.signature(); + let mut signature = Vec::with_capacity(2 + method_argument_signature.len()); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERICINST.bits()); + signature.push(1); + signature.extend_from_slice(&method_argument_signature); + + let default_method_spec = module_metadata + .emit + .define_method_spec(self.get_default_member_ref, &signature) + .map_err(|e| { + log::warn!("Could not define default method spec"); + e + })?; + + Ok(default_method_spec) + } + + fn get_current_type_ref(&self, current_type: &TypeInfo) -> (mdToken, bool) { + let mut is_value_type = current_type.is_value_type; + if current_type.type_spec != mdTypeSpecNil { + (current_type.type_spec, is_value_type) + } else { + let mut t = current_type; + while !t.is_generic { + if let Some(p) = &t.parent_type { + t = p; + } else { + return (t.id, is_value_type); + } + } + + is_value_type = false; + (self.object_type_ref, is_value_type) + } + } + + fn create_local_sig( + &mut self, + method: &Method, + method_return_value: &FunctionMethodArgument, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + let local_var_sig = method.header.local_var_sig_tok(); + let mut call_target_state_type_ref_compressed = + compress_token(self.call_target_state_type_ref).unwrap(); + let mut original_sig = vec![]; + let mut original_signature_size = 0; + + if local_var_sig != mdTokenNil { + original_sig = module_metadata.import.get_sig_from_token(local_var_sig)?; + original_signature_size += original_sig.len(); + let offset = original_sig.len() - call_target_state_type_ref_compressed.len(); + if offset > 0 + && original_sig[offset - 1] == CorElementType::ELEMENT_TYPE_VALUETYPE as u8 + && original_sig[offset..] == call_target_state_type_ref_compressed + { + log::warn!("method signature has already been modified"); + return Err(E_FAIL); + } + } + + let mut new_locals_count: usize = 3; + let mut ex_type_ref_compressed = compress_token(self.ex_type_ref).unwrap(); + let (_, ret_type_flags) = method_return_value.get_type_flags(); + + let call_target_return; + let mut call_target_return_signature = vec![]; + let mut call_target_return_compressed = vec![]; + let mut ret_sig = vec![]; + + if ret_type_flags != MethodArgumentTypeFlag::VOID { + ret_sig = method_return_value.signature().to_vec(); + call_target_return = + self.get_target_return_value_type_ref(method_return_value, module_metadata)?; + call_target_return_signature = { + let type_spec = module_metadata.import.get_type_spec_from_token( + call_target_return + ).map_err(|e| { + log::warn!("Could not get type spec from token, call_target_return={}, signature={:?}", call_target_return, &ret_sig); + e + })?; + type_spec.signature + }; + + new_locals_count += 1; + } else { + call_target_return = self.get_target_void_return_type_ref(module_metadata)?; + call_target_return_compressed = compress_token(call_target_return).unwrap(); + } + + let mut new_locals_buffer; + let mut old_locals_len = 0; + + if original_signature_size == 0 { + new_locals_buffer = compress_data(new_locals_count as ULONG).unwrap(); + } else { + let (data, len) = uncompress_data(&original_sig[1..]).unwrap(); + old_locals_len += len; + new_locals_count += data as usize; + new_locals_buffer = compress_data(new_locals_count as ULONG).unwrap(); + } + + let mut new_signature = vec![CorCallingConvention::IMAGE_CEE_CS_CALLCONV_LOCAL_SIG.bits()]; + new_signature.append(&mut new_locals_buffer); + + if original_signature_size > 0 { + new_signature.extend_from_slice(&original_sig[(1 + old_locals_len)..]); + } + + if !ret_sig.is_empty() { + new_signature.extend_from_slice(&ret_sig); + } + + new_signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + new_signature.append(&mut ex_type_ref_compressed); + + if !call_target_return_signature.is_empty() { + new_signature.append(&mut call_target_return_signature); + } else { + new_signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + new_signature.append(&mut call_target_return_compressed); + } + + new_signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + new_signature.append(&mut call_target_state_type_ref_compressed); + + let new_local_var_sig = module_metadata + .emit + .get_token_from_sig(&new_signature) + .map_err(|e| { + log::warn!( + "Error creating new local vars signature {:?}", + &new_signature + ); + e + })?; + + Ok(LocalSig { + new_local_var_sig, + call_target_state_token: self.call_target_state_type_ref, + exception_token: self.ex_type_ref, + call_target_return_token: call_target_return, + return_value_index: if !ret_sig.is_empty() { + new_locals_count - 4 + } else { + usize::MAX + }, + exception_index: new_locals_count - 3, + call_target_return_index: new_locals_count - 2, + call_target_state_index: new_locals_count - 1, + }) + } + + pub fn write_begin_method_with_arguments_array( + &mut self, + integration_type_ref: mdTypeRef, + current_type: &TypeInfo, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + if self.begin_array_member_ref == mdMemberRefNil { + let call_target_state_compressed = + compress_token(self.call_target_state_type_ref).unwrap(); + + let mut signature = Vec::with_capacity(8 + call_target_state_compressed.len()); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC.bits()); + signature.push(2); + signature.push(2); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.extend_from_slice(&call_target_state_compressed); + signature.push(CorElementType::ELEMENT_TYPE_MVAR as COR_SIGNATURE); + signature.push(1); + signature.push(CorElementType::ELEMENT_TYPE_SZARRAY as COR_SIGNATURE); + signature.push(CorElementType::ELEMENT_TYPE_OBJECT as COR_SIGNATURE); + + self.begin_array_member_ref = module_metadata + .emit + .define_member_ref( + self.call_target_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_BEGINMETHOD_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_BEGINMETHOD_NAME + ); + e + })?; + } + + let integration_type_compressed = compress_token(integration_type_ref).unwrap(); + let (current_type_ref, is_value_type) = self.get_current_type_ref(current_type); + let current_type_compressed = compress_token(current_type_ref).unwrap(); + + let mut signature = Vec::with_capacity( + 4 + integration_type_compressed.len() + current_type_compressed.len(), + ); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERICINST.bits()); + signature.push(2); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.extend_from_slice(&integration_type_compressed); + + if is_value_type { + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + } else { + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + } + + signature.extend_from_slice(¤t_type_compressed); + + let begin_array_method_spec = module_metadata + .emit + .define_method_spec(self.begin_array_member_ref, &signature) + .map_err(|e| { + log::warn!( + "Could not define method spec for {}", + managed::MANAGED_PROFILER_CALLTARGET_BEGINMETHOD_NAME + ); + e + })?; + + Ok(Instruction::call(begin_array_method_spec)) + } + + pub fn modify_local_sig_and_initialize( + &mut self, + method: &Method, + function_info: &FunctionInfo, + module_metadata: &ModuleMetadata, + ) -> Result<(LocalSig, Vec), HRESULT> { + // TODO: cache the parsed method in method_signature... + let parsed_method = function_info.method_signature.try_parse().unwrap(); + let return_function_method = parsed_method.return_type(); + + let local_sig = self.create_local_sig(method, &return_function_method, module_metadata)?; + let mut instructions = Vec::with_capacity(6); + + if local_sig.return_value_index != usize::MAX { + let call_target_default_value = self.get_call_target_default_value_method_spec( + &return_function_method, + module_metadata, + )?; + + instructions.push(Instruction::call(call_target_default_value)); + instructions.push(Instruction::store_local( + local_sig.return_value_index as u16, + )); + + let call_target_return_value = self.get_call_target_return_value_default_member_ref( + local_sig.call_target_return_token, + module_metadata, + )?; + + instructions.push(Instruction::call(call_target_return_value)); + instructions.push(Instruction::store_local( + local_sig.call_target_return_index as u16, + )); + } else { + let call_target_void = + self.get_call_target_return_void_default_member_ref(module_metadata)?; + instructions.push(Instruction::call(call_target_void)); + instructions.push(Instruction::store_local( + local_sig.call_target_return_index as u16, + )); + } + + instructions.push(Instruction::ldnull()); + instructions.push(Instruction::store_local(local_sig.exception_index as u16)); + Ok((local_sig, instructions)) + } + + pub fn write_begin_method( + &mut self, + integration_type_ref: mdTypeRef, + current_type: &TypeInfo, + method_arguments: &[FunctionMethodArgument], + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + let len = method_arguments.len(); + if len >= Self::FAST_PATH_COUNT { + // slow path + return self.write_begin_method_with_arguments_array( + integration_type_ref, + current_type, + module_metadata, + ); + } + + // fast path + if self.begin_method_fast_path_refs[len] == mdMemberRefNil { + let call_target_state = compress_token(self.call_target_state_type_ref).unwrap(); + + let mut signature = Vec::with_capacity(6 + (len * 2) + call_target_state.len()); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC.bits()); + signature.push(2 + len as u8); + signature.push(1 + len as u8); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.extend_from_slice(&call_target_state); + signature.push(CorElementType::ELEMENT_TYPE_MVAR as COR_SIGNATURE); + signature.push(1); + + for i in 0..len { + signature.push(CorElementType::ELEMENT_TYPE_MVAR as COR_SIGNATURE); + signature.push(1 + (i as u8 + 1)); + } + + self.begin_method_fast_path_refs[len] = module_metadata + .emit + .define_member_ref( + self.call_target_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_BEGINMETHOD_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_BEGINMETHOD_NAME + ); + e + })? + } + + let integration_type_ref_compressed = compress_token(integration_type_ref).unwrap(); + + let (current_type_ref, is_value_type) = self.get_current_type_ref(current_type); + + let current_type_ref_compressed = compress_token(current_type_ref).unwrap(); + + let mut arguments_signature = Vec::with_capacity(Self::FAST_PATH_COUNT); + for method_argument in method_arguments { + let sig = method_argument.signature(); + arguments_signature.extend_from_slice(sig); + } + + let mut signature = Vec::with_capacity( + 4 + integration_type_ref_compressed.len() + + current_type_ref_compressed.len() + + arguments_signature.len(), + ); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERICINST.bits()); + signature.push(2 + len as u8); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.extend_from_slice(&integration_type_ref_compressed); + + if is_value_type { + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + } else { + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + } + + signature.extend_from_slice(¤t_type_ref_compressed); + signature.extend_from_slice(&arguments_signature); + + let begin_method_spec = module_metadata + .emit + .define_method_spec(self.begin_method_fast_path_refs[len], &signature) + .map_err(|e| { + log::warn!("Could not define member spec for fast path args {}", len); + e + })?; + + Ok(Instruction::call(begin_method_spec)) + } + + pub fn write_end_void_return_member_ref( + &mut self, + integration_type_ref: mdTypeRef, + current_type: &TypeInfo, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + if self.end_void_member_ref == mdMemberRefNil { + let mut call_target_return_void_compressed = + compress_token(self.call_target_return_void_type_ref).unwrap(); + + let mut ex_type_ref_compressed = compress_token(self.ex_type_ref).unwrap(); + + let mut call_target_state = compress_token(self.call_target_state_type_ref).unwrap(); + + let mut signature = Vec::with_capacity( + 8 + call_target_return_void_compressed.len() + + ex_type_ref_compressed.len() + + call_target_state.len(), + ); + + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC.bits()); + signature.push(2); + signature.push(3); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as u8); + signature.append(&mut call_target_return_void_compressed); + + signature.push(CorElementType::ELEMENT_TYPE_MVAR as u8); + signature.push(1); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as u8); + signature.append(&mut ex_type_ref_compressed); + + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as u8); + signature.append(&mut call_target_state); + + self.end_void_member_ref = module_metadata + .emit + .define_member_ref( + self.call_target_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_ENDMETHOD_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_ENDMETHOD_NAME + ); + e + })?; + } + + let mut integration_type_ref_compressed = compress_token(integration_type_ref).unwrap(); + + let (current_type_ref, is_value_type) = self.get_current_type_ref(current_type); + + let mut current_type_ref_compressed = compress_token(current_type_ref).unwrap(); + + let mut signature = Vec::with_capacity( + 4 + integration_type_ref_compressed.len() + current_type_ref_compressed.len(), + ); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERICINST.bits()); + signature.push(2); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.append(&mut integration_type_ref_compressed); + + if is_value_type { + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + } else { + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + } + + signature.append(&mut current_type_ref_compressed); + + let end_void_method_spec = module_metadata + .emit + .define_method_spec(self.end_void_member_ref, &signature) + .map_err(|e| { + log::warn!("Could not define member spec for end void method"); + e + })?; + + Ok(Instruction::call(end_void_method_spec)) + } + + pub fn write_end_return_member_ref( + &mut self, + integration_type_ref: mdTypeRef, + current_type: &TypeInfo, + return_argument: &FunctionMethodArgument, + module_metadata: &ModuleMetadata, + ) -> Result { + let return_type_spec = + self.get_target_return_value_type_ref(return_argument, module_metadata)?; + + let mut call_target_return_type_compressed = + compress_token(self.call_target_return_type_ref).unwrap(); + + let mut ex_type_ref_compressed = compress_token(self.ex_type_ref).unwrap(); + + let mut call_target_state = compress_token(self.call_target_state_type_ref).unwrap(); + + let mut signature = Vec::with_capacity( + 14 + call_target_return_type_compressed.len() + + ex_type_ref_compressed.len() + + call_target_state.len(), + ); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC.bits()); + signature.push(3); + signature.push(4); + signature.push(CorElementType::ELEMENT_TYPE_GENERICINST as COR_SIGNATURE); + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.append(&mut call_target_return_type_compressed); + signature.push(1); + signature.push(CorElementType::ELEMENT_TYPE_MVAR as COR_SIGNATURE); + signature.push(2); + + signature.push(CorElementType::ELEMENT_TYPE_MVAR as COR_SIGNATURE); + signature.push(1); + + signature.push(CorElementType::ELEMENT_TYPE_MVAR as COR_SIGNATURE); + signature.push(2); + + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.append(&mut ex_type_ref_compressed); + + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + signature.append(&mut call_target_state); + + let end_method_member_ref = module_metadata + .emit + .define_member_ref( + self.call_target_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_ENDMETHOD_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_ENDMETHOD_NAME + ); + e + })?; + + let mut integration_type_ref_compressed = compress_token(integration_type_ref).unwrap(); + + let (current_type_ref, is_value_type) = self.get_current_type_ref(current_type); + + let mut current_type_ref_compressed = compress_token(current_type_ref).unwrap(); + + let ret_type_sig = return_argument.signature(); + + let mut signature = Vec::with_capacity( + 4 + integration_type_ref_compressed.len() + + current_type_ref_compressed.len() + + ret_type_sig.len(), + ); + + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERICINST.bits()); + signature.push(3); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.append(&mut integration_type_ref_compressed); + + if is_value_type { + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + } else { + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + } + + signature.append(&mut current_type_ref_compressed); + signature.extend_from_slice(ret_type_sig); + + let end_method_spec = module_metadata + .emit + .define_method_spec(end_method_member_ref, &signature) + .map_err(|e| { + log::warn!("Could not define member spec for end method"); + e + })?; + + Ok(Instruction::call(end_method_spec)) + } + + pub fn write_log_exception( + &mut self, + integration_type_ref: mdTypeRef, + current_type: &TypeInfo, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + if self.log_exception_ref == mdMemberRefNil { + let mut ex_type_ref = compress_token(self.ex_type_ref).unwrap(); + let mut signature = Vec::with_capacity(5 + ex_type_ref.len()); + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC.bits()); + signature.push(2); + signature.push(1); + signature.push(CorElementType::ELEMENT_TYPE_VOID as COR_SIGNATURE); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.append(&mut ex_type_ref); + + self.log_exception_ref = module_metadata + .emit + .define_member_ref( + self.call_target_type_ref, + managed::MANAGED_PROFILER_CALLTARGET_LOGEXCEPTION_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_LOGEXCEPTION_NAME + ); + e + })?; + } + + let mut integration_type_ref_compressed = compress_token(integration_type_ref).unwrap(); + + let (current_type_ref, is_value_type) = self.get_current_type_ref(current_type); + + let mut current_type_ref_compressed = compress_token(current_type_ref).unwrap(); + let mut signature = Vec::with_capacity( + 4 + integration_type_ref_compressed.len() + current_type_ref_compressed.len(), + ); + + signature.push(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERICINST.bits()); + signature.push(2); + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + signature.append(&mut integration_type_ref_compressed); + + if is_value_type { + signature.push(CorElementType::ELEMENT_TYPE_VALUETYPE as COR_SIGNATURE); + } else { + signature.push(CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE); + } + + signature.append(&mut current_type_ref_compressed); + + let log_exception_method_spec = module_metadata + .emit + .define_method_spec(self.log_exception_ref, &signature) + .map_err(|e| { + log::warn!("Could not define member spec for log exception method"); + e + })?; + + Ok(Instruction::call(log_exception_method_spec)) + } + + pub fn write_call_target_return_get_return_value( + &mut self, + call_target_return_type_spec: mdTypeSpec, + module_metadata: &ModuleMetadata, + ) -> Result { + self.ensure_base_calltarget_tokens(module_metadata)?; + + let signature = vec![ + (CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT + | CorCallingConvention::IMAGE_CEE_CS_CALLCONV_HASTHIS) + .bits(), + 0, + CorElementType::ELEMENT_TYPE_VAR as COR_SIGNATURE, + 0, + ]; + + let call_target_return_get_value_member_ref = module_metadata + .emit + .define_member_ref( + call_target_return_type_spec, + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETRETURNVALUE_NAME, + &signature, + ) + .map_err(|e| { + log::warn!( + "Could not define member ref {}", + managed::MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETRETURNVALUE_NAME + ); + e + })?; + + Ok(Instruction::call(call_target_return_get_value_member_ref)) + } +} + +/// Metadata about a local signature +#[derive(Debug)] +pub struct LocalSig { + pub new_local_var_sig: mdToken, + pub call_target_state_token: mdTypeRef, + pub exception_token: mdTypeRef, + pub call_target_return_token: mdToken, + pub return_value_index: usize, + pub exception_index: usize, + pub call_target_return_index: usize, + pub call_target_state_index: usize, +} diff --git a/src/elastic_apm_profiler/src/profiler/env.rs b/src/elastic_apm_profiler/src/profiler/env.rs new file mode 100644 index 000000000..b7b82944a --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/env.rs @@ -0,0 +1,458 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ffi::E_FAIL, profiler::types::Integration}; +use com::sys::HRESULT; +use log::LevelFilter; +use log4rs::{ + append::{ + console::ConsoleAppender, + rolling_file::{ + policy::compound::{ + roll::fixed_window::FixedWindowRoller, trigger::size::SizeTrigger, CompoundPolicy, + }, + RollingFileAppender, + }, + }, + config::{Appender, Root}, + encode::pattern::PatternEncoder, + Config, Handle, +}; +use once_cell::sync::Lazy; +use std::{collections::HashSet, fs::File, io::BufReader, path::PathBuf, str::FromStr}; + +const APP_POOL_ID_ENV_VAR: &str = "APP_POOL_ID"; +const DOTNET_CLI_TELEMETRY_PROFILE_ENV_VAR: &str = "DOTNET_CLI_TELEMETRY_PROFILE"; +const COMPLUS_LOADEROPTIMIZATION: &str = "COMPLUS_LOADEROPTIMIZATION"; + +const ELASTIC_APM_PROFILER_CALLTARGET_ENABLED_ENV_VAR: &str = + "ELASTIC_APM_PROFILER_CALLTARGET_ENABLED"; +const ELASTIC_APM_PROFILER_DISABLE_OPTIMIZATIONS_ENV_VAR: &str = + "ELASTIC_APM_PROFILER_DISABLE_OPTIMIZATIONS"; +const ELASTIC_APM_PROFILER_ENABLE_INLINING_ENV_VAR: &str = "ELASTIC_APM_PROFILER_ENABLE_INLINING"; +const ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS_ENV_VAR: &str = + "ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS"; +const ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES_ENV_VAR: &str = + "ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES"; +const ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES_ENV_VAR: &str = + "ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES"; +const ELASTIC_APM_PROFILER_HOME_ENV_VAR: &str = "ELASTIC_APM_PROFILER_HOME"; +const ELASTIC_APM_PROFILER_INTEGRATIONS_ENV_VAR: &str = "ELASTIC_APM_PROFILER_INTEGRATIONS"; +const ELASTIC_APM_PROFILER_LOG_DIR_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_DIR"; +const ELASTIC_APM_PROFILER_LOG_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG"; +const ELASTIC_APM_PROFILER_LOG_TARGETS_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_TARGETS"; +const ELASTIC_APM_PROFILER_LOG_IL_ENV_VAR: &str = "ELASTIC_APM_PROFILER_LOG_IL"; + +const ELASTIC_APM_SERVICE_NAME_ENV_VAR: &str = "ELASTIC_APM_SERVICE_NAME"; + +pub static ELASTIC_APM_PROFILER_LOG_IL: Lazy = + Lazy::new(|| read_bool_env_var(ELASTIC_APM_PROFILER_LOG_IL_ENV_VAR, false)); + +pub static ELASTIC_APM_PROFILER_CALLTARGET_ENABLED: Lazy = + Lazy::new(|| read_bool_env_var(ELASTIC_APM_PROFILER_CALLTARGET_ENABLED_ENV_VAR, true)); + +pub static IS_AZURE_APP_SERVICE: Lazy = Lazy::new(|| { + std::env::var("WEBSITE_SITE_NAME").is_ok() + && std::env::var("WEBSITE_OWNER_NAME").is_ok() + && std::env::var("WEBSITE_RESOURCE_GROUP").is_ok() + && std::env::var("WEBSITE_INSTANCE_ID").is_ok() +}); + +/// Checks if the profiler is running in Azure App Service, in an infrastructure or +/// reserved process, and returns an error if so +pub fn check_if_running_in_azure_app_service() -> Result<(), HRESULT> { + if *IS_AZURE_APP_SERVICE { + log::info!("Initialize: detected Azure App Service context"); + if let Some(app_pool_id) = std::env::var(APP_POOL_ID_ENV_VAR).ok() { + if app_pool_id.starts_with('~') { + log::info!( + "Initialize: {} environment variable value {} suggests \ + this is an Azure App Service infrastructure process. Profiler disabled", + APP_POOL_ID_ENV_VAR, + app_pool_id + ); + return Err(E_FAIL); + } + } + + if let Some(cli_telemetry) = std::env::var(DOTNET_CLI_TELEMETRY_PROFILE_ENV_VAR).ok() { + if &cli_telemetry == "AzureKudu" { + log::info!( + "Initialize: {} environment variable value {} suggests \ + this is an Azure App Service reserved process. Profiler disabled", + DOTNET_CLI_TELEMETRY_PROFILE_ENV_VAR, + cli_telemetry + ); + return Err(E_FAIL); + } + } + } + + Ok(()) +} + +/// Gets the environment variables of interest +pub fn get_env_vars() -> String { + std::env::vars() + .filter_map(|(k, v)| { + let key = k.to_uppercase(); + if key.starts_with("ELASTIC_") + || key.starts_with("CORECLR_") + || key.starts_with("COR_") + || &key == APP_POOL_ID_ENV_VAR + || &key == DOTNET_CLI_TELEMETRY_PROFILE_ENV_VAR + || &key == COMPLUS_LOADEROPTIMIZATION + { + let value = if key.contains("SECRET") || key.contains("API_KEY") { + "[REDACTED]" + } else { + &v + }; + Some(format!(" {}=\"{}\"", k, value)) + } else { + None + } + }) + .collect::>() + .join("\n") +} + +fn read_semicolon_separated_env_var(key: &str) -> Option> { + match std::env::var(key) { + Ok(val) => Some(val.split(';').map(|s| s.to_string()).collect()), + Err(_) => None, + } +} + +pub fn get_exclude_processes() -> Option> { + read_semicolon_separated_env_var(ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES_ENV_VAR) +} + +pub fn get_exclude_service_names() -> Option> { + read_semicolon_separated_env_var(ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES_ENV_VAR) +} + +pub fn get_service_name() -> Option { + std::env::var(ELASTIC_APM_SERVICE_NAME_ENV_VAR).ok() +} + +/// Gets the path to the profiler file on windows +#[cfg(target_os = "windows")] +pub fn get_native_profiler_file() -> Result { + Ok("elastic_apm_profiler.dll".into()) +} + +/// Gets the path to the profiler file on non windows +#[cfg(not(target_os = "windows"))] +pub fn get_native_profiler_file() -> Result { + let env_var = if cfg!(target_pointer_width = "64") { + "CORECLR_PROFILER_PATH_64" + } else { + "CORECLR_PROFILER_PATH_32" + }; + match std::env::var(env_var) { + Ok(v) => Ok(v), + Err(_) => std::env::var("CORECLR_PROFILER_PATH").map_err(|e| { + log::warn!( + "problem getting env var CORECLR_PROFILER_PATH: {}", + e.to_string() + ); + E_FAIL + }), + } +} + +pub fn disable_optimizations() -> bool { + read_bool_env_var(ELASTIC_APM_PROFILER_DISABLE_OPTIMIZATIONS_ENV_VAR, false) +} + +pub fn enable_inlining(default: bool) -> bool { + read_bool_env_var(ELASTIC_APM_PROFILER_ENABLE_INLINING_ENV_VAR, default) +} + +fn read_log_targets_from_env_var() -> HashSet { + let mut set = match std::env::var(ELASTIC_APM_PROFILER_LOG_TARGETS_ENV_VAR) { + Ok(value) => value + .split(';') + .into_iter() + .filter_map(|s| match s.to_lowercase().as_str() { + out if out == "file" || out == "stdout" => Some(out.into()), + _ => None, + }) + .collect(), + _ => HashSet::with_capacity(1), + }; + + if set.is_empty() { + set.insert("file".into()); + } + set +} + +pub fn read_log_level_from_env_var(default: LevelFilter) -> LevelFilter { + match std::env::var(ELASTIC_APM_PROFILER_LOG_ENV_VAR) { + Ok(value) => LevelFilter::from_str(value.as_str()).unwrap_or(default), + _ => default, + } +} + +fn read_bool_env_var(key: &str, default: bool) -> bool { + match std::env::var(key) { + Ok(enabled) => match enabled.to_lowercase().as_str() { + "true" | "1" => true, + "false" | "0" => false, + _ => { + log::warn!( + "Unknown value for {}: {}. Setting to {}", + key, + enabled, + default + ); + default + } + }, + Err(e) => { + log::debug!( + "Problem reading {}: {}. Setting to {}", + key, + e.to_string(), + default + ); + default + } + } +} + +/// get the profiler directory +fn get_profiler_dir() -> String { + let env_var = if cfg!(target_pointer_width = "64") { + "CORECLR_PROFILER_PATH_64" + } else { + "CORECLR_PROFILER_PATH_32" + }; + + match std::env::var(env_var) { + Ok(v) => v, + Err(_) => match std::env::var("CORECLR_PROFILER_PATH") { + Ok(v) => v, + Err(_) => { + // try .NET Framework env vars + let env_var = if cfg!(target_pointer_width = "64") { + "COR_PROFILER_PATH_64" + } else { + "COR_PROFILER_PATH_32" + }; + + match std::env::var(env_var) { + Ok(v) => v, + Err(_) => std::env::var("COR_PROFILER_PATH").unwrap_or_else(|_| String::new()), + } + } + }, + } +} + +/// Gets the default log directory on Windows +#[cfg(target_os = "windows")] +fn get_default_log_dir() -> PathBuf { + // ideally we would use the windows function SHGetKnownFolderPath to get + // the CommonApplicationData special folder. However, this requires a few package dependencies + // like winapi that would increase the size of the profiler binary. Instead, + // use the %PROGRAMDATA% environment variable if it exists + match std::env::var("PROGRAMDATA") { + Ok(path) => { + let mut path_buf = PathBuf::from(path); + path_buf.push("elastic"); + path_buf.push("apm-agent-dotnet"); + path_buf.push("logs"); + path_buf + } + Err(_) => get_home_log_dir(), + } +} + +/// Gets the path to the profiler file on non windows +#[cfg(not(target_os = "windows"))] +fn get_default_log_dir() -> PathBuf { + PathBuf::from_str("/var/log/elastic/apm-agent-dotnet").unwrap() +} + +fn get_home_log_dir() -> PathBuf { + let mut path_buf = match std::env::var(ELASTIC_APM_PROFILER_HOME_ENV_VAR) { + Ok(val) => PathBuf::from(val), + Err(_) => PathBuf::from(get_profiler_dir()), + }; + + path_buf.push("logs"); + path_buf +} + +fn get_log_dir() -> PathBuf { + match std::env::var(ELASTIC_APM_PROFILER_LOG_DIR_ENV_VAR) { + Ok(path) => PathBuf::from(path), + Err(_) => get_default_log_dir(), + } +} + +pub fn initialize_logging(process_name: &str) -> Handle { + let targets = read_log_targets_from_env_var(); + let level = read_log_level_from_env_var(LevelFilter::Warn); + let mut root_builder = Root::builder(); + let mut config_builder = Config::builder(); + let log_pattern = "[{d(%Y-%m-%dT%H:%M:%S.%f%:z)}] [{l:<5}] {m}{n}"; + + if targets.contains("stdout") { + let pattern = PatternEncoder::new(log_pattern); + let stdout = ConsoleAppender::builder() + .encoder(Box::new(pattern)) + .build(); + config_builder = + config_builder.appender(Appender::builder().build("stdout", Box::new(stdout))); + root_builder = root_builder.appender("stdout"); + } + + if targets.contains("file") { + let pid = std::process::id(); + let mut log_dir = get_log_dir(); + let mut valid_log_dir = true; + + // try to create the log directory ahead of time so that we can determine if it's a valid + // directory. if the directory can't be created, try the default log directory or home directory + // before bailing and not setting up the file logger. + if let Err(_) = std::fs::create_dir_all(&log_dir) { + let default_log_dir = get_default_log_dir(); + if log_dir != default_log_dir { + log_dir = default_log_dir; + if let Err(_) = std::fs::create_dir_all(&log_dir) { + let home_log_dir = get_home_log_dir(); + if log_dir != home_log_dir { + log_dir = home_log_dir; + if let Err(e) = std::fs::create_dir_all(&log_dir) { + valid_log_dir = false; + } + } else { + valid_log_dir = false; + } + } + } else { + log_dir = get_home_log_dir(); + if let Err(e) = std::fs::create_dir_all(&log_dir) { + valid_log_dir = false; + } + } + } + + if valid_log_dir { + let log_file_name = log_dir + .clone() + .join(format!("elastic_apm_profiler_{}_{}.log", process_name, pid)) + .to_string_lossy() + .to_string(); + let rolling_log_file_name = log_dir + .clone() + .join(format!( + "elastic_apm_profiler_{}_{}_{{}}.log", + process_name, pid + )) + .to_string_lossy() + .to_string(); + + let trigger = SizeTrigger::new(5 * 1024 * 1024); + let roller = FixedWindowRoller::builder() + .build(&rolling_log_file_name, 10) + .unwrap(); + + let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller)); + let pattern = PatternEncoder::new(log_pattern); + let file = RollingFileAppender::builder() + .append(true) + .encoder(Box::new(pattern)) + .build(&log_file_name, Box::new(policy)) + .unwrap(); + + config_builder = + config_builder.appender(Appender::builder().build("file", Box::new(file))); + root_builder = root_builder.appender("file"); + } + } + + let root = root_builder.build(level); + let config = config_builder.build(root).unwrap(); + log4rs::init_config(config).unwrap() +} + +/// Loads the integrations by reading the yml file pointed to +/// by [ELASTIC_APM_PROFILER_INTEGRATIONS] environment variable, filtering +/// integrations by [ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS_ENV_VAR] environment variable, +/// if present +pub fn load_integrations() -> Result, HRESULT> { + let path = match std::env::var(ELASTIC_APM_PROFILER_INTEGRATIONS_ENV_VAR) { + Ok(val) => val, + Err(e) => { + log::debug!( + "problem reading {} environment variable: {}. trying integrations.yml in directory of {} environment variable value", + ELASTIC_APM_PROFILER_INTEGRATIONS_ENV_VAR, + e.to_string(), + ELASTIC_APM_PROFILER_HOME_ENV_VAR + ); + + match std::env::var(ELASTIC_APM_PROFILER_HOME_ENV_VAR) { + Ok(val) => { + let mut path_buf = PathBuf::from(val); + path_buf.push("integrations.yml"); + path_buf.to_string_lossy().to_string() + } + Err(e) => { + log::warn!( + "problem reading {} environment variable: {}. profiler disabled", + ELASTIC_APM_PROFILER_HOME_ENV_VAR, + e.to_string(), + ); + return Err(E_FAIL); + } + } + } + }; + + let file = File::open(&path).map_err(|e| { + log::warn!( + "problem reading integrations file {}: {}. profiler is disabled.", + &path, + e.to_string() + ); + E_FAIL + })?; + + let reader = BufReader::new(file); + let mut integrations: Vec = serde_yaml::from_reader(reader).map_err(|e| { + log::warn!( + "problem reading integrations file {}: {}. profiler is disabled.", + &path, + e.to_string() + ); + E_FAIL + })?; + + log::trace!( + "loaded {} integration(s) from {}", + integrations.len(), + &path + ); + + // Now filter integrations + match std::env::var(ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS_ENV_VAR) { + Ok(val) => { + let exclude_integrations = val.split(';'); + for exclude_integration in exclude_integrations { + log::trace!("exclude integrations that match {}", exclude_integration); + integrations + .retain(|i| i.name.to_lowercase() != exclude_integration.to_lowercase()); + } + } + Err(_) => (), + }; + + Ok(integrations) +} diff --git a/src/elastic_apm_profiler/src/profiler/helpers.rs b/src/elastic_apm_profiler/src/profiler/helpers.rs new file mode 100644 index 000000000..1aa5bb5d9 --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/helpers.rs @@ -0,0 +1,684 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + cil::{ + uncompress_data, uncompress_token, CorExceptionFlag, Method, + Operand::{ + InlineBrTarget, InlineField, InlineI, InlineI8, InlineMethod, InlineString, InlineType, + ShortInlineBrTarget, ShortInlineI, ShortInlineVar, + }, + Section, BOX, CALL, CALLVIRT, CASTCLASS, INITOBJ, LDSTR, NEWARR, NEWOBJ, UNBOX_ANY, + }, + ffi::{ + mdAssemblyRef, mdToken, mdTokenNil, mdTypeDef, mdTypeDefNil, type_from_token, + CorElementType, CorTokenType, ASSEMBLYMETADATA, + }, + interfaces::{IMetaDataAssemblyEmit, IMetaDataEmit2, IMetaDataImport2}, + profiler::{ + sig::parse_type, + types::{ + AssemblyMetaData, FunctionInfo, Integration, IntegrationMethod, MethodSignature, + ModuleMetadata, + }, + }, +}; +use com::sys::HRESULT; +use num_traits::FromPrimitive; + +pub(crate) fn return_type_is_value_type_or_generic( + module_metadata: &ModuleMetadata, + target_function_token: mdToken, + target_function_signature: &MethodSignature, +) -> Option { + let metadata_import = &module_metadata.import; + let metadata_emit = &module_metadata.emit; + let assembly_emit = &module_metadata.assembly_emit; + let cor_assembly = &module_metadata.cor_assembly_property; + let generic_count = target_function_signature.type_arguments_len(); + let mut method_def_sig_index = target_function_signature.index_of_return_type(); + let signature = target_function_signature.bytes(); + let ret_type_byte = signature[method_def_sig_index]; + let spec_signature; + + if let Some(ret_type) = CorElementType::from_u8(ret_type_byte) { + match ret_type { + CorElementType::ELEMENT_TYPE_VOID => { + return None; + } + CorElementType::ELEMENT_TYPE_GENERICINST => { + return if let Some(CorElementType::ELEMENT_TYPE_VALUETYPE) = + CorElementType::from_u8(signature[method_def_sig_index + 1]) + { + let start_idx = method_def_sig_index; + let mut end_idx = start_idx; + if let Some(type_idx) = parse_type(&signature[method_def_sig_index..]) { + end_idx += type_idx; + metadata_emit + .get_token_from_type_spec(&signature[start_idx..end_idx]) + .ok() + } else { + None + } + } else { + None + } + } + CorElementType::ELEMENT_TYPE_VAR | CorElementType::ELEMENT_TYPE_MVAR => { + method_def_sig_index += 1; + return if let Some((generic_type_index, generic_type_len)) = + uncompress_data(&signature[method_def_sig_index..]) + { + let generic_type_index = generic_type_index as usize; + let token_type = { + let t = type_from_token(target_function_token); + CorTokenType::from_bits(t).unwrap() + }; + let parent_token; + match token_type { + CorTokenType::mdtMemberRef => { + if generic_count > 0 { + return None; + } + + if let Ok(member_ref_props) = + metadata_import.get_member_ref_props(target_function_token) + { + parent_token = member_ref_props.class_token; + if let Ok(type_spec) = + metadata_import.get_type_spec_from_token(parent_token) + { + spec_signature = type_spec.signature; + } else { + log::warn!("element_type={:?}: failed to get parent token or signature", ret_type); + return None; + } + } else { + log::warn!( + "element_type={:?}: failed to get parent token or signature", + ret_type + ); + return None; + } + } + CorTokenType::mdtMethodDef => { + if generic_count > 0 { + return None; + } + + if let Ok(member_props) = + metadata_import.get_member_props(target_function_token) + { + parent_token = member_props.class_token; + if let Ok(type_spec) = + metadata_import.get_type_spec_from_token(parent_token) + { + spec_signature = type_spec.signature; + } else { + log::warn!("element_type={:?}: failed to get parent token or signature", ret_type); + return None; + } + } else { + log::warn!( + "element_type={:?}: failed to get parent token or signature", + ret_type + ); + return None; + } + } + CorTokenType::mdtMethodSpec => { + if let Ok(method_spec_props) = + metadata_import.get_method_spec_props(target_function_token) + { + parent_token = method_spec_props.parent; + spec_signature = method_spec_props.signature; + } else { + log::warn!( + "element_type={:?}: failed to get parent token or signature", + ret_type + ); + return None; + } + } + _ => { + return None; + } + } + + let mut parent_token_index = 0; + if token_type == CorTokenType::mdtMemberRef + || token_type == CorTokenType::mdtMethodDef + { + parent_token_index = 2; + let (token, len) = uncompress_token(&spec_signature[parent_token_index..]); + parent_token_index += len; + } else if token_type == CorTokenType::mdtMethodSpec { + parent_token_index = 1; + } + + let mut num_generic_arguments = 0; + if let Some((token, len)) = + uncompress_data(&spec_signature[parent_token_index..]) + { + parent_token_index += len; + num_generic_arguments = token as usize; + } + + let mut current_idx = parent_token_index; + + for i in 0..num_generic_arguments { + if i != generic_type_index { + if let Some(type_idx) = parse_type(&spec_signature[current_idx..]) { + current_idx += type_idx; + } else { + log::warn!("element_type={:?}: unable to parse generic type argument {} from parent token signature {}", ret_type, i, parent_token); + return None; + } + } else if let Some(element_type) = + CorElementType::from_u8(spec_signature[current_idx]) + { + return match element_type { + CorElementType::ELEMENT_TYPE_MVAR + | CorElementType::ELEMENT_TYPE_VAR => metadata_emit + .get_token_from_type_spec( + &spec_signature[current_idx..(current_idx + 2)], + ) + .ok(), + CorElementType::ELEMENT_TYPE_GENERICINST => { + if let Some(CorElementType::ELEMENT_TYPE_VALUETYPE) = + CorElementType::from_u8(spec_signature[current_idx + 1]) + { + let start_idx = current_idx; + let mut end_idx = start_idx; + if let Some(type_idx) = + parse_type(&spec_signature[end_idx..]) + { + end_idx += type_idx; + } else { + return None; + } + metadata_emit + .get_token_from_type_spec( + &spec_signature[start_idx..end_idx], + ) + .ok() + } else { + None + } + } + _ => return_type_token_for_value_type_element_type( + &spec_signature[current_idx..], + metadata_emit, + assembly_emit, + cor_assembly, + ), + }; + } else { + return return_type_token_for_value_type_element_type( + &spec_signature[current_idx..], + metadata_emit, + assembly_emit, + cor_assembly, + ); + } + } + + None + } else { + log::warn!( + "element_type={:?}: unable to retrieve VAR|MVAR index", + ret_type + ); + None + }; + } + _ => {} + } + } + + return_type_token_for_value_type_element_type( + &signature[method_def_sig_index..], + metadata_emit, + assembly_emit, + cor_assembly, + ) +} + +fn return_type_token_for_value_type_element_type( + signature: &[u8], + metadata_emit: &IMetaDataEmit2, + assembly_emit: &IMetaDataAssemblyEmit, + cor_assembly: &AssemblyMetaData, +) -> Option { + log::trace!( + "return_type_token_for_value_type_element_type, signature: {:?}", + signature + ); + + if let Some(cor_element_type) = CorElementType::from_u8(signature[0]) { + let mut managed_type_name = String::new(); + match cor_element_type { + CorElementType::ELEMENT_TYPE_VALUETYPE => { + let (token, len) = uncompress_token(&signature[1..]); + return if len > 0 { + Some(token) + } else { + log::warn!( + "ELEMENT_TYPE_VALUETYPE failed to find uncompress TypeRef or TypeDef" + ); + None + }; + } + CorElementType::ELEMENT_TYPE_VOID => managed_type_name.push_str("System.Void"), + CorElementType::ELEMENT_TYPE_BOOLEAN => managed_type_name.push_str("System.Boolean"), + CorElementType::ELEMENT_TYPE_CHAR => managed_type_name.push_str("System.Char"), + CorElementType::ELEMENT_TYPE_I1 => managed_type_name.push_str("System.SByte"), + CorElementType::ELEMENT_TYPE_U1 => managed_type_name.push_str("System.Byte"), + CorElementType::ELEMENT_TYPE_I2 => managed_type_name.push_str("System.Int16"), + CorElementType::ELEMENT_TYPE_U2 => managed_type_name.push_str("System.UInt16"), + CorElementType::ELEMENT_TYPE_I4 => managed_type_name.push_str("System.Int32"), + CorElementType::ELEMENT_TYPE_U4 => managed_type_name.push_str("System.UInt32"), + CorElementType::ELEMENT_TYPE_I8 => managed_type_name.push_str("System.Int64"), + CorElementType::ELEMENT_TYPE_U8 => managed_type_name.push_str("System.UInt64"), + CorElementType::ELEMENT_TYPE_R4 => managed_type_name.push_str("System.Single"), + CorElementType::ELEMENT_TYPE_R8 => managed_type_name.push_str("System.Double"), + CorElementType::ELEMENT_TYPE_TYPEDBYREF => { + managed_type_name.push_str("System.TypedReference") + } + CorElementType::ELEMENT_TYPE_I => managed_type_name.push_str("System.IntPtr"), + CorElementType::ELEMENT_TYPE_U => managed_type_name.push_str("System.UIntPtr"), + _ => { + return None; + } + } + + if managed_type_name.is_empty() { + log::warn!("no managed type name given"); + return None; + } + + let corlib_ref = match create_assembly_ref_to_corlib(assembly_emit, cor_assembly) { + Ok(r) => r, + Err(_) => { + log::warn!("failed to define AssemblyRef to {}", &cor_assembly.name); + return None; + } + }; + + let type_ref = metadata_emit.define_type_ref_by_name(corlib_ref, &managed_type_name); + if type_ref.is_err() { + log::warn!( + "unable to create type ref for managed_type_name={}", + &managed_type_name + ); + return None; + } + let type_ref = type_ref.unwrap(); + Some(type_ref) + } else { + None + } +} + +pub fn create_assembly_ref_to_corlib( + assembly_emit: &IMetaDataAssemblyEmit, + cor_assembly: &AssemblyMetaData, +) -> Result { + let assembly_metadata = ASSEMBLYMETADATA { + usMajorVersion: cor_assembly.version.major, + usMinorVersion: cor_assembly.version.minor, + usBuildNumber: cor_assembly.version.build, + usRevisionNumber: cor_assembly.version.revision, + szLocale: std::ptr::null_mut(), + cbLocale: 0, + rProcessor: std::ptr::null_mut(), + ulProcessor: 0, + rOS: std::ptr::null_mut(), + ulOS: 0, + }; + + assembly_emit.define_assembly_ref( + cor_assembly.public_key.bytes(), + &cor_assembly.name, + &assembly_metadata, + &[], + cor_assembly.assembly_flags, + ) +} + +pub fn get_il_codes( + title: &str, + method: &Method, + caller: &FunctionInfo, + module_metadata: &ModuleMetadata, +) -> String { + let mut buf = String::new(); + buf.push_str(title); + buf.push_str(caller.type_info.as_ref().map_or("", |t| t.name.as_str())); + buf.push('.'); + buf.push_str(&caller.name); + buf.push_str(&format!(" => (max_stack: {})", method.header.max_stack())); + + let local_sig = method.header.local_var_sig_tok(); + if local_sig != mdTokenNil { + if let Ok(signature) = module_metadata.import.get_sig_from_token(local_sig) { + buf.push('\n'); + buf.push_str(". Local Var Signature "); + buf.push_str(hex::encode(signature).as_str()); + buf.push('\n'); + } + } + + buf.push('\n'); + + let mut address = method.address; + let mut sum_len = 0; + let mut indent = 1; + + for (idx, instruction) in method.instructions.iter().enumerate() { + for section in &method.sections { + match section { + Section::FatSection(h, s) => { + for ss in s { + if ss.flag == CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_FINALLY { + if ss.try_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".try {\n"); + indent += 1; + } + if (ss.try_offset + ss.try_length) as usize == sum_len { + indent -= 1; + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str("}\n"); + } + if ss.handler_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".finally {\n"); + indent += 1; + } + } + } + } + Section::SmallSection(h, s) => { + for ss in s { + if ss.flag == CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_FINALLY { + if ss.try_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".try {\n"); + indent += 1; + } + if (ss.try_offset + ss.try_length as u16) as usize == sum_len { + indent -= 1; + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str("}\n"); + } + if ss.handler_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".finally {\n"); + indent += 1; + } + } + } + } + } + } + + for section in &method.sections { + match section { + Section::FatSection(h, s) => { + for ss in s { + if ss.flag == CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_NONE { + if ss.try_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".try {\n"); + indent += 1; + } + if (ss.try_offset + ss.try_length) as usize == sum_len { + indent -= 1; + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str("}\n"); + } + if ss.handler_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".catch {\n"); + indent += 1; + } + } + } + } + Section::SmallSection(h, s) => { + for ss in s { + if ss.flag == CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_NONE { + if ss.try_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".try {\n"); + indent += 1; + } + if (ss.try_offset + ss.try_length as u16) as usize == sum_len { + indent -= 1; + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str("}\n"); + } + if ss.handler_offset as usize == sum_len { + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str(".catch {\n"); + indent += 1; + } + } + } + } + } + } + + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + + buf.push_str(&format!("{} {:>10}", address, instruction.opcode.name)); + + if instruction.opcode == CALL + || instruction.opcode == CALLVIRT + || instruction.opcode == NEWOBJ + { + if let InlineMethod(token) = instruction.operand { + buf.push_str(&format!(" {}", token)); + if let Ok(member_info) = module_metadata.import.get_function_info(token) { + buf.push_str(" | "); + buf.push_str(member_info.full_name().as_str()); + if member_info.signature.arguments_len() > 0 { + buf.push_str(&format!( + "({} argument{{s}})", + member_info.signature.arguments_len() + )); + } else { + buf.push_str("()"); + } + } + } + } else if instruction.opcode == CASTCLASS + || instruction.opcode == BOX + || instruction.opcode == UNBOX_ANY + || instruction.opcode == NEWARR + || instruction.opcode == INITOBJ + { + if let InlineType(token) = instruction.operand { + buf.push_str(&format!(" {}", token)); + if let Ok(type_info) = module_metadata.import.get_type_info(token) { + if let Some(t) = type_info { + buf.push_str(" | "); + buf.push_str(&t.name); + } else { + buf.push_str(&format!(" {}", token)); + } + } + } + } else if instruction.opcode == LDSTR { + if let InlineString(token) = instruction.operand { + buf.push_str(&format!(" {}", token)); + if let Ok(str) = module_metadata.import.get_user_string(token) { + buf.push_str(" | \""); + buf.push_str(&str); + buf.push('"'); + } + } + } else if let InlineI8(arg) = instruction.operand { + buf.push_str(&format!(" {}", arg)); + } else if let InlineBrTarget(t) = instruction.operand { + buf.push_str(&format!( + " {}", + address as i64 + (t as i64) + instruction.len() as i64 + )); + } else if let ShortInlineBrTarget(t) = instruction.operand { + buf.push_str(&format!( + " {}", + address as i64 + (t as i64) + instruction.len() as i64 + )); + } else if let ShortInlineVar(arg) = instruction.operand { + buf.push_str(&format!(" {}", arg)); + } else if let ShortInlineI(arg) = instruction.operand { + buf.push_str(&format!(" {}", arg)); + } else if let InlineI(arg) = instruction.operand { + buf.push_str(&format!(" {}", arg)); + } else if let InlineField(arg) = instruction.operand { + buf.push_str(&format!(" {}", arg)); + } + + buf.push('\n'); + sum_len += instruction.len(); + + for section in &method.sections { + match section { + Section::FatSection(h, s) => { + for ss in s { + if (ss.handler_offset + ss.handler_length) as usize == sum_len { + indent -= 1; + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str("}\n"); + } + } + } + Section::SmallSection(h, s) => { + for ss in s { + if (ss.handler_offset + ss.handler_length as u16) as usize == sum_len { + indent -= 1; + if indent > 0 { + buf.push_str(&" ".repeat(indent)); + } + buf.push_str("}\n"); + } + } + } + } + } + + address += instruction.len(); + } + + buf +} + +pub fn find_type_def_by_name( + target_method_type_name: &str, + assembly_name: &str, + metadata_import: &IMetaDataImport2, +) -> Option { + let parts: Vec<&str> = target_method_type_name.split('+').collect(); + let method_type_name; + let mut parent = mdTypeDefNil; + match parts.len() { + 1 => method_type_name = target_method_type_name, + 2 => { + method_type_name = parts[1]; + if let Ok(parent_type_def) = metadata_import.find_type_def_by_name(parts[0], None) { + parent = parent_type_def; + } else { + log::debug!( + "unable to load parent type_def={} for nested class: {}, module={}", + parts[0], + target_method_type_name, + assembly_name + ); + return None; + } + } + _ => { + log::warn!( + "invalid type_def. only one layer of nested classes are supported: {}, module={}", + target_method_type_name, + assembly_name + ); + return None; + } + } + + if let Ok(type_def) = metadata_import.find_type_def_by_name(method_type_name, Some(parent)) { + Some(type_def) + } else { + log::debug!( + "Cannot find type_def={}, module={}", + method_type_name, + assembly_name + ); + None + } +} + +/// Flattens integrations into relevant integration methods +pub fn flatten_integrations( + integrations: Vec, + calltarget_enabled: bool, +) -> Vec { + integrations + .into_iter() + .flat_map(|i| { + let name = i.name.clone(); + i.method_replacements + .into_iter() + .filter_map(move |method_replacement| { + if let Some(wrapper_method) = method_replacement.wrapper() { + let is_calltarget = &wrapper_method.action == "CallTargetModification"; + if (calltarget_enabled && is_calltarget) + || (!calltarget_enabled && !is_calltarget) + { + Some(IntegrationMethod { + name: name.clone(), + method_replacement, + }) + } else { + None + } + } else { + None + } + }) + }) + .collect() +} diff --git a/src/elastic_apm_profiler/src/profiler/managed.rs b/src/elastic_apm_profiler/src/profiler/managed.rs new file mode 100644 index 000000000..4e12f812f --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/managed.rs @@ -0,0 +1,80 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::profiler::{ + types::{AssemblyReference, PublicKeyToken}, + IS_ATTACHED, IS_DESKTOP_CLR, +}; +use once_cell::sync::Lazy; +use rust_embed::RustEmbed; +use std::sync::atomic::Ordering; + +/// Embedded assets of the managed loader assembly +#[derive(RustEmbed)] +#[folder = "../Elastic.Apm.Profiler.Managed.Loader/bin/Release"] +#[prefix = ""] +struct ManagedLoader; + +pub const MANAGED_PROFILER_ASSEMBLY_LOADER: &str = "Elastic.Apm.Profiler.Managed.Loader"; +pub const MANAGED_PROFILER_ASSEMBLY_LOADER_STARTUP: &str = + "Elastic.Apm.Profiler.Managed.Loader.Startup"; + +pub const MANAGED_PROFILER_ASSEMBLY: &str = "Elastic.Apm.Profiler.Managed"; + +pub static MANAGED_PROFILER_FULL_ASSEMBLY_VERSION: Lazy = Lazy::new(|| { + AssemblyReference::new( + MANAGED_PROFILER_ASSEMBLY, + crate::profiler::PROFILER_VERSION.clone(), + "neutral", + PublicKeyToken::new("ae7400d2c189cf22"), + ) +}); + +pub const MANAGED_PROFILER_CALLTARGET_TYPE: &str = + "Elastic.Apm.Profiler.Managed.CallTarget.CallTargetInvoker"; +pub const MANAGED_PROFILER_CALLTARGET_BEGINMETHOD_NAME: &str = "BeginMethod"; +pub const MANAGED_PROFILER_CALLTARGET_ENDMETHOD_NAME: &str = "EndMethod"; +pub const MANAGED_PROFILER_CALLTARGET_LOGEXCEPTION_NAME: &str = "LogException"; +pub const MANAGED_PROFILER_CALLTARGET_GETDEFAULTVALUE_NAME: &str = "GetDefaultValue"; +pub const MANAGED_PROFILER_CALLTARGET_STATETYPE: &str = + "Elastic.Apm.Profiler.Managed.CallTarget.CallTargetState"; +pub const MANAGED_PROFILER_CALLTARGET_STATETYPE_GETDEFAULT_NAME: &str = "GetDefault"; +pub const MANAGED_PROFILER_CALLTARGET_RETURNTYPE: &str = + "Elastic.Apm.Profiler.Managed.CallTarget.CallTargetReturn"; +pub const MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETDEFAULT_NAME: &str = "GetDefault"; +pub const MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GENERICS: &str = + "Elastic.Apm.Profiler.Managed.CallTarget.CallTargetReturn`1"; +pub const MANAGED_PROFILER_CALLTARGET_RETURNTYPE_GETRETURNVALUE_NAME: &str = "GetReturnValue"; + +pub const IGNORE: &str = "_"; + +/// Checks whether the profiler is attached. +#[no_mangle] +pub extern "C" fn IsProfilerAttached() -> bool { + IS_ATTACHED.load(Ordering::SeqCst) +} + +/// Gets the embedded loader assembly and symbol bytes +#[no_mangle] +pub extern "C" fn GetAssemblyAndSymbolsBytes( + assembly: *mut *mut u8, + assembly_size: &mut i32, + symbols: *mut *mut u8, + symbols_size: &mut i32, +) { + let tfm = if IS_DESKTOP_CLR.load(Ordering::SeqCst) { + "net461" + } else { + "netcoreapp2.0" + }; + let a = + ManagedLoader::get(&format!("{}/{}.dll", tfm, MANAGED_PROFILER_ASSEMBLY_LOADER)).unwrap(); + unsafe { *assembly = a.as_ptr() as *mut _ }; + *assembly_size = a.len() as i32; + let s = + ManagedLoader::get(&format!("{}/{}.pdb", tfm, MANAGED_PROFILER_ASSEMBLY_LOADER)).unwrap(); + unsafe { *symbols = s.as_ptr() as *mut _ }; + *symbols_size = s.len() as i32; +} diff --git a/src/elastic_apm_profiler/src/profiler/mod.rs b/src/elastic_apm_profiler/src/profiler/mod.rs new file mode 100644 index 000000000..6f392e480 --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/mod.rs @@ -0,0 +1,1664 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + ffi::{types::RuntimeInfo, *}, + interfaces::{ + ICorProfilerAssemblyReferenceProvider, ICorProfilerCallback, ICorProfilerCallback2, + ICorProfilerCallback3, ICorProfilerCallback4, ICorProfilerCallback5, ICorProfilerCallback6, + ICorProfilerCallback7, ICorProfilerCallback8, ICorProfilerCallback9, + ICorProfilerFunctionControl, ICorProfilerInfo4, ICorProfilerInfo5, IMetaDataAssemblyEmit, + IMetaDataAssemblyImport, IMetaDataEmit2, IMetaDataImport2, + }, + profiler::{ + calltarget_tokens::CallTargetTokens, + helpers::flatten_integrations, + managed::{ + IGNORE, MANAGED_PROFILER_ASSEMBLY, MANAGED_PROFILER_ASSEMBLY_LOADER, + MANAGED_PROFILER_FULL_ASSEMBLY_VERSION, + }, + rejit::RejitHandler, + sig::get_sig_type_token_name, + types::{IntegrationMethod, MethodReplacement, ModuleMetadata, ModuleWrapperTokens}, + }, +}; +use com::{ + interfaces::iunknown::IUnknown, + sys::{FAILED, GUID, HRESULT, S_OK}, +}; +use log::Level; +use log4rs::Handle; +use once_cell::sync::Lazy; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + ffi::c_void, + ops::Deref, + path::PathBuf, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Mutex, RwLock, + }, +}; +use types::{AssemblyMetaData, FunctionInfo, Version}; +use widestring::{U16CStr, U16CString}; + +mod calltarget_tokens; +pub mod env; +mod helpers; +pub mod managed; +mod process; +mod rejit; +pub mod sig; +mod startup_hook; +pub mod types; + +const SKIP_ASSEMBLY_PREFIXES: [&str; 22] = [ + "Elastic.Apm", + "MessagePack", + "Microsoft.AI", + "Microsoft.ApplicationInsights", + "Microsoft.Build", + "Microsoft.CSharp", + "Microsoft.Extensions", + "Microsoft.Web.Compilation.Snapshots", + "Sigil", + "System.Core", + "System.Console", + "System.Collections", + "System.ComponentModel", + "System.Diagnostics", + "System.Drawing", + "System.EnterpriseServices", + "System.IO", + "System.Runtime", + "System.Text", + "System.Threading", + "System.Xml", + "Newtonsoft", +]; +const SKIP_ASSEMBLIES: [&str; 7] = [ + "mscorlib", + "netstandard", + "System.Configuration", + "Microsoft.AspNetCore.Razor.Language", + "Microsoft.AspNetCore.Mvc.RazorPages", + "Anonymously Hosted DynamicMethods Assembly", + "ISymWrapper", +]; + +/// The git hash defined on build +static GIT_HASH: &str = env!("GIT_HASH"); + +/// The profiler package version +static PROFILER_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// The profiler version. Must match the managed assembly version +pub static PROFILER_VERSION: Lazy = Lazy::new(|| { + let major = env!("CARGO_PKG_VERSION_MAJOR").parse::().unwrap(); + let minor = env!("CARGO_PKG_VERSION_MINOR").parse::().unwrap(); + let patch = env!("CARGO_PKG_VERSION_PATCH").parse::().unwrap(); + Version::new(major, minor, patch, 0) +}); + +/// Whether the managed profiler has been loaded as domain-neutral i.e. +/// into the shared domain, which can be shared code among other app domains +static MANAGED_PROFILER_LOADED_DOMAIN_NEUTRAL: AtomicBool = AtomicBool::new(false); + +/// Tracks the app domain ids into which the managed profiler assembly has been loaded +static MANAGED_PROFILER_LOADED_APP_DOMAINS: Lazy>> = + Lazy::new(|| Mutex::new(HashSet::new())); + +/// Indicates whether the profiler is attached +pub(crate) static IS_ATTACHED: AtomicBool = AtomicBool::new(false); +/// Indicates whether the profiler is running in a Desktop CLR +pub(crate) static IS_DESKTOP_CLR: AtomicBool = AtomicBool::new(false); + +class! { + /// The profiler implementation + pub class Profiler: + ICorProfilerCallback9(ICorProfilerCallback8(ICorProfilerCallback7( + ICorProfilerCallback6(ICorProfilerCallback5(ICorProfilerCallback4( + ICorProfilerCallback3(ICorProfilerCallback2(ICorProfilerCallback)))))))) { + logger: RefCell>, + profiler_info: RefCell>, + rejit_handler: RefCell>, + runtime_info: RefCell>, + modules: Mutex>, + module_wrapper_tokens: Mutex>, + call_target_tokens: RefCell>, + cor_assembly_property: RefCell>, + cor_lib_module_loaded: AtomicBool, + cor_app_domain_id: AtomicUsize, + is_desktop_iis: AtomicBool, + integration_methods: RwLock>, + first_jit_compilation_app_domains: RwLock>, + } + + impl ICorProfilerCallback for Profiler { + pub fn Initialize( + &self, + pICorProfilerInfoUnk: IUnknown, + ) -> HRESULT { + match self.initialize(pICorProfilerInfoUnk) { + Ok(_) => S_OK, + Err(hr) => hr + } + } + pub fn Shutdown(&self) -> HRESULT { + match self.shutdown() { + Ok(_) => S_OK, + Err(_) => S_OK + } + } + pub fn AppDomainCreationStarted(&self, appDomainId: AppDomainID) -> HRESULT { S_OK } + pub fn AppDomainCreationFinished( + &self, + appDomainId: AppDomainID, + hrStatus: HRESULT, + ) -> HRESULT { S_OK } + pub fn AppDomainShutdownStarted(&self, appDomainId: AppDomainID) -> HRESULT { S_OK } + pub fn AppDomainShutdownFinished( + &self, + appDomainId: AppDomainID, + hrStatus: HRESULT, + ) -> HRESULT { + self.app_domain_shutdown_finished(appDomainId, hrStatus); + S_OK + } + pub fn AssemblyLoadStarted(&self, assemblyId: AssemblyID) -> HRESULT { S_OK } + pub fn AssemblyLoadFinished( + &self, + assemblyId: AssemblyID, + hrStatus: HRESULT, + ) -> HRESULT { + match self.assembly_load_finished(assemblyId, hrStatus) { + Ok(_) => S_OK, + Err(_) => S_OK, + } + } + pub fn AssemblyUnloadStarted(&self, assemblyId: AssemblyID) -> HRESULT { S_OK } + pub fn AssemblyUnloadFinished( + &self, + assemblyId: AssemblyID, + hrStatus: HRESULT, + ) -> HRESULT { S_OK } + pub fn ModuleLoadStarted(&self, moduleId: ModuleID) -> HRESULT { S_OK } + pub fn ModuleLoadFinished(&self, moduleId: ModuleID, hrStatus: HRESULT) -> HRESULT { + match self.module_load_finished(moduleId, hrStatus) { + Ok(_) => S_OK, + Err(_) => S_OK, + } + } + pub fn ModuleUnloadStarted(&self, moduleId: ModuleID) -> HRESULT { + match self.module_unload_started(moduleId) { + Ok(_) => S_OK, + Err(_) => S_OK, + } + } + pub fn ModuleUnloadFinished(&self, moduleId: ModuleID, hrStatus: HRESULT) -> HRESULT { S_OK } + pub fn ModuleAttachedToAssembly( + &self, + moduleId: ModuleID, + AssemblyId: AssemblyID, + ) -> HRESULT { S_OK } + pub fn ClassLoadStarted(&self, classId: ClassID) -> HRESULT { S_OK } + pub fn ClassLoadFinished(&self, classId: ClassID, hrStatus: HRESULT) -> HRESULT { S_OK } + pub fn ClassUnloadStarted(&self, classId: ClassID) -> HRESULT { S_OK } + pub fn ClassUnloadFinished(&self, classId: ClassID, hrStatus: HRESULT) -> HRESULT { S_OK } + pub fn FunctionUnloadStarted(&self, functionId: FunctionID) -> HRESULT { S_OK } + pub fn JITCompilationStarted( + &self, + functionId: FunctionID, + fIsSafeToBlock: BOOL, + ) -> HRESULT { + match self.jit_compilation_started(functionId, fIsSafeToBlock) { + Ok(_) => S_OK, + Err(_) => S_OK, + } + } + pub fn JITCompilationFinished( + &self, + functionId: FunctionID, + hrStatus: HRESULT, + fIsSafeToBlock: BOOL, + ) -> HRESULT { S_OK } + pub fn JITCachedFunctionSearchStarted( + &self, + functionId: FunctionID, + pbUseCachedFunction: *mut BOOL, + ) -> HRESULT { S_OK } + pub fn JITCachedFunctionSearchFinished( + &self, + functionId: FunctionID, + result: COR_PRF_JIT_CACHE, + ) -> HRESULT { S_OK } + pub fn JITFunctionPitched(&self, functionId: FunctionID) -> HRESULT { S_OK } + pub fn JITInlining( + &self, + callerId: FunctionID, + calleeId: FunctionID, + pfShouldInline: *mut BOOL, + ) -> HRESULT { S_OK } + pub fn ThreadCreated(&self, threadId: ThreadID) -> HRESULT { S_OK } + pub fn ThreadDestroyed(&self, threadId: ThreadID) -> HRESULT { S_OK } + pub fn ThreadAssignedToOSThread( + &self, + managedThreadId: ThreadID, + osThreadId: DWORD, + ) -> HRESULT { S_OK } + pub fn RemotingClientInvocationStarted(&self) -> HRESULT { S_OK } + pub fn RemotingClientSendingMessage(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT { S_OK } + pub fn RemotingClientReceivingReply(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT { S_OK } + pub fn RemotingClientInvocationFinished(&self) -> HRESULT { S_OK } + pub fn RemotingServerReceivingMessage(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT { S_OK } + pub fn RemotingServerInvocationStarted(&self) -> HRESULT { S_OK } + pub fn RemotingServerInvocationReturned(&self) -> HRESULT { S_OK } + pub fn RemotingServerSendingReply(&self, pCookie: *const GUID, fIsAsync: BOOL) -> HRESULT { S_OK } + pub fn UnmanagedToManagedTransition( + &self, + functionId: FunctionID, + reason: COR_PRF_TRANSITION_REASON, + ) -> HRESULT { S_OK } + pub fn ManagedToUnmanagedTransition( + &self, + functionId: FunctionID, + reason: COR_PRF_TRANSITION_REASON, + ) -> HRESULT { S_OK } + pub fn RuntimeSuspendStarted(&self, suspendReason: COR_PRF_SUSPEND_REASON) -> HRESULT { S_OK } + pub fn RuntimeSuspendFinished(&self) -> HRESULT { S_OK } + pub fn RuntimeSuspendAborted(&self) -> HRESULT { S_OK } + pub fn RuntimeResumeStarted(&self) -> HRESULT { S_OK } + pub fn RuntimeResumeFinished(&self) -> HRESULT { S_OK } + pub fn RuntimeThreadSuspended(&self, threadId: ThreadID) -> HRESULT { S_OK } + pub fn RuntimeThreadResumed(&self, threadId: ThreadID) -> HRESULT { S_OK } + pub fn MovedReferences( + &self, + cMovedObjectIDRanges: ULONG, + oldObjectIDRangeStart: *const ObjectID, + newObjectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const ULONG, + ) -> HRESULT { S_OK } + pub fn ObjectAllocated(&self, objectId: ObjectID, classId: ClassID) -> HRESULT { S_OK } + pub fn ObjectsAllocatedByClass( + &self, + cClassCount: ULONG, + classIds: *const ClassID, + cObjects: *const ULONG, + ) -> HRESULT { S_OK } + pub fn ObjectReferences( + &self, + objectId: ObjectID, + classId: ClassID, + cObjectRefs: ULONG, + objectRefIds: *const ObjectID, + ) -> HRESULT { S_OK } + pub fn RootReferences( + &self, + cRootRefs: ULONG, + rootRefIds: *const ObjectID, + ) -> HRESULT { S_OK } + pub fn ExceptionThrown(&self, thrownObjectId: ObjectID) -> HRESULT { S_OK } + pub fn ExceptionSearchFunctionEnter(&self, functionId: FunctionID) -> HRESULT { S_OK } + pub fn ExceptionSearchFunctionLeave(&self) -> HRESULT { S_OK } + pub fn ExceptionSearchFilterEnter(&self, functionId: FunctionID) -> HRESULT { S_OK } + pub fn ExceptionSearchFilterLeave(&self) -> HRESULT { S_OK } + pub fn ExceptionSearchCatcherFound(&self, functionId: FunctionID) -> HRESULT { S_OK } + pub fn ExceptionOSHandlerEnter(&self, __unused: UINT_PTR) -> HRESULT { S_OK } + pub fn ExceptionOSHandlerLeave(&self, __unused: UINT_PTR) -> HRESULT { S_OK } + pub fn ExceptionUnwindFunctionEnter(&self, functionId: FunctionID) -> HRESULT { S_OK } + pub fn ExceptionUnwindFunctionLeave(&self) -> HRESULT { S_OK } + pub fn ExceptionUnwindFinallyEnter(&self, functionId: FunctionID) -> HRESULT { S_OK } + pub fn ExceptionUnwindFinallyLeave(&self) -> HRESULT { S_OK } + pub fn ExceptionCatcherEnter( + &self, + functionId: FunctionID, + objectId: ObjectID, + ) -> HRESULT { S_OK } + pub fn ExceptionCatcherLeave(&self) -> HRESULT { S_OK } + pub fn COMClassicVTableCreated( + &self, + wrappedClassId: ClassID, + implementedIID: REFGUID, + pVTable: *const c_void, + cSlots: ULONG, + ) -> HRESULT { S_OK } + pub fn COMClassicVTableDestroyed( + &self, + wrappedClassId: ClassID, + implementedIID: REFGUID, + pVTable: *const c_void, + ) -> HRESULT { S_OK } + pub fn ExceptionCLRCatcherFound(&self) -> HRESULT { S_OK } + pub fn ExceptionCLRCatcherExecute(&self) -> HRESULT { S_OK } + } + + impl ICorProfilerCallback2 for Profiler { + pub fn ThreadNameChanged(&self, + threadId: ThreadID, + cchName: ULONG, + name: *const WCHAR, + ) -> HRESULT { S_OK } + pub fn GarbageCollectionStarted(&self, + cGenerations: int, + generationCollected: *const BOOL, + reason: COR_PRF_GC_REASON, + ) -> HRESULT { S_OK } + pub fn SurvivingReferences(&self, + cSurvivingObjectIDRanges: ULONG, + objectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const ULONG, + ) -> HRESULT { S_OK } + pub fn GarbageCollectionFinished(&self) -> HRESULT { S_OK } + pub fn FinalizeableObjectQueued(&self, + finalizerFlags: DWORD, + objectID: ObjectID, + ) -> HRESULT { S_OK } + pub fn RootReferences2(&self, + cRootRefs: ULONG, + rootRefIds: *const ObjectID, + rootKinds: *const COR_PRF_GC_ROOT_KIND, + rootFlags: *const COR_PRF_GC_ROOT_FLAGS, + rootIds: *const UINT_PTR, + ) -> HRESULT { S_OK } + pub fn HandleCreated(&self, + handleId: GCHandleID, + initialObjectId: ObjectID, + ) -> HRESULT { S_OK } + pub fn HandleDestroyed(&self, handleId: GCHandleID) -> HRESULT { S_OK } + } + + impl ICorProfilerCallback3 for Profiler { + pub fn InitializeForAttach(&self, + pCorProfilerInfoUnk: IUnknown, + pvClientData: *const c_void, + cbClientData: UINT, + ) -> HRESULT { S_OK } + pub fn ProfilerAttachComplete(&self) -> HRESULT { S_OK } + pub fn ProfilerDetachSucceeded(&self) -> HRESULT { S_OK } + } + + impl ICorProfilerCallback4 for Profiler { + pub fn ReJITCompilationStarted(&self, + functionId: FunctionID, + rejitId: ReJITID, + fIsSafeToBlock: BOOL, + ) -> HRESULT { + match self.rejit_compilation_started(functionId, rejitId, fIsSafeToBlock) { + Ok(_) => S_OK, + Err(_) => S_OK, + } + } + pub fn GetReJITParameters(&self, + moduleId: ModuleID, + methodId: mdMethodDef, + pFunctionControl: ICorProfilerFunctionControl, + ) -> HRESULT { + match self.get_rejit_parameters(moduleId, methodId, pFunctionControl) { + Ok(_) => S_OK, + Err(hr) => hr, + } + } + pub fn ReJITCompilationFinished(&self, + functionId: FunctionID, + rejitId: ReJITID, + hrStatus: HRESULT, + fIsSafeToBlock: BOOL, + ) -> HRESULT { + self.rejit_compilation_finished(functionId, rejitId, hrStatus, fIsSafeToBlock); + S_OK + } + pub fn ReJITError(&self, + moduleId: ModuleID, + methodId: mdMethodDef, + functionId: FunctionID, + hrStatus: HRESULT, + ) -> HRESULT { + self.rejit_error(moduleId, methodId, functionId, hrStatus); + S_OK + } + pub fn MovedReferences2(&self, + cMovedObjectIDRanges: ULONG, + oldObjectIDRangeStart: *const ObjectID, + newObjectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const SIZE_T, + ) -> HRESULT { S_OK } + pub fn SurvivingReferences2(&self, + cSurvivingObjectIDRanges: ULONG, + objectIDRangeStart: *const ObjectID, + cObjectIDRangeLength: *const SIZE_T, + ) -> HRESULT { S_OK } + } + + impl ICorProfilerCallback5 for Profiler { + pub fn ConditionalWeakTableElementReferences(&self, + cRootRefs: ULONG, + keyRefIds: *const ObjectID, + valueRefIds: *const ObjectID, + rootIds: *const GCHandleID, + ) -> HRESULT { S_OK } + } + + impl ICorProfilerCallback6 for Profiler { + pub fn GetAssemblyReferences(&self, + wszAssemblyPath: *const WCHAR, + pAsmRefProvider: ICorProfilerAssemblyReferenceProvider, + ) -> HRESULT { + match self.get_assembly_references(wszAssemblyPath, pAsmRefProvider) { + Ok(_) => S_OK, + Err(_) => S_OK, + } + } + } + + impl ICorProfilerCallback7 for Profiler { + pub fn ModuleInMemorySymbolsUpdated(&self, moduleId: ModuleID) -> HRESULT { S_OK } + } + + impl ICorProfilerCallback8 for Profiler { + pub fn DynamicMethodJITCompilationStarted(&self, + functionId: FunctionID, + fIsSafeToBlock: BOOL, + pILHeader: LPCBYTE, + cbILHeader: ULONG, + ) -> HRESULT { S_OK } + pub fn DynamicMethodJITCompilationFinished(&self, + functionId: FunctionID, + hrStatus: HRESULT, + fIsSafeToBlock: BOOL, + ) -> HRESULT { S_OK } + } + + impl ICorProfilerCallback9 for Profiler { + pub fn DynamicMethodUnloaded(&self, functionId: FunctionID) -> HRESULT { S_OK } + } +} + +impl Profiler { + fn initialize(&self, unknown: IUnknown) -> Result<(), HRESULT> { + unsafe { + unknown.AddRef(); + } + + let process_path = std::env::current_exe().map_err(|e| { + // logging hasn't yet been initialized so unable to log + E_FAIL + })?; + + let process_file_name = process_path + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + + let process_name = process_path + .file_stem() + .unwrap() + .to_string_lossy() + .to_string(); + let logger = env::initialize_logging(&process_name); + + log::trace!( + "Initialize: started. profiler package version {} (commit: {})", + PROFILER_PACKAGE_VERSION, + GIT_HASH + ); + + if log::log_enabled!(Level::Debug) { + log::debug!("Environment variables\n{}", env::get_env_vars()); + } + + if let Some(exclude_process_names) = env::get_exclude_processes() { + for exclude_process_name in exclude_process_names { + if process_file_name == exclude_process_name { + log::info!( + "Initialize: process name {} matches excluded name {}. Profiler disabled", + &process_file_name, + &exclude_process_name + ); + return Err(E_FAIL); + } + } + } + + if let Some(exclude_service_names) = env::get_exclude_service_names() { + if let Some(service_name) = env::get_service_name() { + for exclude_service_name in exclude_service_names { + if service_name == exclude_service_name { + log::info!( + "Initialize: service name {} matches excluded name {}. Profiler disabled", + &service_name, + &exclude_service_name); + return Err(E_FAIL); + } + } + } + } + + env::check_if_running_in_azure_app_service()?; + + // get the ICorProfilerInfo4 interface, which will be available for all CLR versions targeted + let profiler_info = unknown + .query_interface::() + .ok_or_else(|| { + log::error!("Initialize: could not get ICorProfilerInfo4 from IUnknown"); + E_FAIL + })?; + + // get the integrations from file + let integrations = env::load_integrations()?; + let calltarget_enabled = *env::ELASTIC_APM_PROFILER_CALLTARGET_ENABLED; + if calltarget_enabled { + let rejit_handler = RejitHandler::new(profiler_info.clone()); + self.rejit_handler.replace(Some(rejit_handler)); + } + + let mut integration_methods = flatten_integrations(integrations, calltarget_enabled); + + if integration_methods.is_empty() { + log::warn!("Initialize: no integrations. Profiler disabled."); + return Err(E_FAIL); + } else { + log::debug!( + "Initialize: loaded {} integration(s)", + integration_methods.len() + ); + } + + self.integration_methods + .write() + .unwrap() + .append(&mut integration_methods); + + // Set the event mask for CLR events we're interested in + let mut event_mask = COR_PRF_MONITOR::COR_PRF_MONITOR_JIT_COMPILATION + | COR_PRF_MONITOR::COR_PRF_DISABLE_TRANSPARENCY_CHECKS_UNDER_FULL_TRUST + | COR_PRF_MONITOR::COR_PRF_MONITOR_MODULE_LOADS + | COR_PRF_MONITOR::COR_PRF_MONITOR_ASSEMBLY_LOADS + | COR_PRF_MONITOR::COR_PRF_MONITOR_APPDOMAIN_LOADS + | COR_PRF_MONITOR::COR_PRF_DISABLE_ALL_NGEN_IMAGES; + + if calltarget_enabled { + log::info!("Initialize: CallTarget instrumentation is enabled"); + event_mask |= COR_PRF_MONITOR::COR_PRF_ENABLE_REJIT; + } else { + log::info!("Initialize: CallTarget instrumentation is disabled"); + } + + if !env::enable_inlining(calltarget_enabled) { + log::info!("Initialize: JIT Inlining is disabled"); + event_mask |= COR_PRF_MONITOR::COR_PRF_DISABLE_INLINING; + } else { + log::info!("Initialize: JIT Inlining is enabled"); + } + + if env::disable_optimizations() { + log::info!("Initialize: optimizations are disabled"); + event_mask |= COR_PRF_MONITOR::COR_PRF_DISABLE_OPTIMIZATIONS; + } + + // if the runtime also supports ICorProfilerInfo5, set eventmask2 + if let Some(profiler_info5) = unknown.query_interface::() { + let event_mask2 = COR_PRF_HIGH_MONITOR::COR_PRF_HIGH_ADD_ASSEMBLY_REFERENCES; + log::trace!( + "Initialize: set event mask2 to {:?}, {:?}", + &event_mask, + &event_mask2 + ); + profiler_info5.set_event_mask2(event_mask, event_mask2)?; + } else { + log::trace!("Initialize: set event mask to {:?}", &event_mask); + profiler_info.set_event_mask(event_mask)?; + } + + // get the details for the runtime + let runtime_info = profiler_info.get_runtime_information()?; + let is_desktop_clr = runtime_info.is_desktop_clr(); + let process_name = process_path.file_name().unwrap(); + if process_name == "w3wp.exe" || process_name == "iisexpress.exe" { + self.is_desktop_iis.store(is_desktop_clr, Ordering::SeqCst); + } + + // Store the profiler and runtime info for later use + self.profiler_info.replace(Some(profiler_info)); + self.runtime_info.replace(Some(runtime_info)); + self.logger.replace(Some(logger)); + + IS_ATTACHED.store(true, Ordering::SeqCst); + IS_DESKTOP_CLR.store(is_desktop_clr, Ordering::SeqCst); + + Ok(()) + } + + fn shutdown(&self) -> Result<(), HRESULT> { + log::trace!("Shutdown: started"); + let _lock = self.modules.lock(); + + // shutdown the rejit handler, if it's running + if let Some(rejit_handler) = self.rejit_handler.replace(None) { + rejit_handler.shutdown(); + } + + // Cannot safely call methods on profiler_info after shutdown is called, + // so replace it on the profiler + self.profiler_info.replace(None); + + IS_ATTACHED.store(false, Ordering::SeqCst); + + Ok(()) + } + + fn app_domain_shutdown_finished(&self, app_domain_id: AppDomainID, hr_status: HRESULT) { + if !IS_ATTACHED.load(Ordering::SeqCst) { + return; + } + let _lock = self.modules.lock(); + if !IS_ATTACHED.load(Ordering::SeqCst) { + return; + } + + self.first_jit_compilation_app_domains + .write() + .unwrap() + .remove(&app_domain_id); + + log::debug!( + "AppDomainShutdownFinished: app_domain={} removed ", + app_domain_id + ); + } + + fn assembly_load_finished( + &self, + assembly_id: AssemblyID, + hr_status: HRESULT, + ) -> Result<(), HRESULT> { + if FAILED(hr_status) { + log::error!("AssemblyLoadFinished: hr_status is {:X}", hr_status); + return Ok(()); + } + + let _lock = self.modules.lock(); + + if !IS_ATTACHED.load(Ordering::SeqCst) { + log::trace!("AssemblyLoadFinished: profiler not attached"); + return Ok(()); + } + + let profiler_info_borrow = self.profiler_info.borrow(); + let profiler_info = profiler_info_borrow.as_ref().unwrap(); + + let assembly_info = profiler_info.get_assembly_info(assembly_id)?; + let metadata_import = profiler_info.get_module_metadata::( + assembly_info.module_id, + CorOpenFlags::ofRead, + )?; + + let metadata_assembly_import = metadata_import + .query_interface::() + .ok_or_else(|| { + log::warn!("AssemblyLoadFinished: unable to get metadata assembly import"); + E_FAIL + })?; + + let assembly_metadata = metadata_assembly_import.get_assembly_metadata()?; + let is_managed_profiler_assembly = assembly_info.name == MANAGED_PROFILER_ASSEMBLY; + + log::debug!( + "AssemblyLoadFinished: name={}, version={}", + &assembly_metadata.name, + &assembly_metadata.version + ); + + if is_managed_profiler_assembly { + if assembly_metadata.version == *PROFILER_VERSION { + log::info!( + "AssemblyLoadFinished: {} {} matched profiler version {}", + MANAGED_PROFILER_ASSEMBLY, + &assembly_metadata.version, + *PROFILER_VERSION + ); + + MANAGED_PROFILER_LOADED_APP_DOMAINS + .lock() + .unwrap() + .insert(assembly_info.app_domain_id); + + let runtime_borrow = self.runtime_info.borrow(); + let runtime_info = runtime_borrow.as_ref().unwrap(); + + if runtime_info.is_desktop_clr() + && self.cor_lib_module_loaded.load(Ordering::SeqCst) + { + if assembly_info.app_domain_id == self.cor_app_domain_id.load(Ordering::SeqCst) + { + log::info!( + "AssemblyLoadFinished: {} was loaded domain-neutral", + MANAGED_PROFILER_ASSEMBLY + ); + MANAGED_PROFILER_LOADED_DOMAIN_NEUTRAL.store(true, Ordering::SeqCst); + } else { + log::info!( + "AssemblyLoadFinished: {} was not loaded domain-neutral", + MANAGED_PROFILER_ASSEMBLY + ); + } + } + } else { + log::warn!( + "AssemblyLoadFinished: {} {} did not match profiler version {}. Will not be marked as loaded", + MANAGED_PROFILER_ASSEMBLY, + &assembly_metadata.version, + *PROFILER_VERSION + ); + } + } + + Ok(()) + } + + fn module_load_finished(&self, module_id: ModuleID, hr_status: HRESULT) -> Result<(), HRESULT> { + if FAILED(hr_status) { + log::error!( + "ModuleLoadFinished: hr status is {} for module id {}. skipping", + hr_status, + module_id + ); + return Ok(()); + } + + if !IS_ATTACHED.load(Ordering::SeqCst) { + log::trace!("ModuleLoadFinished: profiler not attached"); + return Ok(()); + } + + let mut modules = self.modules.lock().unwrap(); + + if !IS_ATTACHED.load(Ordering::SeqCst) { + log::trace!("ModuleLoadFinished: profiler not attached"); + return Ok(()); + } + + if let Some(module_info) = self.get_module_info(module_id) { + let app_domain_id = module_info.assembly.app_domain_id; + let assembly_name = &module_info.assembly.name; + + log::debug!( + "ModuleLoadFinished: {} {} app domain {} {}", + module_id, + assembly_name, + app_domain_id, + &module_info.assembly.app_domain_name + ); + + if !self.cor_lib_module_loaded.load(Ordering::SeqCst) + && (assembly_name == "mscorlib" || assembly_name == "System.Private.CoreLib") + { + self.cor_lib_module_loaded.store(true, Ordering::SeqCst); + self.cor_app_domain_id + .store(app_domain_id, Ordering::SeqCst); + + let profiler_borrow = self.profiler_info.borrow(); + let profiler_info = profiler_borrow.as_ref().unwrap(); + let metadata_assembly_import = profiler_info + .get_module_metadata::( + module_id, + CorOpenFlags::ofRead | CorOpenFlags::ofWrite, + )?; + + let mut assembly_metadata = metadata_assembly_import.get_assembly_metadata()?; + assembly_metadata.name = assembly_name.to_string(); + + log::info!( + "ModuleLoadFinished: Cor library {} {}", + &assembly_metadata.name, + &assembly_metadata.version + ); + self.cor_assembly_property.replace(Some(assembly_metadata)); + + return Ok(()); + } + + // if this is the loader module, track the app domain it's been loaded into + if assembly_name == MANAGED_PROFILER_ASSEMBLY_LOADER { + log::info!( + "ModuleLoadFinished: {} loaded into AppDomain {} {}", + MANAGED_PROFILER_ASSEMBLY_LOADER, + app_domain_id, + &module_info.assembly.app_domain_name + ); + + self.first_jit_compilation_app_domains + .write() + .unwrap() + .insert(app_domain_id); + + return Ok(()); + } + + // if this is a windows runtime module, skip it + if module_info.is_windows_runtime() { + log::debug!( + "ModuleLoadFinished: skipping windows metadata module {} {}", + module_id, + &assembly_name + ); + return Ok(()); + } + + for pattern in SKIP_ASSEMBLY_PREFIXES { + if assembly_name.starts_with(pattern) { + log::debug!( + "ModuleLoadFinished: skipping module {} {} because it matches skip pattern {}", + module_id, + assembly_name, + pattern + ); + return Ok(()); + } + } + + for skip in SKIP_ASSEMBLIES { + if assembly_name == skip { + log::debug!( + "ModuleLoadFinished: skipping module {} {} because it matches skip {}", + module_id, + assembly_name, + skip + ); + return Ok(()); + } + } + + let call_target_enabled = *env::ELASTIC_APM_PROFILER_CALLTARGET_ENABLED; + + // TODO: Avoid cloning integration methods. Should be possible to make all filtered_integrations a collection of references + let mut filtered_integrations = if call_target_enabled { + self.integration_methods.read().unwrap().to_vec() + } else { + self.integration_methods + .read() + .unwrap() + .iter() + .filter(|m| { + if let Some(caller) = m.method_replacement.caller() { + caller.assembly.is_empty() || &caller.assembly == assembly_name + } else { + true + } + }) + .cloned() + .collect() + }; + + if filtered_integrations.is_empty() { + log::debug!( + "ModuleLoadFinished: skipping module {} {} because filtered by caller", + module_id, + assembly_name + ); + return Ok(()); + } + + // get the metadata interfaces for the module + let profiler_borrow = self.profiler_info.borrow(); + let profiler_info = profiler_borrow.as_ref().unwrap(); + let metadata_import = profiler_info + .get_module_metadata::( + module_id, + CorOpenFlags::ofRead | CorOpenFlags::ofWrite, + ) + .map_err(|e| { + log::warn!( + "ModuleLoadFinished: unable to get IMetaDataEmit2 for module id {}", + module_id + ); + e + })?; + + let metadata_emit = metadata_import + .query_interface::() + .ok_or_else(|| { + log::warn!( + "ModuleLoadFinished: unable to get IMetaDataEmit2 for module id {}", + module_id + ); + E_FAIL + })?; + let assembly_import = metadata_import + .query_interface::().ok_or_else(|| { + log::warn!("ModuleLoadFinished: unable to get IMetaDataAssemblyImport for module id {}", module_id); + E_FAIL + })?; + let assembly_emit = metadata_import + .query_interface::() + .ok_or_else(|| { + log::warn!( + "ModuleLoadFinished: unable to get IMetaDataAssemblyEmit for module id {}", + module_id + ); + E_FAIL + })?; + + // don't skip Microsoft.AspNetCore.Hosting so we can run the startup hook and + // subscribe to DiagnosticSource events. + // don't skip Dapper: it makes ADO.NET calls even though it doesn't reference + // System.Data or System.Data.Common + if assembly_name != "Microsoft.AspNetCore.Hosting" + && assembly_name != "Dapper" + && !call_target_enabled + { + let assembly_metadata = assembly_import.get_assembly_metadata().map_err(|e| { + log::warn!( + "ModuleLoadFinished: unable to get assembly metadata for {}", + assembly_name + ); + e + })?; + + let assembly_refs = assembly_import.enum_assembly_refs()?; + let assembly_ref_metadata: Result, HRESULT> = assembly_refs + .into_iter() + .map(|r| assembly_import.get_referenced_assembly_metadata(r)) + .collect(); + if assembly_ref_metadata.is_err() { + log::warn!("ModuleLoadFinished: unable to get referenced assembly metadata"); + return Err(assembly_ref_metadata.err().unwrap()); + } + + fn meets_requirements( + assembly_metadata: &AssemblyMetaData, + method_replacement: &MethodReplacement, + ) -> bool { + match method_replacement.target() { + Some(target) + if target.is_valid_for_assembly( + &assembly_metadata.name, + &assembly_metadata.version, + ) => + { + true + } + _ => false, + } + } + + let assembly_ref_metadata = assembly_ref_metadata.unwrap(); + filtered_integrations.retain(|i| { + meets_requirements(&assembly_metadata, &i.method_replacement) + || assembly_ref_metadata + .iter() + .any(|m| meets_requirements(m, &i.method_replacement)) + }); + + if filtered_integrations.is_empty() { + log::debug!( + "ModuleLoadFinished: skipping module {} {} because filtered by target", + module_id, + assembly_name + ); + return Ok(()); + } + } + + let module_version_id = metadata_import.get_module_version_id().map_err(|e| { + log::warn!("ModuleLoadFinished: unable to get module version id for {} {} app domain {} {}", + module_id, + assembly_name, + app_domain_id, + &module_info.assembly.app_domain_name); + e + })?; + + let cor_assembly_property = { + let cor_assembly_property_borrow = self.cor_assembly_property.borrow(); + cor_assembly_property_borrow.as_ref().unwrap().clone() + }; + + let module_metadata = ModuleMetadata::new( + metadata_import, + metadata_emit, + assembly_import, + assembly_emit, + assembly_name.to_string(), + app_domain_id, + module_version_id, + filtered_integrations, + cor_assembly_property, + ); + + modules.insert(module_id, module_metadata); + let module_metadata = modules.get(&module_id).unwrap(); + + { + let module_wrapper_tokens = self + .module_wrapper_tokens + .lock() + .unwrap() + .insert(module_id, ModuleWrapperTokens::new()); + } + + log::debug!( + "ModuleLoadFinished: stored metadata for {} {} app domain {} {}", + module_id, + assembly_name, + app_domain_id, + &module_info.assembly.app_domain_name + ); + + log::trace!("ModuleLoadFinished: tracking {} module(s)", modules.len()); + + if call_target_enabled { + let rejit_count = + self.calltarget_request_rejit_for_module(module_id, module_metadata)?; + if rejit_count > 0 { + log::trace!( + "ModuleLoadFinished: requested rejit of {} methods", + rejit_count + ); + } + } + } + + Ok(()) + } + + fn module_unload_started(&self, module_id: ModuleID) -> Result<(), HRESULT> { + if !IS_ATTACHED.load(Ordering::SeqCst) { + return Ok(()); + } + + if log::log_enabled!(log::Level::Debug) { + if let Some(module_info) = self.get_module_info(module_id) { + log::debug!( + "ModuleUnloadStarted: {} {} app domain {} {}", + module_id, + &module_info.assembly.name, + &module_info.assembly.app_domain_id, + &module_info.assembly.app_domain_name + ); + } + } + + let mut modules = self.modules.lock().unwrap(); + + if !IS_ATTACHED.load(Ordering::SeqCst) { + return Ok(()); + } + + if let Some(module_metadata) = modules.remove(&module_id) { + MANAGED_PROFILER_LOADED_APP_DOMAINS + .lock() + .unwrap() + .retain(|app_domain_id| app_domain_id != &module_metadata.app_domain_id); + } + + Ok(()) + } + + fn jit_compilation_started( + &self, + function_id: FunctionID, + is_safe_to_block: BOOL, + ) -> Result<(), HRESULT> { + if !IS_ATTACHED.load(Ordering::SeqCst) || is_safe_to_block == 0 { + return Ok(()); + } + + let modules = self.modules.lock().unwrap(); + + if !IS_ATTACHED.load(Ordering::SeqCst) { + return Ok(()); + } + + let profiler_borrow = self.profiler_info.borrow(); + let profiler_info = profiler_borrow.as_ref().unwrap(); + let function_info = profiler_info.get_function_info(function_id).map_err(|e| { + log::warn!( + "JITCompilationStarted: get function info failed for {}", + function_id + ); + e + })?; + + let module_metadata = modules.get(&function_info.module_id); + if module_metadata.is_none() { + return Ok(()); + } + + let module_metadata = module_metadata.unwrap(); + let call_target_enabled = *env::ELASTIC_APM_PROFILER_CALLTARGET_ENABLED; + let loader_injected_in_app_domain = { + // scope reading to this block + self.first_jit_compilation_app_domains + .read() + .unwrap() + .contains(&module_metadata.app_domain_id) + }; + + if call_target_enabled && loader_injected_in_app_domain { + return Ok(()); + } + + let caller = module_metadata + .import + .get_function_info(function_info.token)?; + + log::trace!( + "JITCompilationStarted: function_id={}, name={}()", + function_id, + caller.full_name() + ); + + let is_desktop_iis = self.is_desktop_iis.load(Ordering::SeqCst); + let valid_startup_hook_callsite = if is_desktop_iis { + match &caller.type_info { + Some(t) => { + &module_metadata.assembly_name == "System.Web" + && t.name == "System.Web.Compilation.BuildManager" + && &caller.name == "InvokePreStartInitMethods" + } + None => false, + } + } else { + !(&module_metadata.assembly_name == "System" + || &module_metadata.assembly_name == "System.Net.Http") + }; + + if valid_startup_hook_callsite && !loader_injected_in_app_domain { + let runtime_info_borrow = self.runtime_info.borrow(); + let runtime_info = runtime_info_borrow.as_ref().unwrap(); + + let domain_neutral_assembly = runtime_info.is_desktop_clr() + && self.cor_lib_module_loaded.load(Ordering::SeqCst) + && self.cor_app_domain_id.load(Ordering::SeqCst) == module_metadata.app_domain_id; + + log::info!( + "JITCompilationStarted: Startup hook registered in function_id={} token={} \ + name={}() assembly_name={} app_domain_id={} domain_neutral={}", + function_id, + &function_info.token, + &caller.full_name(), + &module_metadata.assembly_name, + &module_metadata.app_domain_id, + domain_neutral_assembly + ); + + self.first_jit_compilation_app_domains + .write() + .unwrap() + .insert(module_metadata.app_domain_id); + + startup_hook::run_il_startup_hook( + profiler_info, + &module_metadata, + function_info.module_id, + function_info.token, + )?; + + if is_desktop_iis { + // TODO: hookup IIS module + } + } + + if !call_target_enabled { + if &module_metadata.assembly_name == "Microsoft.AspNetCore.Hosting" { + return Ok(()); + } + + let method_replacements = module_metadata.get_method_replacements_for_caller(&caller); + if method_replacements.is_empty() { + return Ok(()); + } + + let mut module_wrapper_tokens = self.module_wrapper_tokens.lock().unwrap(); + let mut module_wrapper_token = module_wrapper_tokens + .get_mut(&function_info.module_id) + .unwrap(); + + process::process_insertion_calls( + profiler_info, + &module_metadata, + &mut module_wrapper_token, + function_id, + function_info.module_id, + function_info.token, + &caller, + &method_replacements, + )?; + + process::process_replacement_calls( + profiler_info, + &module_metadata, + &mut module_wrapper_token, + function_id, + function_info.module_id, + function_info.token, + &caller, + &method_replacements, + )?; + } + + Ok(()) + } + + fn get_module_info(&self, module_id: ModuleID) -> Option { + let borrow = self.profiler_info.borrow(); + let profiler_info = borrow.as_ref().unwrap(); + if let Ok(module_info) = profiler_info.get_module_info_2(module_id) { + if let Ok(assembly_info) = profiler_info.get_assembly_info(module_info.assembly_id) { + if let Ok(app_domain_info) = + profiler_info.get_app_domain_info(assembly_info.app_domain_id) + { + return Some(types::ModuleInfo { + id: module_id, + assembly: types::AssemblyInfo { + id: module_info.assembly_id, + name: assembly_info.name, + app_domain_id: assembly_info.app_domain_id, + app_domain_name: app_domain_info.name, + manifest_module_id: assembly_info.module_id, + }, + path: module_info.file_name, + flags: module_info.module_flags, + }); + } + } + } + + None + } + + fn get_assembly_references( + &self, + assembly_path: *const WCHAR, + assembly_reference_provider: ICorProfilerAssemblyReferenceProvider, + ) -> Result<(), HRESULT> { + unsafe { + assembly_reference_provider.AddRef(); + } + + let path = { + let p = unsafe { U16CStr::from_ptr_str(assembly_path) }; + p.to_string_lossy() + }; + + if *env::IS_AZURE_APP_SERVICE { + log::debug!( + "GetAssemblyReferences: skipping because profiler is running in \ + Azure App Services, which is not yet supported. path={}", + &path + ); + return Ok(()); + } + + log::trace!("GetAssemblyReferences: called for path={}", &path); + + let path_buf = PathBuf::from(&path); + let mut assembly_name = path_buf.file_name().unwrap().to_str().unwrap(); + if assembly_name.ends_with(".dll") { + assembly_name = assembly_name.strip_suffix(".dll").unwrap(); + } else if assembly_name.ends_with(".ni.dll") { + assembly_name = assembly_name.strip_suffix(".ni.dll").unwrap(); + } + + for pattern in SKIP_ASSEMBLY_PREFIXES.iter() { + if assembly_name.starts_with(pattern) { + log::debug!( + "GetAssemblyReferences: skipping module {} {} because it matches skip pattern {}", + assembly_name, + &path, + pattern + ); + return Ok(()); + } + } + + for skip in SKIP_ASSEMBLIES.iter() { + if &assembly_name == skip { + log::debug!( + "GetAssemblyReferences: skipping assembly {} {} because it matches skip {}", + assembly_name, + &path, + skip + ); + return Ok(()); + } + } + + let assembly_reference = MANAGED_PROFILER_FULL_ASSEMBLY_VERSION.deref(); + let locale = if &assembly_reference.locale == "neutral" { + U16CString::default() + } else { + U16CString::from_str(&assembly_reference.locale).unwrap() + }; + + let cb_locale = locale.len() as ULONG; + let sz_locale = locale.into_vec_with_nul().as_mut_ptr(); + let assembly_metadata = ASSEMBLYMETADATA { + usMajorVersion: assembly_reference.version.major, + usMinorVersion: assembly_reference.version.minor, + usBuildNumber: assembly_reference.version.build, + usRevisionNumber: assembly_reference.version.revision, + szLocale: sz_locale, + cbLocale: cb_locale, + rProcessor: std::ptr::null_mut(), + ulProcessor: 0, + rOS: std::ptr::null_mut(), + ulOS: 0, + }; + + let public_key = assembly_reference.public_key.into_bytes(); + let name = U16CString::from_str(&assembly_reference.name).unwrap(); + let len = name.len() as ULONG; + + let assembly_reference_info = COR_PRF_ASSEMBLY_REFERENCE_INFO { + pbPublicKeyOrToken: public_key.as_ptr() as *const _ as *const c_void, + cbPublicKeyOrToken: public_key.len() as ULONG, + szName: name.as_ptr(), + pMetaData: &assembly_metadata as *const ASSEMBLYMETADATA, + pbHashValue: std::ptr::null() as *const c_void, + cbHashValue: 0, + dwAssemblyRefFlags: 0, + }; + + match assembly_reference_provider.add_assembly_reference(&assembly_reference_info) { + Ok(()) => { + log::trace!( + "GetAssemblyReferences succeeded for {}, path={}", + assembly_name, + &path + ) + } + Err(e) => log::warn!( + "GetAssemblyReferences failed for {}, path={}. 0x{:X}", + assembly_name, + &path, + e + ), + } + + Ok(()) + } + + fn rejit_compilation_started( + &self, + function_id: FunctionID, + rejit_id: ReJITID, + is_safe_to_block: BOOL, + ) -> Result<(), HRESULT> { + if !IS_ATTACHED.load(Ordering::SeqCst) || is_safe_to_block == 0 { + return Ok(()); + } + + log::debug!( + "ReJITCompilationStarted: function_id={} rejit_id={} is_safe_to_block={}", + function_id, + rejit_id, + is_safe_to_block + ); + + let borrow = self.rejit_handler.borrow(); + let rejit_handler: &RejitHandler = borrow.as_ref().unwrap(); + rejit_handler.notify_rejit_compilation_started(function_id, rejit_id) + } + + fn rejit_compilation_finished( + &self, + function_id: FunctionID, + rejit_id: ReJITID, + hr_status: HRESULT, + is_safe_to_block: BOOL, + ) { + if !IS_ATTACHED.load(Ordering::SeqCst) { + return; + } + + log::debug!( + "ReJITCompilationFinished: function_id={} rejit_id={} hr_status={} is_safe_to_block={}", + function_id, + rejit_id, + hr_status, + is_safe_to_block + ); + } + + fn get_rejit_parameters( + &self, + module_id: ModuleID, + method_id: mdMethodDef, + function_control: ICorProfilerFunctionControl, + ) -> Result<(), HRESULT> { + unsafe { + function_control.AddRef(); + } + + if !IS_ATTACHED.load(Ordering::SeqCst) { + return Ok(()); + } + + log::debug!( + "GetReJITParameters: module_id={} method_id={}", + module_id, + method_id + ); + + let modules = self.modules.lock().unwrap(); + + if let Some(module_metadata) = modules.get(&module_id) { + let mut module_wrapper_tokens = self.module_wrapper_tokens.lock().unwrap(); + + let module_wrapper_token = module_wrapper_tokens.get_mut(&module_id).unwrap(); + + let borrow = self.profiler_info.borrow(); + let profiler_info = borrow.as_ref().unwrap(); + + let mut tokens = self.call_target_tokens.borrow_mut(); + let call_target_tokens = tokens + .entry(module_id) + .or_insert_with(CallTargetTokens::new); + + let mut rejit_borrow = self.rejit_handler.borrow_mut(); + let rejit_handler: &mut RejitHandler = rejit_borrow.as_mut().unwrap(); + rejit_handler.notify_rejit_parameters( + module_id, + method_id, + &function_control, + module_metadata, + module_wrapper_token, + profiler_info, + call_target_tokens, + ) + } else { + Ok(()) + } + } + + fn rejit_error( + &self, + module_id: ModuleID, + method_id: mdMethodDef, + function_id: FunctionID, + hr_status: HRESULT, + ) { + if !IS_ATTACHED.load(Ordering::SeqCst) { + return; + } + + log::warn!( + "ReJITError: function_id={} module_id={} method_id={} hr_status={}", + function_id, + module_id, + method_id, + hr_status + ); + } + + fn calltarget_request_rejit_for_module( + &self, + module_id: ModuleID, + module_metadata: &ModuleMetadata, + ) -> Result { + let metadata_import = &module_metadata.import; + let assembly_metadata: AssemblyMetaData = + module_metadata.assembly_import.get_assembly_metadata()?; + + let mut method_ids = vec![]; + + for integration in &module_metadata.integrations { + let target = match integration.method_replacement.target() { + Some(t) + if t.is_valid_for_assembly( + &module_metadata.assembly_name, + &assembly_metadata.version, + ) => + { + t + } + _ => continue, + }; + + let wrapper = match integration.method_replacement.wrapper() { + Some(w) if &w.action == "CallTargetModification" => w, + _ => continue, + }; + + let type_def = match helpers::find_type_def_by_name( + target.type_name(), + &module_metadata.assembly_name, + &metadata_import, + ) { + Some(t) => t, + None => continue, + }; + + let method_defs = + metadata_import.enum_methods_with_name(type_def, target.method_name())?; + for method_def in method_defs { + let caller: FunctionInfo = match metadata_import.get_function_info(method_def) { + Ok(c) => c, + Err(e) => { + log::warn!( + "Could not get function_info for method_def={}, {}", + method_def, + e + ); + continue; + } + }; + + let parsed_signature = match caller.method_signature.try_parse() { + Some(p) => p, + None => { + log::warn!( + "The method {} with signature={:?} cannot be parsed", + &caller.full_name(), + &caller.method_signature.data + ); + continue; + } + }; + + let signature_types = match target.signature_types() { + Some(s) => s, + None => { + log::debug!("target does not have arguments defined"); + continue; + } + }; + + if parsed_signature.arg_len as usize != signature_types.len() - 1 { + log::debug!( + "The caller for method_def {} does not have expected number of arguments", + target.method_name() + ); + continue; + } + + log::trace!( + "comparing signature for method {}.{}", + target.type_name(), + target.method_name() + ); + let mut mismatch = false; + for arg_idx in 0..parsed_signature.arg_len { + let (start_idx, _) = parsed_signature.args[arg_idx as usize]; + let (argument_type_name, _) = get_sig_type_token_name( + &parsed_signature.data[start_idx..], + &metadata_import, + ); + + let integration_argument_type_name = &signature_types[arg_idx as usize + 1]; + log::trace!( + "-> {} = {}", + &argument_type_name, + integration_argument_type_name + ); + if &argument_type_name != integration_argument_type_name + && integration_argument_type_name != IGNORE + { + mismatch = true; + break; + } + } + + if mismatch { + log::debug!( + "The caller for method_def {} does not have the right type of arguments", + target.method_name() + ); + continue; + } + + let mut borrow = self.rejit_handler.borrow_mut(); + let rejit_handler: &mut RejitHandler = borrow.as_mut().unwrap(); + let rejit_module = rejit_handler.get_or_add_module(module_id); + let rejit_method = rejit_module.get_or_add_method(method_def); + rejit_method.set_function_info(caller); + rejit_method.set_method_replacement(integration.method_replacement.clone()); + + method_ids.push(method_def); + + if log::log_enabled!(Level::Info) { + let caller_assembly_is_domain_neutral = IS_DESKTOP_CLR.load(Ordering::SeqCst) + && self.cor_lib_module_loaded.load(Ordering::SeqCst) + && module_metadata.app_domain_id + == self.cor_app_domain_id.load(Ordering::SeqCst); + let caller = rejit_method.function_info().unwrap(); + + log::info!( + "enqueue for ReJIT module_id={}, method_def={}, app_domain_id={}, \ + domain_neutral={}, assembly={}, type={}, method={}, signature={:?}", + module_id, + method_def, + module_metadata.app_domain_id, + caller_assembly_is_domain_neutral, + &module_metadata.assembly_name, + caller.type_info.as_ref().map_or("", |t| t.name.as_str()), + &caller.name, + &caller.signature.bytes() + ); + } + } + } + + let len = method_ids.len(); + if !method_ids.is_empty() { + let borrow = self.rejit_handler.borrow(); + let rejit_handler = borrow.as_ref().unwrap(); + rejit_handler.enqueue_for_rejit(vec![module_id; method_ids.len()], method_ids); + } + + Ok(len) + } +} + +pub fn profiler_assembly_loaded_in_app_domain(app_domain_id: AppDomainID) -> bool { + MANAGED_PROFILER_LOADED_DOMAIN_NEUTRAL.load(Ordering::SeqCst) + || MANAGED_PROFILER_LOADED_APP_DOMAINS + .lock() + .unwrap() + .contains(&app_domain_id) +} diff --git a/src/elastic_apm_profiler/src/profiler/process.rs b/src/elastic_apm_profiler/src/profiler/process.rs new file mode 100644 index 000000000..d2d7cb0f6 --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/process.rs @@ -0,0 +1,604 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + cil::{ + uncompress_token, Instruction, Method, Operand::InlineMethod, CALL, CALLVIRT, CONSTRAINED, + }, + ffi::{ + mdMemberRefNil, mdToken, mdTypeRefNil, CorElementType, FunctionID, ModuleID, E_FAIL, ULONG, + }, + interfaces::ICorProfilerInfo4, + profiler, + profiler::{ + env, helpers, + helpers::return_type_is_value_type_or_generic, + managed::IGNORE, + sig::{parse_signature_types, parse_type}, + types::{ + FunctionInfo, MetadataBuilder, MethodReplacement, ModuleMetadata, ModuleWrapperTokens, + WrapperMethodRef, WrapperMethodReference, + }, + }, +}; +use com::sys::HRESULT; +use log::Level; +use num_traits::FromPrimitive; +use std::mem::transmute; + +pub fn process_insertion_calls( + profiler_info: &ICorProfilerInfo4, + module_metadata: &ModuleMetadata, + module_wrapper_tokens: &mut ModuleWrapperTokens, + function_id: FunctionID, + module_id: ModuleID, + function_token: mdToken, + caller: &FunctionInfo, + method_replacements: &[MethodReplacement], +) -> Result<(), HRESULT> { + // TODO: implement + + Ok(()) +} + +pub fn process_replacement_calls( + profiler_info: &ICorProfilerInfo4, + module_metadata: &ModuleMetadata, + module_wrapper_tokens: &mut ModuleWrapperTokens, + function_id: FunctionID, + module_id: ModuleID, + function_token: mdToken, + caller: &FunctionInfo, + method_replacements: &[MethodReplacement], +) -> Result<(), HRESULT> { + let il_body = profiler_info.get_il_function_body(module_id, function_token)?; + let mut method = Method::new(il_body.into()).map_err(|e| { + log::warn!("process_replacement_calls: error decoding il. {:?}", e); + E_FAIL + })?; + + let mut original_il = None; + let mut modified = false; + for method_replacement in method_replacements { + if method_replacement.wrapper().is_none() { + continue; + } + let wrapper = method_replacement.wrapper().unwrap(); + if &wrapper.action != "ReplaceTargetMethod" { + continue; + } + + let wrapper_method_key = wrapper.get_method_cache_key(); + if module_wrapper_tokens.is_failed_wrapper_member_key(&wrapper_method_key) { + continue; + } + + let instructions = method.instructions.clone(); + let mut index_mod = 0; + for (mut instr_index, instruction) in instructions.iter().enumerate() { + if instruction.opcode != CALL && instruction.opcode != CALLVIRT { + continue; + } + + let original_argument; + if let InlineMethod(token) = &instruction.operand { + original_argument = *token; + } else { + continue; + } + + let target = match module_metadata.import.get_function_info(original_argument) { + Ok(t) => t, + Err(_) => continue, + }; + + if method_replacement.target().map_or(true, |t| { + target + .type_info + .as_ref() + .map_or(true, |tt| tt.name != t.type_name()) + || t.method_name() != target.name + }) { + continue; + } + + if wrapper.method_signature.is_none() { + continue; + } + + let added_parameters_count = 3; + let wrapper_method_signature = wrapper.method_signature.as_ref().unwrap(); + if wrapper_method_signature.len() < (added_parameters_count + 3) as usize { + log::warn!( + "skipping because wrapper signature length {} does not match expected {}", + wrapper_method_signature.len(), + added_parameters_count + 3 + ); + continue; + } + + let mut expected_number_args = wrapper_method_signature.arguments_len(); + expected_number_args -= added_parameters_count; + + let target_signature = &target.signature; + if target_signature.is_instance_method() { + expected_number_args -= 1; + } + + let target_arg_count = target_signature.arguments_len(); + if expected_number_args != target_arg_count { + log::warn!( + "skipping {} because expected argument length {} does not match actual {}", + caller.full_name(), + expected_number_args, + target_arg_count + ); + continue; + } + + let mut generated_wrapper_method_ref = match get_wrapper_method_ref( + profiler_info, + module_metadata, + module_wrapper_tokens, + module_id, + wrapper, + &wrapper_method_key, + ) { + Ok(g) => g, + Err(_) => { + log::warn!( + "JITCompilationStarted failed to obtain wrapper method ref for {}.{}(). function_id={}, function_token={}, name={}()", + &wrapper.type_name, + wrapper.method_name.as_ref().map_or("", |m| m.as_str()), + function_id, + function_token, + caller.full_name() + ); + continue; + } + }; + + let mut method_def_md_token = target.id; + if target.is_generic { + if target_signature.type_arguments_len() + != wrapper_method_signature.type_arguments_len() + { + continue; + } + + // we need to emit a method spec to populate the generic arguments + generated_wrapper_method_ref.method_ref = + module_metadata.emit.define_method_spec( + generated_wrapper_method_ref.method_ref, + target.function_spec_signature.as_ref().unwrap().bytes(), + ).map_err(|e| { + log::warn!("JITCompilationStarted: error defining method spec. function_id={}, function_token={}, name={}()", + function_id, + function_token, + target.full_name()); + e + })?; + method_def_md_token = target.method_def_id; + } + + let actual_sig = parse_signature_types(module_metadata, &target); + if actual_sig.is_none() { + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "JITCompilationStarted: skipping function call, failed to parse signature. function_id={}, function_token={}, name={}()", + function_id, + function_token, + target.full_name()); + } + + continue; + } + + let actual_sig = actual_sig.unwrap(); + let expected_sig = method_replacement + .target() + .unwrap() + .signature_types() + .unwrap(); + + if actual_sig.len() != expected_sig.len() { + if log::log_enabled!(log::Level::Debug) { + log::debug!("JITCompilationStarted: skipping function call, actual signature length does not match expected signature length. function_id={}, function_token={}, name={}(), expected_sig={:?}, actual_sig={:?}", + function_id, + function_token, + target.full_name(), + expected_sig, + &actual_sig + ); + } + + continue; + } + + let mut mismatch = false; + for (idx, expected) in expected_sig.iter().enumerate() { + if expected != IGNORE && expected != &actual_sig[idx] { + if log::log_enabled!(log::Level::Debug) { + log::debug!("JITCompilationStarted: skipping function call, types don't match. function_id={}, function_token={}, name={}(), expected_sig[{}]={}, actual_sig[{}]={}", + function_id, + function_token, + target.full_name(), + idx, + expected, + idx, + &actual_sig[idx] + ); + } + + mismatch = true; + break; + } + } + + if mismatch { + continue; + } + + if !profiler::profiler_assembly_loaded_in_app_domain(module_metadata.app_domain_id) { + log::warn!( + "JITCompilationStarted: skipping method as replacement found but managed profiler \ + has not been loaded into AppDomain id={}, function_id={}, function_token={}, caller_name={}(), target_name={}()", + module_metadata.app_domain_id, + function_id, + function_token, + caller.full_name(), + target.full_name() + ); + + continue; + } + + if instruction.opcode == CALLVIRT && instructions[instr_index - 1].opcode == CONSTRAINED + { + log::warn!( + "JITCompilationStarted: skipping method as replacement found but target method is a constrained \ + virtual method call, which is not supported. function_id={}, function_token={}, caller_name={}(), target_name={}()", + function_id, + function_token, + caller.full_name(), + target.full_name() + ); + continue; + } + + if *env::ELASTIC_APM_PROFILER_LOG_IL { + original_il = Some(helpers::get_il_codes( + "IL original code for caller: ", + &method, + caller, + module_metadata, + )); + } + + // We're going to push instructions onto the stack which may exceed tiny format max stack, + // so expand tiny format to fat format to ensure the stack will be big enough + method.expand_tiny_to_fat(); + + let original_opcode = instruction.opcode; + + // Replace the original call instruction with a nop + method + .replace(instr_index + index_mod, Instruction::nop()) + .map_err(|e| { + log::warn!("unable to replace instruction {:?}", e); + E_FAIL + })?; + + // insert instructions after the original instruction + instr_index += 1; + + let original_method_def = target.id; + let argument_len = target_arg_count; + let return_type_index = target_signature.index_of_return_type(); + let return_type_bytes = &target_signature.bytes()[return_type_index..]; + + let mut signature_read_success = true; + let mut idx = 0; + for _ in 0..argument_len { + if let Some(type_idx) = parse_type(&return_type_bytes[idx..]) { + idx += type_idx; + } else { + signature_read_success = false; + break; + } + } + + let mut this_index_mod = 0; + + if signature_read_success { + // handle CancellationToken in the last argument position + if let Some(CorElementType::ELEMENT_TYPE_VALUETYPE) = + CorElementType::from_u8(return_type_bytes[idx]) + { + idx += 1; + let (value_type_token, len) = uncompress_token(&return_type_bytes[idx..]); + if len > 0 { + if let Ok(Some(type_info)) = + module_metadata.import.get_type_info(value_type_token) + { + if &type_info.name == "System.Threading.CancellationToken" { + method + .insert( + instr_index + index_mod, + Instruction::box_(value_type_token), + ) + .map_err(|e| { + log::warn!( + "error inserting box({}) instruction, {:?}", + value_type_token, + e + ); + E_FAIL + })?; + + instr_index += 1; + this_index_mod += 1; + } + } + } + } + // handle ReadOnlyMemory in the last argument position + else if let Some(CorElementType::ELEMENT_TYPE_GENERICINST) = + CorElementType::from_u8(return_type_bytes[idx]) + { + let start_idx = idx; + let mut end_idx = start_idx; + + idx += 1; + if let Some(CorElementType::ELEMENT_TYPE_VALUETYPE) = + CorElementType::from_u8(return_type_bytes[idx]) + { + idx += 1; + let (value_type_token, len) = uncompress_token(&return_type_bytes[idx..]); + if len > 0 { + if let Ok(Some(type_info)) = + module_metadata.import.get_type_info(value_type_token) + { + if &type_info.name == "System.ReadOnlyMemory`1" { + if let Some(type_idx) = + parse_type(&return_type_bytes[end_idx..]) + { + end_idx += type_idx; + if let Ok(type_token) = + module_metadata.emit.get_token_from_type_spec( + &return_type_bytes[start_idx..end_idx], + ) + { + method + .insert( + instr_index + index_mod, + Instruction::box_(type_token), + ) + .map_err(|e| { + log::warn!( + "error inserting box({}) instruction, {:?}", + type_token, + e + ); + E_FAIL + })?; + + instr_index += 1; + this_index_mod += 1; + } + } + } + } + } + } + } + } + + // TODO: add a fn to method insert all instructions in one go + method + .insert( + instr_index + index_mod, + Instruction::call(generated_wrapper_method_ref.method_ref), + ) + .map_err(|e| { + log::warn!( + "error inserting call({}) instruction, {:?}", + generated_wrapper_method_ref.method_ref, + e + ); + E_FAIL + })?; + + method + .insert( + instr_index + index_mod, + Instruction::load_int32(original_opcode.byte_2 as i32), + ) + .map_err(|e| { + log::warn!( + "error inserting load_int32({}) instruction, {:?}", + original_opcode.byte_2, + e + ); + E_FAIL + })?; + + method + .insert( + instr_index + 1 + index_mod, + Instruction::load_int32(method_def_md_token as i32), + ) + .map_err(|e| { + log::warn!( + "error inserting load_int32({}) instruction, {:?}", + method_def_md_token, + e + ); + E_FAIL + })?; + + let module_ptr: i64 = unsafe { transmute(&module_metadata.module_version_id) }; + method + .insert(instr_index + 2 + index_mod, Instruction::ldc_i8(module_ptr)) + .map_err(|e| { + log::warn!( + "error inserting ldc_i8({}) instruction, {:?}", + module_ptr, + e + ); + E_FAIL + })?; + + this_index_mod += 4; + + if wrapper_method_signature.return_type_is_object() { + if let Some(type_token) = return_type_is_value_type_or_generic( + &module_metadata, + target.id, + &target.signature, + ) { + if log::log_enabled!(Level::Debug) { + log::debug!( + "JITCompilationStarted: insert 'unbox.any' {} instruction after \ + calling target function. function_id={} token={} target_name={}()", + type_token, + function_id, + function_token, + target.full_name() + ); + } + + method + .insert( + instr_index + 4 + index_mod, + Instruction::unbox_any(type_token), + ) + .map_err(|e| { + log::warn!("error inserting unbox.any instruction, {:?}", e); + E_FAIL + })?; + + this_index_mod += 1; + } + } + + index_mod += this_index_mod; + + modified = true; + log::info!( + "JITCompilationStarted: replaced calls from {}() to {}() with calls to {}() {}", + caller.full_name(), + target.full_name(), + wrapper.full_name(), + generated_wrapper_method_ref.method_ref + ); + } + } + + if modified { + if *env::ELASTIC_APM_PROFILER_LOG_IL { + let modified_il = helpers::get_il_codes( + "IL modification for caller: ", + &method, + caller, + module_metadata, + ); + log::debug!("{}\n{}", original_il.unwrap_or_default(), modified_il); + } + + let method_bytes = method.into_bytes(); + let allocator = profiler_info.get_il_function_body_allocator(module_id)?; + let allocated_bytes = allocator.alloc(method_bytes.len() as ULONG).map_err(|e| { + log::warn!("process_replacement_calls: failed to allocate memory for replacement il"); + e + })?; + + let address = unsafe { allocated_bytes.into_inner() }; + unsafe { + std::ptr::copy(method_bytes.as_ptr(), address, method_bytes.len()); + } + profiler_info + .set_il_function_body(module_id, function_token, address as *const _) + .map_err(|e| { + log::warn!( + "process_replacement_calls: failed to set il for module_id={} {}", + module_id, + function_token + ); + e + })?; + } + + Ok(()) +} + +// TODO: move +pub fn get_wrapper_method_ref( + profiler_info: &ICorProfilerInfo4, + module_metadata: &ModuleMetadata, + module_wrapper_tokens: &mut ModuleWrapperTokens, + module_id: ModuleID, + wrapper: &WrapperMethodReference, + wrapper_method_key: &str, +) -> Result { + let wrapper_type_key = wrapper.get_type_cache_key(); + if let Some(method_ref) = module_wrapper_tokens.get_wrapper_member_ref(wrapper_method_key) { + let type_ref = module_wrapper_tokens + .get_wrapper_parent_type_ref(&wrapper_type_key) + .unwrap_or(mdTypeRefNil); + return Ok(WrapperMethodRef { + type_ref, + method_ref, + }); + } + + let module_info = profiler_info.get_module_info(module_id)?; + let module = module_metadata + .import + .get_module_from_scope() + .map_err(|e| { + log::warn!( + "JITCompilationStarted failed to get module for module_id={} module_name={}", + module_id, + &module_info.file_name + ); + e + })?; + + let mut metadata_builder = MetadataBuilder::new(module_metadata, module_wrapper_tokens, module); + + metadata_builder + .emit_assembly_ref(&wrapper.assembly) + .map_err(|e| { + log::warn!( + "JITCompilationStarted: failed to emit wrapper assembly ref for assembly={}", + &wrapper.assembly + ); + e + })?; + + metadata_builder + .store_wrapper_method_ref(wrapper) + .map_err(|e| { + log::warn!( + "JITCompilationStarted: failed to store wrapper method ref for {}.{}()", + &wrapper.type_name, + wrapper.method_name.as_ref().map_or("", |m| m.as_str()) + ); + e + })?; + + let method_ref = module_wrapper_tokens + .get_wrapper_member_ref(&wrapper_method_key) + .unwrap_or(mdMemberRefNil); + let type_ref = module_wrapper_tokens + .get_wrapper_parent_type_ref(&wrapper_type_key) + .unwrap_or(mdTypeRefNil); + + Ok(WrapperMethodRef { + type_ref, + method_ref, + }) +} diff --git a/src/elastic_apm_profiler/src/profiler/rejit.rs b/src/elastic_apm_profiler/src/profiler/rejit.rs new file mode 100644 index 000000000..ea32216c4 --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/rejit.rs @@ -0,0 +1,905 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + cil::{ + CorExceptionFlag, FatSectionClause, Instruction, Method, Operand::ShortInlineBrTarget, RET, + }, + ffi::{ + mdMethodDef, mdTokenNil, mdTypeSpecNil, CorCallingConvention, FunctionID, ModuleID, ReJITID, + }, + interfaces::{ICorProfilerFunctionControl, ICorProfilerInfo4}, + profiler::{ + calltarget_tokens::CallTargetTokens, + env, helpers, process, + types::{ + FunctionInfo, MethodArgumentTypeFlag, MethodReplacement, ModuleMetadata, + ModuleWrapperTokens, TypeInfo, + }, + }, +}; +use com::sys::{HRESULT, S_FALSE}; +use log::Level; +use std::{ + collections::HashMap, + sync::{ + mpsc::{channel, Sender}, + Mutex, + }, + thread, + thread::JoinHandle, +}; + +pub struct RejitHandlerModule { + module_id: ModuleID, + method_defs: HashMap, + method_defs_mutex: Mutex<()>, +} + +impl RejitHandlerModule { + pub fn new(module_id: ModuleID) -> Self { + Self { + module_id, + method_defs: HashMap::new(), + method_defs_mutex: Mutex::new(()), + } + } + + pub fn get_or_add_method(&mut self, method_def: mdMethodDef) -> &mut RejitHandlerModuleMethod { + let _lock = self.method_defs_mutex.lock().unwrap(); + self.method_defs + .entry(method_def) + .or_insert_with(|| RejitHandlerModuleMethod::new(method_def)) + } +} + +pub struct RejitHandlerModuleMethod { + method_def: mdMethodDef, + function_info: Option, + method_replacement: Option, +} + +impl RejitHandlerModuleMethod { + pub fn new(method_def: mdMethodDef) -> Self { + Self { + method_def, + function_info: None, + method_replacement: None, + } + } + + pub fn set_function_info(&mut self, function_info: FunctionInfo) { + self.function_info = Some(function_info); + } + + pub fn set_method_replacement(&mut self, method_replacement: MethodReplacement) { + self.method_replacement = Some(method_replacement); + } + + pub fn function_info(&self) -> Option<&FunctionInfo> { + self.function_info.as_ref() + } +} + +#[derive(Debug)] +struct RejitItem { + module_ids: Vec, + method_ids: Vec, +} + +pub struct RejitHandler { + sender: Sender, + handle: JoinHandle<()>, + modules: HashMap, + modules_mutex: Mutex<()>, +} + +impl RejitHandler { + pub fn new(profiler_info: ICorProfilerInfo4) -> Self { + let (sender, receiver) = channel::(); + let handle = thread::spawn(move || { + // initialize the current thread in advance of rejit calls + match profiler_info.initialize_current_thread() { + Ok(_) => { + while let Ok(item) = receiver.recv() { + match profiler_info.request_rejit(&item.module_ids, &item.method_ids) { + Ok(_) => { + log::info!( + "request ReJIT done for {} methods", + item.method_ids.len() + ); + } + Err(e) => { + log::warn!( + "error requesting ReJIT for {} methods. {}", + item.method_ids.len(), + e + ); + } + } + } + } + Err(hr) => { + log::warn!("call to initialize_current_thread failed: {}", hr); + } + } + }); + + Self { + sender, + handle, + modules: HashMap::new(), + modules_mutex: Mutex::new(()), + } + } + + pub fn shutdown(self) { + // dropping channel sender causes the channel receiver to Err and break out thread loop. + drop(self.sender); + match self.handle.join() { + Ok(()) => log::trace!("rejit thread finished"), + Err(err) => log::error!("Error in joining rejit thread"), + } + } + + pub fn enqueue_for_rejit(&self, module_ids: Vec, method_ids: Vec) { + if let Err(err) = self.sender.send(RejitItem { + module_ids, + method_ids, + }) { + log::warn!( + "Unable to send module_ids={:?} method_ids={:?} for rejit", + &err.0.method_ids, + &err.0.module_ids + ); + } + } + + pub fn notify_rejit_compilation_started( + &self, + function_id: FunctionID, + rejit_id: ReJITID, + ) -> Result<(), HRESULT> { + log::debug!( + "notify_rejit_compilation_started: function_id={}, rejit_id={}", + function_id, + rejit_id + ); + Ok(()) + } + + pub fn notify_rejit_parameters( + &mut self, + module_id: ModuleID, + method_id: mdMethodDef, + function_control: &ICorProfilerFunctionControl, + module_metadata: &ModuleMetadata, + module_wrapper_tokens: &mut ModuleWrapperTokens, + profiler_info: &ICorProfilerInfo4, + call_target_tokens: &mut CallTargetTokens, + ) -> Result<(), HRESULT> { + log::debug!( + "notify_rejit_parameters: module_id={} method_id={}, {}", + module_id, + method_id, + &module_metadata.assembly_name + ); + + let rejit_module = self.get_or_add_module(module_id); + let rejit_method = rejit_module.get_or_add_method(method_id); + + if rejit_method.function_info().is_none() { + log::warn!( + "notify_rejit_parameters: function_info is missing for method_def={}", + method_id + ); + return Err(S_FALSE); + } + + if rejit_method.method_replacement.is_none() { + log::warn!( + "notify_rejit_parameters: method_replacement is missing for method_def={}", + method_id + ); + return Err(S_FALSE); + } + + calltarget_rewriter_callback( + module_metadata, + module_wrapper_tokens, + rejit_module, + method_id, + function_control, + profiler_info, + call_target_tokens, + ) + } + + pub fn get_or_add_module(&mut self, module_id: ModuleID) -> &mut RejitHandlerModule { + let _lock = self.modules_mutex.lock().unwrap(); + self.modules + .entry(module_id) + .or_insert_with(|| RejitHandlerModule::new(module_id)) + } +} + +pub fn calltarget_rewriter_callback( + module_metadata: &ModuleMetadata, + module_wrapper_tokens: &mut ModuleWrapperTokens, + rejit_handler_module: &mut RejitHandlerModule, + method_id: mdMethodDef, + function_control: &ICorProfilerFunctionControl, + profiler_info: &ICorProfilerInfo4, + call_target_tokens: &mut CallTargetTokens, +) -> Result<(), HRESULT> { + log::trace!("called calltarget_rewriter_callback"); + + let module_id = rejit_handler_module.module_id; + let rejit_handler_module_method = rejit_handler_module.get_or_add_method(method_id); + + let caller = rejit_handler_module_method.function_info.as_ref().unwrap(); + let function_token = caller.id; + let parsed_function_method_signature = caller.method_signature.try_parse().unwrap(); + let ret_func_arg = parsed_function_method_signature.return_type(); + let method_replacement = rejit_handler_module_method + .method_replacement + .as_ref() + .unwrap(); + + let (_, ret_type_flags) = ret_func_arg.get_type_flags(); + let is_void = ret_type_flags.contains(MethodArgumentTypeFlag::VOID); + let is_static = !caller + .method_signature + .calling_convention() + .unwrap() + .contains(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_HASTHIS); + + let method_arguments = parsed_function_method_signature.arguments(); + let num_args = parsed_function_method_signature.arg_len; + let wrapper = method_replacement.wrapper().unwrap(); + let wrapper_method_key = wrapper.get_method_cache_key(); + + let wrapper_method_ref = process::get_wrapper_method_ref( + profiler_info, + module_metadata, + module_wrapper_tokens, + module_id, + wrapper, + &wrapper_method_key, + )?; + + let meta_emit = &module_metadata.emit; + let meta_import = &module_metadata.import; + + log::debug!("calltarget_rewriter_callback: start {}() [is_void={}, is_static={}, integration_type={}, arguments={}]", + caller.full_name(), + is_void, + is_static, + &wrapper.type_name, + num_args + ); + + if !crate::profiler::profiler_assembly_loaded_in_app_domain(module_metadata.app_domain_id) { + log::warn!("calltarget_rewriter_callback: skipping method as method replacement \ + found but profiler has not been loaded into app domain. app_domain_id={}, token={}, caller_name={}()", + module_metadata.app_domain_id, + function_token, + caller.full_name() + ); + return Err(S_FALSE); + } + + let il_body = profiler_info.get_il_function_body(module_id, function_token)?; + let mut method = Method::new(il_body.into()).map_err(|e| { + log::warn!("calltarget_rewriter_callback: error decoding il. {:?}", e); + S_FALSE + })?; + + let original_il = if *env::ELASTIC_APM_PROFILER_LOG_IL { + Some(helpers::get_il_codes( + "IL original code for caller: ", + &method, + caller, + module_metadata, + )) + } else { + None + }; + + // expand to a fat method with fat sections to work with + method.expand_tiny_to_fat(); + method.expand_small_sections_to_fat(); + + let (local_sig, instructions) = + call_target_tokens.modify_local_sig_and_initialize(&method, caller, module_metadata)?; + + method + .header + .set_local_var_sig_tok(local_sig.new_local_var_sig); + + let mut idx: usize = instructions.len(); + method.insert_prelude(instructions).map_err(|_| S_FALSE)?; + + let type_info = caller.type_info.as_ref().unwrap(); + + if is_static { + if type_info.is_value_type { + log::warn!("calltarget_rewriter_callback: static methods on value types cannot be instrumented"); + return Err(S_FALSE); + } + method + .insert(idx, Instruction::ldnull()) + .map_err(|_| S_FALSE)?; + idx += 1; + } else { + method + .insert(idx, Instruction::ldarg_0()) + .map_err(|_| S_FALSE)?; + idx += 1; + + if type_info.is_value_type { + if type_info.type_spec != mdTypeSpecNil { + method + .insert(idx, Instruction::ldobj(type_info.type_spec)) + .map_err(|_| S_FALSE)?; + idx += 1; + } else if !type_info.is_generic { + method + .insert(idx, Instruction::ldobj(type_info.id)) + .map_err(|_| S_FALSE)?; + idx += 1; + } else { + // Generic struct instrumentation is not supported + // IMetaDataImport::GetMemberProps and IMetaDataImport::GetMemberRefProps returns + // The parent token as mdTypeDef and not as a mdTypeSpec + // that's because the method definition is stored in the mdTypeDef + // The problem is that we don't have the exact Spec of that generic + // We can't emit LoadObj or Box because that would result in an invalid IL. + // This problem doesn't occur on a class type because we can always relay in the + // object type. + return Err(S_FALSE); + } + } + } + + // insert instructions for loading arguments + if num_args < CallTargetTokens::FAST_PATH_COUNT as u8 { + // load arguments directly + for (i, method_argument) in method_arguments.iter().enumerate() { + let arg = if is_static { i } else { i + 1 }; + method + .insert(idx, Instruction::load_argument(arg as u16)) + .map_err(|_| S_FALSE)?; + idx += 1; + let (_, flags) = method_argument.get_type_flags(); + if flags.contains(MethodArgumentTypeFlag::BY_REF) { + log::warn!("calltarget_rewriter_callback: methods with ref parameters cannot be instrumented"); + return Err(S_FALSE); + } + } + } else { + // load into an object array + method + .insert(idx, Instruction::load_int32(num_args as i32)) + .map_err(|_| S_FALSE)?; + idx += 1; + method + .insert( + idx, + Instruction::newarr(call_target_tokens.get_object_type_ref()), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + for (i, method_argument) in method_arguments.iter().enumerate() { + method + .insert(idx, Instruction::dup()) + .map_err(|_| S_FALSE)?; + idx += 1; + method + .insert(idx, Instruction::load_int32(i as i32)) + .map_err(|_| S_FALSE)?; + idx += 1; + + let arg = if is_static { i } else { i + 1 }; + method + .insert(idx, Instruction::load_argument(arg as u16)) + .map_err(|_| S_FALSE)?; + idx += 1; + + let (_, flags) = method_argument.get_type_flags(); + if flags.contains(MethodArgumentTypeFlag::BY_REF) { + log::warn!("calltarget_rewriter_callback: methods with ref parameters cannot be instrumented"); + return Err(S_FALSE); + } + + if flags.contains(MethodArgumentTypeFlag::BOXED_TYPE) { + let tok = method_argument + .get_type_tok(meta_emit, call_target_tokens.get_cor_lib_assembly_ref())?; + if tok == mdTokenNil { + return Err(S_FALSE); + } + method + .insert(idx, Instruction::box_(tok)) + .map_err(|_| S_FALSE)?; + idx += 1; + } + + method + .insert(idx, Instruction::stelem_ref()) + .map_err(|_| S_FALSE)?; + idx += 1; + } + } + + if log::log_enabled!(Level::Trace) { + log_caller_type_info(caller, type_info); + } + + let begin_method = call_target_tokens.write_begin_method( + wrapper_method_ref.type_ref, + type_info, + &method_arguments, + module_metadata, + )?; + + method.insert(idx, begin_method).map_err(|_| S_FALSE)?; + idx += 1; + + method + .insert( + idx, + Instruction::store_local(local_sig.call_target_state_index as u16), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + + // Capture the idx so that we can update the instruction offset later + method + .insert(idx, Instruction::leave_s(-1)) + .map_err(|_| S_FALSE)?; + let state_leave_to_begin_original_method_idx = idx; + idx += 1; + + let log_exception = call_target_tokens.write_log_exception( + wrapper_method_ref.type_ref, + type_info, + module_metadata, + )?; + + // clone the log exception instruction as we'll use the original later + method + .insert(idx, log_exception.clone()) + .map_err(|_| S_FALSE)?; + let begin_method_log_exception_idx = idx; + idx += 1; + + method + .insert(idx, Instruction::leave_s(0)) + .map_err(|_| S_FALSE)?; + let begin_method_catch_leave_idx = idx; + idx += 1; + + let offsets = method.get_instruction_offsets(); + + let begin_method_ex_clause = FatSectionClause { + flag: CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_NONE, + try_offset: 0, + try_length: offsets[begin_method_log_exception_idx] as u32, + handler_offset: offsets[begin_method_log_exception_idx] as u32, + handler_length: (offsets[begin_method_catch_leave_idx + 1] + - offsets[begin_method_log_exception_idx]) as u32, + class_token_or_filter_offset: call_target_tokens.get_ex_type_ref(), + }; + + // original method + + // The idx of the start of the original instructions + let begin_original_method_idx = idx; + + // update the leave_s instruction offset to point to the original method + if let Some(state_leave) = method + .instructions + .get_mut(state_leave_to_begin_original_method_idx) + { + if let ShortInlineBrTarget(offset) = &mut state_leave.operand { + let state_offset = (offsets[begin_original_method_idx] + - offsets[state_leave_to_begin_original_method_idx + 1]) + as u32; + *offset = state_offset as i8; + } + } + + // write end + + // add return instruction at the end + idx = method.instructions.len(); + method + .insert(idx, Instruction::ret()) + .map_err(|_| S_FALSE)?; + + // store any original exception that might be thrown, so that we can capture it in our end method + method + .insert( + idx, + Instruction::store_local(local_sig.exception_index as u16), + ) + .map_err(|_| S_FALSE)?; + let mut start_exception_catch_idx = idx; + idx += 1; + + // then rethrow any original exception + method + .insert(idx, Instruction::rethrow()) + .map_err(|_| S_FALSE)?; + let mut rethrow_idx = idx; + idx += 1; + + let mut end_method_try_start_idx = idx; + if is_static { + if type_info.is_value_type { + log::warn!("calltarget_rewriter_callback: static methods on value types cannot be instrumented"); + return Err(S_FALSE); + } + method + .insert(idx, Instruction::ldnull()) + .map_err(|_| S_FALSE)?; + idx += 1; + } else { + method + .insert(idx, Instruction::ldarg_0()) + .map_err(|_| S_FALSE)?; + idx += 1; + + if type_info.is_value_type { + if type_info.type_spec != mdTypeSpecNil { + method + .insert(idx, Instruction::ldobj(type_info.type_spec)) + .map_err(|_| S_FALSE)?; + idx += 1; + } else if !type_info.is_generic { + method + .insert(idx, Instruction::ldobj(type_info.id)) + .map_err(|_| S_FALSE)?; + idx += 1; + } else { + // Generic struct instrumentation is not supported + // IMetaDataImport::GetMemberProps and IMetaDataImport::GetMemberRefProps returns + // The parent token as mdTypeDef and not as a mdTypeSpec + // that's because the method definition is stored in the mdTypeDef + // The problem is that we don't have the exact Spec of that generic + // We can't emit LoadObj or Box because that would result in an invalid IL. + // This problem doesn't occur on a class type because we can always relay in the + // object type. + return Err(S_FALSE); + } + } + } + + if !is_void { + method + .insert( + idx, + Instruction::load_local(local_sig.return_value_index as u16), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + } + + method + .insert( + idx, + Instruction::load_local(local_sig.exception_index as u16), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + method + .insert( + idx, + Instruction::load_local(local_sig.call_target_state_index as u16), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + + let end_method_call_instruction = if is_void { + call_target_tokens.write_end_void_return_member_ref( + wrapper_method_ref.type_ref, + type_info, + module_metadata, + )? + } else { + call_target_tokens.write_end_return_member_ref( + wrapper_method_ref.type_ref, + type_info, + &ret_func_arg, + module_metadata, + )? + }; + + method + .insert(idx, end_method_call_instruction) + .map_err(|_| S_FALSE)?; + idx += 1; + + method + .insert( + idx, + Instruction::store_local(local_sig.call_target_return_index as u16), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + + if !is_void { + method + .insert( + idx, + Instruction::load_local_address(local_sig.call_target_return_index as u16), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + + let get_return_value_instruction = call_target_tokens + .write_call_target_return_get_return_value( + local_sig.call_target_return_token, + module_metadata, + )?; + + method + .insert(idx, get_return_value_instruction) + .map_err(|_| S_FALSE)?; + idx += 1; + method + .insert( + idx, + Instruction::store_local(local_sig.return_value_index as u16), + ) + .map_err(|_| S_FALSE)?; + idx += 1; + } + + method + .insert(idx, Instruction::leave_s(-1)) + .map_err(|_| S_FALSE)?; + let mut end_method_try_leave_idx = idx; + idx += 1; + + method.insert(idx, log_exception).map_err(|_| S_FALSE)?; + let mut end_method_catch_start_idx = idx; + idx += 1; + + method + .insert(idx, Instruction::leave_s(0)) + .map_err(|_| S_FALSE)?; + let mut end_method_catch_leave_idx = idx; + idx += 1; + + method + .insert(idx, Instruction::endfinally()) + .map_err(|_| S_FALSE)?; + let mut end_finally_idx = idx; + idx += 1; + + if !is_void { + method + .insert( + idx, + Instruction::load_local(local_sig.return_value_index as u16), + ) + .map_err(|_| S_FALSE)?; + } + + let mut i = start_exception_catch_idx; + let mut added_instruction_count = 0; + + // change all original method ret instructions to leave.s or leave instructions + // with an offset pointing to the instruction before the ending ret instruction. + while i > begin_original_method_idx { + if method.instructions[i].opcode != RET { + i -= 1; + continue; + } + + let mut current = i; + + if !is_void { + // Since we're adding additional instructions to the original method, + // make a note of how many are added so that we can later increment the indices + // for instructions that are targets for clauses that come after the original + // method instructions + added_instruction_count += 1; + method + .insert( + i, + Instruction::store_local(local_sig.return_value_index as u16), + ) + .map_err(|_| S_FALSE)?; + + i -= 1; + current += 1; + } + + // calculate the offset to the target instruction to determine whether to + // insert a leave_s or leave instruction + let leave_instr = { + let mut leave_offset = method + .instructions + .iter() + .skip(current + 1) + .map(|i| i.len()) + .sum::() + - Instruction::ret().len(); + + if !is_void { + leave_offset -= Instruction::load_local(local_sig.return_value_index as u16).len(); + } + + if leave_offset > i8::MAX as usize { + Instruction::leave(leave_offset as i32) + } else { + Instruction::leave_s(leave_offset as i8) + } + }; + + method.replace(current, leave_instr).map_err(|_| S_FALSE)?; + i -= 1; + } + + if added_instruction_count > 0 { + start_exception_catch_idx += added_instruction_count; + rethrow_idx += added_instruction_count; + end_method_try_start_idx += added_instruction_count; + end_method_try_leave_idx += added_instruction_count; + end_method_catch_start_idx += added_instruction_count; + end_method_catch_leave_idx += added_instruction_count; + end_finally_idx += added_instruction_count; + } + + let offsets = method.get_instruction_offsets(); + + // update the end method leave_s instruction offset to point to the endfinally + if let Some(end_method_try_leave) = method.instructions.get_mut(end_method_try_leave_idx) { + if let ShortInlineBrTarget(offset) = &mut end_method_try_leave.operand { + let finally_offset = + (offsets[end_finally_idx] - offsets[end_method_try_leave_idx + 1]) as u32; + *offset = finally_offset as i8; + } + } + + // create all the later clauses, now that all instructions are inserted + let end_method_ex_clause = { + FatSectionClause { + flag: CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_NONE, + try_offset: offsets[end_method_try_start_idx], + try_length: offsets[end_method_try_leave_idx + 1] - offsets[end_method_try_start_idx], + handler_offset: offsets[end_method_catch_start_idx], + handler_length: offsets[end_method_catch_leave_idx + 1] + - offsets[end_method_catch_start_idx], + class_token_or_filter_offset: call_target_tokens.get_ex_type_ref(), + } + }; + + let ex_clause = FatSectionClause { + flag: CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_NONE, + try_offset: 0, + try_length: offsets[start_exception_catch_idx], + handler_offset: offsets[start_exception_catch_idx], + handler_length: offsets[rethrow_idx + 1] - offsets[start_exception_catch_idx], + class_token_or_filter_offset: call_target_tokens.get_ex_type_ref(), + }; + + let finally_clause = FatSectionClause { + flag: CorExceptionFlag::COR_ILEXCEPTION_CLAUSE_FINALLY, + try_offset: 0, + try_length: offsets[rethrow_idx + 1], + handler_offset: offsets[rethrow_idx + 1], + handler_length: offsets[end_finally_idx + 1] - offsets[rethrow_idx + 1], + class_token_or_filter_offset: mdTokenNil, + }; + + // add the exception handling clauses to the method + method + .push_clauses(vec![ + begin_method_ex_clause, + end_method_ex_clause, + ex_clause, + finally_clause, + ]) + .map_err(|e| { + log::warn!("calltarget_rewriter_callback: could not add clauses to method"); + S_FALSE + })?; + + if *env::ELASTIC_APM_PROFILER_LOG_IL { + let modified_il = helpers::get_il_codes( + "IL modification for caller: ", + &method, + caller, + module_metadata, + ); + log::debug!("{}\n{}", original_il.unwrap_or_default(), modified_il); + } + + let method_bytes = method.into_bytes(); + + // write the new IL + function_control + .set_il_function_body(&method_bytes) + .map_err(|e| { + log::warn!( + "calltarget_rewriter_callback: failed to set il function body for \ + module_id={} function_token={}", + module_id, + function_token + ); + e + })?; + + log::info!("calltarget_rewriter_callback: finished {}() [is_void={}, is_static={}, integration_type={}, arguments={}]", + caller.full_name(), + is_void, + is_static, + &wrapper.type_name, + num_args + ); + + Ok(()) +} + +fn log_caller_type_info(caller: &FunctionInfo, type_info: &TypeInfo) { + let mut s = vec![ + format!("caller type.id: {}", caller.id), + format!("caller type.is_generic: {}", type_info.is_generic), + format!("caller type.name: {}", &type_info.name), + format!("caller type.token_type: {:?}", type_info.token_type), + format!("caller type.spec: {}", type_info.type_spec), + format!("caller type.is_value_type: {}", type_info.is_value_type), + ]; + + if let Some(extend_from) = &type_info.extends_from { + s.push(format!("caller type extend_from.id: {}", extend_from.id)); + s.push(format!( + "caller type extend_from.is_generic: {}", + extend_from.is_generic + )); + s.push(format!( + "caller type extend_from.name: {}", + &extend_from.name + )); + s.push(format!( + "caller type extend_from.token_type: {:?}", + extend_from.token_type + )); + s.push(format!( + "caller type extend_from.spec: {}", + extend_from.type_spec + )); + s.push(format!( + "caller type extend_from.is_value_type: {}", + extend_from.is_value_type + )); + } + + if let Some(parent_type) = &type_info.parent_type { + s.push(format!("caller parent_type.id: {}", parent_type.id)); + s.push(format!( + "caller parent_type.is_generic: {}", + parent_type.is_generic + )); + s.push(format!("caller parent_type.name: {}", &parent_type.name)); + s.push(format!( + "caller parent_type.token_type: {:?}", + parent_type.token_type + )); + s.push(format!( + "caller parent_type.spec: {}", + parent_type.type_spec + )); + s.push(format!( + "caller parent_type.is_value_type: {}", + parent_type.is_value_type + )); + } + + log::trace!("{}", s.join("\n")); +} diff --git a/src/elastic_apm_profiler/src/profiler/sig.rs b/src/elastic_apm_profiler/src/profiler/sig.rs new file mode 100644 index 000000000..1cb42f71a --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/sig.rs @@ -0,0 +1,648 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + cil::{uncompress_data, uncompress_token}, + ffi::{CorCallingConvention, CorElementType, E_FAIL, ULONG}, + interfaces::IMetaDataImport2, + profiler::types::{FunctionInfo, ModuleMetadata, TypeInfo}, +}; +use com::sys::HRESULT; +use num_traits::FromPrimitive; + +fn parse_type_def_or_ref_encoded(signature: &[u8]) -> Option<(ULONG, usize)> { + parse_number(signature) +} + +fn parse_custom_mod(signature: &[u8]) -> Option { + if let Some(cor_element_type) = CorElementType::from_u8(signature[0]) { + match cor_element_type { + CorElementType::ELEMENT_TYPE_CMOD_OPT | CorElementType::ELEMENT_TYPE_CMOD_REQD => { + let idx = 1_usize; + if let Some((_, token_idx)) = parse_type_def_or_ref_encoded(&signature[idx..]) { + Some(idx + token_idx) + } else { + Some(idx) + } + } + _ => None, + } + } else { + None + } +} + +fn parse_optional_custom_mods(signature: &[u8]) -> Option { + let mut idx = 0; + + loop { + if let Some(cor_element_type) = CorElementType::from_u8(signature[idx]) { + match cor_element_type { + CorElementType::ELEMENT_TYPE_CMOD_OPT | CorElementType::ELEMENT_TYPE_CMOD_REQD => { + if let Some(mod_idx) = parse_custom_mod(&signature) { + idx += mod_idx; + } else { + return None; + } + } + _ => return Some(idx), + } + } else { + return Some(idx); + } + } +} + +/// parses a number from a bytes slice. +/// first value is the number +/// second value is the count of bytes +pub fn parse_number(signature: &[u8]) -> Option<(ULONG, usize)> { + //uncompress_data(signature).map(|(p, c) | (c as ULONG, p as usize)) + + if signature.is_empty() { + return None; + } + + let b1 = signature[0] as ULONG; + + if b1 == 0xff { + return None; + } + + if b1 & 0x80 == 0 { + return Some((b1, 1_usize)); + } + + if signature.len() < 2 { + return None; + } + + let b2 = signature[1] as ULONG; + + if b1 & 0x40 == 0 { + let out = ((b1 & 0x3f) << 8) | b2; + return Some((out, 2_usize)); + } + + if b1 & 0x20 != 0 { + // 4 byte encoding has this bit clear -- error if not + return None; + } + + if signature.len() < 4 { + return None; + } + + let b3 = signature[2] as ULONG; + let b4 = signature[3] as ULONG; + + let out = ((b1 & 0x1f) << 24) | (b2 << 16) | (b3 << 8) | b4; + Some((out, 4_usize)) +} + +fn parse_return_type(signature: &[u8]) -> Option { + if let Some(mut idx) = parse_optional_custom_mods(&signature) { + if let Some(cor_element_type) = CorElementType::from_u8(signature[idx]) { + match cor_element_type { + CorElementType::ELEMENT_TYPE_TYPEDBYREF | CorElementType::ELEMENT_TYPE_VOID => { + idx += 1; + return Some(idx); + } + CorElementType::ELEMENT_TYPE_BYREF => { + idx += 1; + } + _ => {} + } + } + + parse_type(&signature[idx..]).map(|i| idx + i) + } else { + None + } +} + +fn parse_method(signature: &[u8]) -> Option { + let mut idx = 0; + if let Some(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_GENERIC) = + CorCallingConvention::from_bits(signature[idx]) + { + idx += 1; + if let Some((_, gen_idx)) = parse_number(&signature[idx..]) { + idx += gen_idx; + } else { + return None; + } + } + + let params; + if let Some((params_len, param_idx)) = parse_number(&signature[idx..]) { + idx += param_idx; + params = params_len; + } else { + return None; + } + + if let Some(ret_idx) = parse_return_type(&signature[idx..]) { + idx += ret_idx; + } else { + return None; + } + + let mut sentinel_found = false; + for _ in 0..params { + if let Some(CorElementType::ELEMENT_TYPE_SENTINEL) = CorElementType::from_u8(signature[idx]) + { + if sentinel_found { + return None; + } + + sentinel_found = true; + idx += 1; + } + + if let Some(param_idx) = parse_param(&signature[idx..]) { + idx += param_idx; + } else { + return None; + } + } + + Some(idx) +} + +fn parse_param(signature: &[u8]) -> Option { + if let Some(mut idx) = parse_optional_custom_mods(&signature) { + if let Some(cor_element_type) = CorElementType::from_u8(signature[idx]) { + match cor_element_type { + CorElementType::ELEMENT_TYPE_TYPEDBYREF => { + idx += 1; + return Some(idx); + } + CorElementType::ELEMENT_TYPE_BYREF => { + idx += 1; + } + _ => {} + } + } + + parse_type(&signature[idx..]).map(|i| idx + i) + } else { + None + } +} + +fn parse_array_shape(signature: &[u8]) -> Option { + let mut idx = 0; + + if let Some((_, rank_idx)) = parse_number(&signature[idx..]) { + idx += rank_idx as usize; + } else { + return None; + } + + let num_sizes; + if let Some((num, numsize_idx)) = parse_number(&signature[idx..]) { + idx += numsize_idx; + num_sizes = num; + } else { + return None; + } + + for _ in 0..num_sizes { + if let Some((_, size_idx)) = parse_number(&signature[idx..]) { + idx += size_idx; + } else { + return None; + } + } + + if let Some((_, size_idx)) = parse_number(&signature[idx..]) { + idx += size_idx; + } else { + return None; + } + + for _ in 0..num_sizes { + if let Some((_, size_idx)) = parse_number(&signature[idx..]) { + idx += size_idx; + } else { + return None; + } + } + + Some(idx) +} + +pub fn parse_type(signature: &[u8]) -> Option { + let mut idx = 0; + if let Some(cor_element_type) = CorElementType::from_u8(signature[idx]) { + idx += 1; + + match cor_element_type { + CorElementType::ELEMENT_TYPE_VOID + | CorElementType::ELEMENT_TYPE_BOOLEAN + | CorElementType::ELEMENT_TYPE_CHAR + | CorElementType::ELEMENT_TYPE_I1 + | CorElementType::ELEMENT_TYPE_U1 + | CorElementType::ELEMENT_TYPE_I2 + | CorElementType::ELEMENT_TYPE_U2 + | CorElementType::ELEMENT_TYPE_I4 + | CorElementType::ELEMENT_TYPE_U4 + | CorElementType::ELEMENT_TYPE_I8 + | CorElementType::ELEMENT_TYPE_U8 + | CorElementType::ELEMENT_TYPE_R4 + | CorElementType::ELEMENT_TYPE_R8 + | CorElementType::ELEMENT_TYPE_STRING + | CorElementType::ELEMENT_TYPE_OBJECT => Some(idx), + CorElementType::ELEMENT_TYPE_PTR => { + if let Some(mods_idx) = parse_optional_custom_mods(&signature[idx..]) { + idx += mods_idx; + if let Some(CorElementType::ELEMENT_TYPE_VOID) = + CorElementType::from_u8(signature[idx]) + { + idx += 1; + Some(idx) + } else { + parse_type(&signature[idx..]).map(|i| idx + i) + } + } else { + None + } + } + CorElementType::ELEMENT_TYPE_VALUETYPE | CorElementType::ELEMENT_TYPE_CLASS => { + parse_type_def_or_ref_encoded(&signature[idx..]).map(|(_, i)| idx + i) + } + CorElementType::ELEMENT_TYPE_FNPTR => parse_method(&signature[idx..]).map(|i| idx + i), + CorElementType::ELEMENT_TYPE_ARRAY => { + if let Some(type_idx) = parse_type(&signature[idx..]) { + idx += type_idx; + } else { + return None; + } + + parse_array_shape(&signature[idx..]).map(|i| idx + i) + } + CorElementType::ELEMENT_TYPE_SZARRAY => { + if let Some(mods_idx) = parse_optional_custom_mods(&signature[idx..]) { + idx += mods_idx; + parse_type(&signature[idx..]).map(|i| idx + i) + } else { + None + } + } + CorElementType::ELEMENT_TYPE_GENERICINST => { + if let Some(elem_type) = CorElementType::from_u8(signature[idx]) { + match elem_type { + CorElementType::ELEMENT_TYPE_VALUETYPE + | CorElementType::ELEMENT_TYPE_CLASS => { + idx += 1; + + if let Some((_, type_idx)) = + parse_type_def_or_ref_encoded(&signature[idx..]) + { + idx += type_idx; + } else { + return None; + } + + let num; + if let Some((num_size, num_idx)) = parse_number(&signature[idx..]) { + idx += num_idx as usize; + num = num_size; + } else { + return None; + } + + for _ in 0..num { + if let Some(type_idx) = parse_type(&signature[idx..]) { + idx += type_idx; + } else { + return None; + } + } + + Some(idx) + } + _ => None, + } + } else { + None + } + } + CorElementType::ELEMENT_TYPE_VAR | CorElementType::ELEMENT_TYPE_MVAR => { + parse_number(&signature[idx..]).map(|(_, i)| idx + i) + } + _ => None, + } + } else { + None + } +} + +fn retrieve_type_for_signature( + metadata_import: &IMetaDataImport2, + signature: &[u8], +) -> Result<(TypeInfo, usize), HRESULT> { + let (token, len) = uncompress_token(signature); + match metadata_import.get_type_info(token) { + Ok(Some(type_info)) => Ok((type_info, len)), + Ok(None) => { + log::warn!( + "None type info from token={} in signature={:?}", + token, + signature + ); + Err(E_FAIL) + } + Err(e) => { + log::warn!( + "Could not get type info from token={} in signature={:?}, {}", + token, + signature, + e + ); + Err(e) + } + } +} + +pub fn parse_signature_types( + module_metadata: &ModuleMetadata, + function_info: &FunctionInfo, +) -> Option> { + let signature = &function_info.signature; + let signature_size = signature.len(); + let generic_count = signature.type_arguments_len(); + let param_count = signature.arguments_len(); + let start_index = if generic_count > 0 { 3 } else { 2 }; + + let expected_number_of_types = param_count + 1; + let mut current_type_index = 0; + let mut type_names = Vec::with_capacity(expected_number_of_types as usize); + + let mut generic_arg_stack = Vec::new(); + let mut append_to_type = String::new(); + let mut current_type_name = String::new(); + + let signature = signature.bytes(); + let params = &signature[start_index..]; + let mut enumerator = params.iter().enumerate().peekable(); + + while let Some((i, param_piece)) = enumerator.next() { + let cor_element_type: CorElementType = CorElementType::from_u8(*param_piece)?; + match cor_element_type { + CorElementType::ELEMENT_TYPE_END => { + continue; + } + CorElementType::ELEMENT_TYPE_VOID => current_type_name.push_str("System.Void"), + CorElementType::ELEMENT_TYPE_BOOLEAN => current_type_name.push_str("System.Boolean"), + CorElementType::ELEMENT_TYPE_CHAR => current_type_name.push_str("System.Char"), + CorElementType::ELEMENT_TYPE_I1 => current_type_name.push_str("System.SByte"), + CorElementType::ELEMENT_TYPE_U1 => current_type_name.push_str("System.Byte"), + CorElementType::ELEMENT_TYPE_I2 => current_type_name.push_str("System.Int16"), + CorElementType::ELEMENT_TYPE_U2 => current_type_name.push_str("System.UInt16"), + CorElementType::ELEMENT_TYPE_I4 => current_type_name.push_str("System.Int32"), + CorElementType::ELEMENT_TYPE_U4 => current_type_name.push_str("System.UInt32"), + CorElementType::ELEMENT_TYPE_I8 => current_type_name.push_str("System.Int64"), + CorElementType::ELEMENT_TYPE_U8 => current_type_name.push_str("System.UInt64"), + CorElementType::ELEMENT_TYPE_R4 => current_type_name.push_str("System.Single"), + CorElementType::ELEMENT_TYPE_R8 => current_type_name.push_str("System.Double"), + CorElementType::ELEMENT_TYPE_STRING => current_type_name.push_str("System.String"), + CorElementType::ELEMENT_TYPE_VALUETYPE | CorElementType::ELEMENT_TYPE_CLASS => { + let (j, next) = enumerator.next().unwrap(); + let type_info = retrieve_type_for_signature(&module_metadata.import, ¶ms[j..]); + if type_info.is_err() { + return None; + } + let (type_data, len) = type_info.unwrap(); + let mut examined_type_token = type_data.id; + let mut examined_type_name = type_data.name; + let mut ongoing_type_name = examined_type_name.to_string(); + + // check for nested class + while examined_type_name.contains('.') { + let parent_token = module_metadata + .import + .get_nested_class_props(examined_type_token); + if parent_token.is_err() { + break; + } + let parent_token = parent_token.unwrap(); + let nesting_type = module_metadata.import.get_type_info(parent_token); + if nesting_type.is_err() { + log::warn!( + "Could not retrieve type info for parent token {}", + parent_token + ); + return None; + } + let nesting_type = match nesting_type.unwrap() { + Some(n) => n, + None => { + return None; + } + }; + + examined_type_token = nesting_type.id; + examined_type_name = nesting_type.name; + ongoing_type_name = format!("{}+{}", &examined_type_name, &ongoing_type_name); + } + + // skip len number of items + for _ in 0..(len - 1) { + enumerator.next(); + } + current_type_name.push_str(&ongoing_type_name); + } + CorElementType::ELEMENT_TYPE_BYREF => current_type_name.push_str("ref"), + CorElementType::ELEMENT_TYPE_GENERICINST => { + // skip generic type indicator token + let _ = enumerator.next(); + // skip generic type token + let (j, next) = enumerator.next().unwrap(); + let generic_type_info = + retrieve_type_for_signature(&module_metadata.import, ¶ms[j..]); + if generic_type_info.is_err() { + return None; + } + let (generic_type_info, len) = generic_type_info.unwrap(); + let type_name = &generic_type_info.name; + current_type_name.push_str(type_name); + current_type_name.push('<'); + + if !generic_arg_stack.is_empty() { + generic_arg_stack[0] -= 1; + } + + let arity_index = type_name.rfind('`').unwrap(); + let actual_args = type_name[(arity_index + 1)..].parse::().unwrap(); + generic_arg_stack.insert(0, actual_args); + + for _ in 0..len { + enumerator.next(); + } + continue; + } + CorElementType::ELEMENT_TYPE_OBJECT => current_type_name.push_str("System.Object"), + CorElementType::ELEMENT_TYPE_SZARRAY => { + append_to_type.push_str("[]"); + while let Some((j, next)) = enumerator.peek() { + // check it's an array element type + if let Some(CorElementType::ELEMENT_TYPE_SZARRAY) = + CorElementType::from_u8(**next) + { + append_to_type.push_str("[]"); + enumerator.next(); + } else { + break; + } + } + + continue; + } + CorElementType::ELEMENT_TYPE_MVAR | CorElementType::ELEMENT_TYPE_VAR => { + let (token, len) = uncompress_token(¶ms[i..]); + current_type_name.push('T'); + // skip len number of items + for _ in 0..len { + enumerator.next(); + } + // TODO: implement conventions for generics (eg., TC1, TC2, TM1, TM2) + // current_type_name.push(type_token.to_string()); + } + _ => { + log::warn!("Unexpected element type: {:?}", cor_element_type); + current_type_name.push_str(&format!("{}", cor_element_type as u32)); + } + } + + if !append_to_type.is_empty() { + current_type_name.push_str(&append_to_type); + append_to_type = String::new(); + } + + if !generic_arg_stack.is_empty() { + generic_arg_stack[0] -= 1; + + if generic_arg_stack[0] > 0 { + current_type_name.push_str(", "); + } + } + + while !generic_arg_stack.is_empty() && generic_arg_stack[0] == 0 { + generic_arg_stack.remove(0); + current_type_name.push('>'); + + if !generic_arg_stack.is_empty() && generic_arg_stack[0] > 0 { + current_type_name.push_str(", "); + } + } + + if !generic_arg_stack.is_empty() { + continue; + } + + if current_type_index >= expected_number_of_types { + return None; + } + + type_names.push(current_type_name); + current_type_name = String::new(); + current_type_index += 1; + } + + Some(type_names) +} + +pub fn get_sig_type_token_name( + signature: &[u8], + metadata_import: &IMetaDataImport2, +) -> (String, usize) { + let mut token_name = String::new(); + let mut ref_flag = false; + let start_idx = 0; + let mut idx = start_idx; + + if signature[idx] == CorElementType::ELEMENT_TYPE_BYREF as u8 { + idx += 1; + ref_flag = true; + } + + if let Some(elem_type) = CorElementType::from_u8(signature[idx]) { + idx += 1; + match elem_type { + CorElementType::ELEMENT_TYPE_BOOLEAN => token_name.push_str("System.Boolean"), + CorElementType::ELEMENT_TYPE_CHAR => token_name.push_str("System.Char"), + CorElementType::ELEMENT_TYPE_I1 => token_name.push_str("System.SByte"), + CorElementType::ELEMENT_TYPE_U1 => token_name.push_str("System.Byte"), + CorElementType::ELEMENT_TYPE_I2 => token_name.push_str("System.Int16"), + CorElementType::ELEMENT_TYPE_U2 => token_name.push_str("System.UInt16"), + CorElementType::ELEMENT_TYPE_I4 => token_name.push_str("System.Int32"), + CorElementType::ELEMENT_TYPE_U4 => token_name.push_str("System.UInt32"), + CorElementType::ELEMENT_TYPE_I8 => token_name.push_str("System.Int64"), + CorElementType::ELEMENT_TYPE_U8 => token_name.push_str("System.UInt64"), + CorElementType::ELEMENT_TYPE_R4 => token_name.push_str("System.Single"), + CorElementType::ELEMENT_TYPE_R8 => token_name.push_str("System.Double"), + CorElementType::ELEMENT_TYPE_STRING => token_name.push_str("System.String"), + CorElementType::ELEMENT_TYPE_OBJECT => token_name.push_str("System.Object"), + CorElementType::ELEMENT_TYPE_CLASS | CorElementType::ELEMENT_TYPE_VALUETYPE => { + let (token, len) = uncompress_token(&signature[idx..]); + idx += len; + if let Ok(Some(type_info)) = metadata_import.get_type_info(token) { + token_name.push_str(&type_info.name); + } + } + CorElementType::ELEMENT_TYPE_SZARRAY => { + let (elem, len) = get_sig_type_token_name(&signature[idx..], metadata_import); + idx += len; + token_name.push_str(&elem); + token_name.push_str("[]"); + } + CorElementType::ELEMENT_TYPE_GENERICINST => { + let (elem, len) = get_sig_type_token_name(&signature[idx..], metadata_import); + idx += len; + token_name.push_str(&elem); + token_name.push('['); + + if let Some((data, len)) = uncompress_data(&signature[idx..]) { + idx += len; + for i in 0..data { + let (elem, end_idx) = + get_sig_type_token_name(&signature[idx..], metadata_import); + idx += end_idx; + token_name.push_str(&elem); + if i != (data - 1) { + token_name.push(','); + } + } + } + token_name.push(']'); + } + CorElementType::ELEMENT_TYPE_MVAR => { + if let Some((data, len)) = uncompress_data(&signature[idx..]) { + idx += len; + token_name.push_str("!!"); + token_name.push_str(&format!("{}", data)); + } + } + CorElementType::ELEMENT_TYPE_VAR => { + if let Some((data, len)) = uncompress_data(&signature[idx..]) { + idx += len; + token_name.push('!'); + token_name.push_str(&format!("{}", data)); + } + } + _ => {} + } + } else { + return (token_name, idx); + } + + if ref_flag { + token_name.push('&'); + } + + (token_name, idx) +} diff --git a/src/elastic_apm_profiler/src/profiler/startup_hook.rs b/src/elastic_apm_profiler/src/profiler/startup_hook.rs new file mode 100644 index 000000000..0e6230c76 --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/startup_hook.rs @@ -0,0 +1,456 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + cil::{compress_token, Instruction, Method, MethodHeader}, + ffi::{ + mdMethodDef, mdToken, CorCallingConvention, CorElementType, CorFieldAttr, CorMethodAttr, + CorMethodImpl, CorPinvokeMap, CorTypeAttr, ModuleID, COR_SIGNATURE, E_FAIL, ULONG, + }, + interfaces::ICorProfilerInfo4, + profiler::{env, helpers, managed, types::ModuleMetadata}, +}; +use com::sys::HRESULT; + +/// Generates the IL for the startup hook method to load the Elastic.Apm.Profiler.Managed.Loader +/// assembly embedded in the profiler, and inserts into the method body of the method defined +/// by module_id and function_token +pub fn run_il_startup_hook( + profiler_info: &ICorProfilerInfo4, + module_metadata: &ModuleMetadata, + module_id: ModuleID, + function_token: mdToken, +) -> Result<(), HRESULT> { + let startup_method_def = + generate_void_il_startup_method(profiler_info, module_id, module_metadata)?; + let il_body = profiler_info.get_il_function_body(module_id, function_token)?; + let mut method = Method::new(il_body.into()).map_err(|e| { + log::warn!("run_il_startup_hook: error decoding il. {:?}", e); + E_FAIL + })?; + + method + .insert_prelude(vec![Instruction::call(startup_method_def)]) + .map_err(|e| { + log::warn!("run_il_startup_hook: error inserting prelude. {:?}", e); + E_FAIL + })?; + + let method_bytes = method.into_bytes(); + let allocator = profiler_info.get_il_function_body_allocator(module_id)?; + let allocated_bytes = allocator.alloc(method_bytes.len() as ULONG)?; + let address = unsafe { allocated_bytes.into_inner() }; + unsafe { + std::ptr::copy(method_bytes.as_ptr(), address, method_bytes.len()); + } + profiler_info + .set_il_function_body(module_id, function_token, address as *const _) + .map_err(|e| { + log::warn!("run_il_startup_hook: failed to set il for startup hook"); + e + })?; + + Ok(()) +} + +fn generate_void_il_startup_method( + profiler_info: &ICorProfilerInfo4, + module_id: ModuleID, + module_metadata: &ModuleMetadata, +) -> Result { + let corlib_ref = helpers::create_assembly_ref_to_corlib( + &module_metadata.assembly_emit, + &module_metadata.cor_assembly_property, + )?; + + log::trace!( + "generate_void_il_startup_method: created corlib ref {} to {}", + corlib_ref, + &module_metadata.cor_assembly_property.name, + ); + + let object_type_ref = module_metadata + .emit + .define_type_ref_by_name(corlib_ref, "System.Object") + .map_err(|e| { + log::warn!("error defining type ref by name for System.Object. {:X}", e); + e + })?; + + let new_type_def = module_metadata + .emit + .define_type_def( + "__ElasticVoidMethodType__", + CorTypeAttr::tdAbstract | CorTypeAttr::tdSealed, + object_type_ref, + None, + ) + .map_err(|e| { + log::warn!("error defining type def __ElasticVoidMethodType__. {:X}", e); + e + })?; + + let initialize_signature = &[ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits(), + 0, + CorElementType::ELEMENT_TYPE_VOID as COR_SIGNATURE, + ]; + + log::trace!("generate_void_il_startup_method: define method __ElasticVoidMethodCall__"); + + let new_method = module_metadata + .emit + .define_method( + new_type_def, + "__ElasticVoidMethodCall__", + CorMethodAttr::mdStatic, + initialize_signature, + 0, + CorMethodImpl::miIL, + ) + .map_err(|e| { + log::warn!("error defining method __ElasticVoidMethodCall__. {:X}", e); + e + })?; + + let field_signature = &[ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_FIELD.bits(), + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + ]; + + let is_assembly_loaded_field_def = module_metadata + .emit + .define_field( + new_type_def, + "_isAssemblyLoaded", + CorFieldAttr::fdStatic | CorFieldAttr::fdPrivate, + field_signature, + CorElementType::ELEMENT_TYPE_END, + None, + 0, + ) + .map_err(|e| { + log::warn!("error defining field _isAssemblyLoaded. {:X}", e); + e + })?; + + let already_loaded_signature = &[ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits(), + 0, + CorElementType::ELEMENT_TYPE_BOOLEAN as COR_SIGNATURE, + ]; + + let already_loaded_method_token = module_metadata + .emit + .define_method( + new_type_def, + "IsAlreadyLoaded", + CorMethodAttr::mdStatic | CorMethodAttr::mdPrivate, + already_loaded_signature, + 0, + CorMethodImpl::miIL, + ) + .map_err(|e| { + log::warn!("error defining method IsAlreadyLoaded. {:X}", e); + e + })?; + + let interlocked_type_ref = module_metadata + .emit + .define_type_ref_by_name(corlib_ref, "System.Threading.Interlocked") + .map_err(|e| { + log::warn!( + "error defining type ref by name for System.Threading.Interlocked. {:X}", + e + ); + e + })?; + + // Create method signature for System.Threading.Interlocked::CompareExchange(int32&, int32, int32) + let interlocked_compare_exchange_signature = &[ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits(), + 3, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_BYREF as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + ]; + + let interlocked_compare_member_ref = module_metadata + .emit + .define_member_ref( + interlocked_type_ref, + "CompareExchange", + interlocked_compare_exchange_signature, + ) + .map_err(|e| { + log::warn!("error defining member ref CompareExchange. {:X}", e); + e + })?; + + // Write the instructions for the IsAlreadyLoaded method + let instructions = vec![ + Instruction::ldsflda(is_assembly_loaded_field_def), + Instruction::ldc_i4_1(), + Instruction::ldc_i4_0(), + Instruction::call(interlocked_compare_member_ref), + Instruction::ldc_i4_1(), + Instruction::ceq(), + Instruction::ret(), + ]; + + let method_bytes = Method::tiny(instructions) + .map_err(|e| { + log::warn!("failed to define IsAlreadyLoaded method"); + E_FAIL + })? + .into_bytes(); + + let allocator = profiler_info.get_il_function_body_allocator(module_id)?; + let allocated_bytes = allocator.alloc(method_bytes.len() as ULONG)?; + let address = unsafe { allocated_bytes.into_inner() }; + unsafe { + std::ptr::copy(method_bytes.as_ptr(), address, method_bytes.len()); + } + log::trace!("generate_void_il_startup_method: write IsAlreadyLoaded body"); + profiler_info + .set_il_function_body(module_id, already_loaded_method_token, address as *const _) + .map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to set il for IsAlreadyLoaded"); + e + })?; + + let get_assembly_bytes_signature = &[ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits(), + 4, + CorElementType::ELEMENT_TYPE_VOID as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_BYREF as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_BYREF as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_BYREF as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_BYREF as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + ]; + + let pinvoke_method_def = module_metadata.emit.define_method( + new_type_def, + "GetAssemblyAndSymbolsBytes", + CorMethodAttr::mdStatic | CorMethodAttr::mdPinvokeImpl | CorMethodAttr::mdHideBySig, + get_assembly_bytes_signature, + 0, + CorMethodImpl::empty()).map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to define method GetAssemblyAndSymbolsBytes"); + e + })?; + + module_metadata.emit.set_method_impl_flags(pinvoke_method_def, CorMethodImpl::miPreserveSig).map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to set method impl flags for GetAssemblyAndSymbolsBytes"); + e + })?; + + let native_profiler_file = env::get_native_profiler_file()?; + let profiler_ref = module_metadata + .emit + .define_module_ref(&native_profiler_file)?; + + module_metadata.emit.define_pinvoke_map( + pinvoke_method_def, + CorPinvokeMap::empty(), + "GetAssemblyAndSymbolsBytes", + profiler_ref).map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to define pinvoke map for GetAssemblyAndSymbolsBytes"); + e + })?; + + let byte_type_ref = module_metadata.emit.define_type_ref_by_name(corlib_ref, "System.Byte").map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to define type ref by name for System.Byte"); + e + })?; + let marshal_type_ref = module_metadata.emit.define_type_ref_by_name(corlib_ref, "System.Runtime.InteropServices.Marshal").map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to define type ref by name for System.Runtime.InteropServices.Marshal"); + e + })?; + + let marshal_copy_signature = &[ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits(), + 4, + CorElementType::ELEMENT_TYPE_VOID as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_SZARRAY as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_U1 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + ]; + + let marshal_copy_member_ref = module_metadata + .emit + .define_member_ref(marshal_type_ref, "Copy", marshal_copy_signature) + .map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to define member ref for Copy"); + e + })?; + + let system_reflection_assembly_type_ref = module_metadata.emit.define_type_ref_by_name(corlib_ref, "System.Reflection.Assembly").map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to define type ref by name for System.Reflection.Assembly"); + e + })?; + + let mut assembly_load_signature: Vec = vec![ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT.bits(), + 2, + CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE, + ]; + assembly_load_signature + .append(&mut compress_token(system_reflection_assembly_type_ref).unwrap()); + assembly_load_signature.push(CorElementType::ELEMENT_TYPE_SZARRAY as COR_SIGNATURE); + assembly_load_signature.push(CorElementType::ELEMENT_TYPE_U1 as COR_SIGNATURE); + assembly_load_signature.push(CorElementType::ELEMENT_TYPE_SZARRAY as COR_SIGNATURE); + assembly_load_signature.push(CorElementType::ELEMENT_TYPE_U1 as COR_SIGNATURE); + + let assembly_load_member_ref = module_metadata + .emit + .define_member_ref( + system_reflection_assembly_type_ref, + "Load", + &assembly_load_signature, + ) + .map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to define member ref Load"); + e + })?; + + let assembly_create_instance_signature = &[ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_HASTHIS.bits(), + 1, + CorElementType::ELEMENT_TYPE_OBJECT as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_STRING as COR_SIGNATURE, + ]; + + let assembly_create_instance_member_ref = module_metadata + .emit + .define_member_ref( + system_reflection_assembly_type_ref, + "CreateInstance", + assembly_create_instance_signature, + ) + .map_err(|e| { + log::warn!( + "generate_void_il_startup_method: failed to define member ref CreateInstance" + ); + e + })?; + + let load_helper_token = module_metadata + .emit + .define_user_string(managed::MANAGED_PROFILER_ASSEMBLY_LOADER_STARTUP) + .map_err(|e| { + log::warn!( + "generate_void_il_startup_method: failed to define user string {}", + managed::MANAGED_PROFILER_ASSEMBLY_LOADER_STARTUP + ); + e + })?; + + let mut locals_signature = vec![ + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_LOCAL_SIG.bits(), + 7, + CorElementType::ELEMENT_TYPE_I as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_I4 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_SZARRAY as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_U1 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_SZARRAY as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_U1 as COR_SIGNATURE, + CorElementType::ELEMENT_TYPE_CLASS as COR_SIGNATURE, + ]; + locals_signature.append(&mut compress_token(system_reflection_assembly_type_ref).unwrap()); + + let locals_signature_token = module_metadata.emit.get_token_from_sig(&locals_signature)?; + + let instructions = vec![ + // Step 0) Check if the assembly was already loaded + Instruction::call(already_loaded_method_token), + // val is the offset of the instruction to go to when false + Instruction::brfalse_s(Instruction::ret().len() as i8), + Instruction::ret(), + // Step 1) Call void GetAssemblyAndSymbolsBytes(out IntPtr assemblyPtr, + // out int assemblySize, out IntPtr symbolsPtr, out int symbolsSize) + Instruction::ldloca_s(0), + Instruction::ldloca_s(1), + Instruction::ldloca_s(2), + Instruction::ldloca_s(3), + Instruction::call(pinvoke_method_def), + // Step 2) Call void Marshal.Copy(IntPtr source, byte[] destination, + // int startIndex, int length) to populate the managed assembly bytes + Instruction::ldloc_1(), + Instruction::newarr(byte_type_ref), + Instruction::stloc_s(4), + Instruction::ldloc_0(), + Instruction::ldloc_s(4), + Instruction::ldc_i4_0(), + Instruction::ldloc_1(), + Instruction::call(marshal_copy_member_ref), + // Step 3) Call void Marshal.Copy(IntPtr source, byte[] destination, + // int startIndex, int length) to populate the symbols bytes + Instruction::ldloc_3(), + Instruction::newarr(byte_type_ref), + Instruction::stloc_s(5), + Instruction::ldloc_2(), + Instruction::ldloc_s(5), + Instruction::ldc_i4_0(), + Instruction::ldloc_3(), + Instruction::call(marshal_copy_member_ref), + // Step 4) Call System.Reflection.Assembly System.Reflection.Assembly.Load(byte[], byte[])) + Instruction::ldloc_s(4), + Instruction::ldloc_s(5), + Instruction::call(assembly_load_member_ref), + Instruction::stloc_s(6), + // Step 5) Call instance method Assembly.CreateInstance("Elastic.Apm.Profiler.Managed.Loader.Startup") + Instruction::ldloc_s(6), + Instruction::ldstr(load_helper_token), + Instruction::callvirt(assembly_create_instance_member_ref), + Instruction::pop(), + Instruction::ret(), + ]; + + let method = Method { + address: 0, + header: MethodHeader::fat( + false, + false, + instructions.iter().map(|i| i.opcode.len as u16).sum(), + instructions.iter().map(|i| i.len() as u32).sum(), + locals_signature_token, + ), + instructions, + sections: vec![], + }; + + let method_bytes = method.into_bytes(); + let allocated_bytes = allocator.alloc(method_bytes.len() as ULONG).map_err(|e| { + log::warn!("generate_void_il_startup_method: failed to allocate memory for __ElasticVoidMethodCall__"); + e + })?; + + let address = unsafe { allocated_bytes.into_inner() }; + unsafe { + std::ptr::copy(method_bytes.as_ptr(), address, method_bytes.len()); + } + log::trace!("generate_void_il_startup_method: write __ElasticVoidMethodCall__ body"); + profiler_info + .set_il_function_body(module_id, new_method, address as *const _) + .map_err(|e| { + log::warn!( + "generate_void_il_startup_method: failed to set il for __ElasticVoidMethodCall__" + ); + e + })?; + + Ok(new_method) +} diff --git a/src/elastic_apm_profiler/src/profiler/types.rs b/src/elastic_apm_profiler/src/profiler/types.rs new file mode 100644 index 000000000..e2a549b59 --- /dev/null +++ b/src/elastic_apm_profiler/src/profiler/types.rs @@ -0,0 +1,1592 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +use crate::{ + cil::uncompress_token, + error::Error, + ffi::{ + mdAssembly, mdAssemblyRef, mdMemberRef, mdMemberRefNil, mdModule, mdToken, mdTokenNil, + mdTypeRef, mdTypeSpec, AppDomainID, AssemblyID, CorAssemblyFlags, CorCallingConvention, + CorElementType, CorTokenType, ModuleID, ASSEMBLYMETADATA, BYTE, CLDB_E_RECORD_NOTFOUND, + COR_PRF_MODULE_FLAGS, COR_SIGNATURE, E_FAIL, ULONG, WCHAR, + }, + interfaces::{ + IMetaDataAssemblyEmit, IMetaDataAssemblyImport, IMetaDataEmit2, IMetaDataImport2, + }, + profiler::sig::parse_number, +}; +use com::sys::{GUID, HRESULT}; +use core::fmt; +use crypto::{digest::Digest, sha1::Sha1}; +use num_traits::FromPrimitive; +use serde::{ + de, + de::{DeserializeOwned, Visitor}, + Deserialize, Deserializer, +}; +use std::{ + cmp::Ordering, + collections::{BTreeMap, HashMap, HashSet}, + fmt::{Display, Formatter}, + iter::repeat, + marker::PhantomData, + str::FromStr, +}; +use widestring::U16CString; + +pub(crate) struct ModuleInfo { + pub id: ModuleID, + pub path: String, + pub assembly: AssemblyInfo, + pub flags: COR_PRF_MODULE_FLAGS, +} +impl ModuleInfo { + pub fn is_windows_runtime(&self) -> bool { + self.flags + .contains(COR_PRF_MODULE_FLAGS::COR_PRF_MODULE_WINDOWS_RUNTIME) + } +} + +pub(crate) struct AssemblyInfo { + pub id: AssemblyID, + pub name: String, + pub manifest_module_id: ModuleID, + pub app_domain_id: AppDomainID, + pub app_domain_name: String, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct MethodSignature { + data: Vec, +} + +impl From<&[u8]> for MethodSignature { + fn from(data: &[u8]) -> Self { + MethodSignature::new(data.to_vec()) + } +} + +impl MethodSignature { + pub fn new(data: Vec) -> Self { + Self { data } + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn bytes(&self) -> &[u8] { + self.data.as_slice() + } + + pub fn calling_convention(&self) -> CorCallingConvention { + if self.data.is_empty() { + CorCallingConvention::IMAGE_CEE_CS_CALLCONV_DEFAULT + } else { + // assume the unwrap is safe here + CorCallingConvention::from_bits(self.data[0]).unwrap() + } + } + + pub fn type_arguments_len(&self) -> u8 { + if self.data.len() > 1 && self.calling_convention().is_generic() { + self.data[1] + } else { + 0 + } + } + + pub fn arguments_len(&self) -> u8 { + if self.data.len() > 2 && self.calling_convention().is_generic() { + self.data[2] + } else if self.data.len() > 1 { + self.data[1] + } else { + 0 + } + } + + pub fn return_type_is_object(&self) -> bool { + if self.data.len() > 2 && self.calling_convention().is_generic() { + CorElementType::from_u8(self.data[3]) == Some(CorElementType::ELEMENT_TYPE_OBJECT) + } else if self.data.len() > 1 { + CorElementType::from_u8(self.data[2]) == Some(CorElementType::ELEMENT_TYPE_OBJECT) + } else { + false + } + } + + pub fn index_of_return_type(&self) -> usize { + if self.data.len() > 2 && self.calling_convention().is_generic() { + 3 + } else if self.data.len() > 1 { + 2 + } else { + 0 + } + } + + pub fn is_instance_method(&self) -> bool { + self.calling_convention() + .contains(CorCallingConvention::IMAGE_CEE_CS_CALLCONV_HASTHIS) + } +} + +struct MethodSignatureVisitor; +impl<'de> Visitor<'de> for MethodSignatureVisitor { + type Value = MethodSignature; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + let parse_bytes: Result, _> = v.split(' ').map(hex::decode).collect(); + match parse_bytes { + Ok(b) => Ok(MethodSignature::new(b.into_iter().flatten().collect())), + Err(e) => Err(de::Error::custom(format!( + "Could not parse MethodSignature: {:?}", + e.to_string() + ))), + } + } +} + +impl<'de> Deserialize<'de> for MethodSignature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(MethodSignatureVisitor) + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct AssemblyReference { + pub name: String, + pub version: Version, + pub locale: String, + pub public_key: PublicKeyToken, +} + +impl AssemblyReference { + pub fn new>( + name: S, + version: Version, + locale: S, + public_key: PublicKeyToken, + ) -> Self { + Self { + name: name.into(), + version, + locale: locale.into(), + public_key, + } + } +} + +impl Display for AssemblyReference { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{}, Version={}, Culture={}, PublicKeyToken={}", + &self.name, &self.version, &self.locale, &self.public_key.0 + ) + } +} + +impl FromStr for AssemblyReference { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut parts: Vec<&str> = s.split(',').map(|p| p.trim()).collect(); + + if parts.len() != 4 { + return Err(Error::InvalidAssemblyReference); + } + + let name = parts.remove(0).to_string(); + let map: BTreeMap<&str, &str> = parts + .iter() + .map(|p| { + let pp: Vec<&str> = p.split('=').map(|pp| pp.trim()).collect(); + (pp[0], pp[1]) + }) + .collect(); + + let version = Version::from_str(map["Version"])?; + let locale = map["Culture"].to_string(); + let public_key = PublicKeyToken(map["PublicKeyToken"].to_string()); + Ok(AssemblyReference { + name, + version, + locale, + public_key, + }) + } +} + +impl<'de> Deserialize<'de> for AssemblyReference { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} + +/// deserializes any type that implements FromStr from a str +pub(crate) fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + FromStr, + D: Deserializer<'de>, +{ + // This is a Visitor that forwards string types to T's `FromStr` impl and + // forwards map types to T's `Deserialize` impl. The `PhantomData` is to + // keep the compiler from complaining about T being an unused generic type + // parameter. We need T in order to know the Value type for the Visitor + // impl. + struct String(PhantomData T>); + + impl<'de, T> Visitor<'de> for String + where + T: Deserialize<'de> + FromStr, + { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + FromStr::from_str(value).map_err(|e| E::custom(format!("{:?}", e))) + } + } + + deserializer.deserialize_str(String(PhantomData)) +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct PublicKeyToken(String); + +impl PublicKeyToken { + pub fn new>(str: S) -> Self { + Self(str.into()) + } + + pub fn into_bytes(&self) -> Vec { + hex::decode(&self.0).unwrap() + } +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Clone)] +pub struct CallerMethodReference { + pub(crate) assembly: String, + #[serde(rename = "type")] + pub(crate) type_name: String, + #[serde(rename = "method")] + pub(crate) method_name: String, +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Clone)] +pub struct WrapperMethodReference { + pub(crate) assembly: AssemblyReference, + #[serde(rename = "type")] + pub(crate) type_name: String, + #[serde(rename = "method")] + pub(crate) method_name: Option, + pub(crate) action: String, + #[serde(rename = "signature")] + pub(crate) method_signature: Option, +} + +impl WrapperMethodReference { + pub fn get_type_cache_key(&self) -> String { + format!("[{}]{}", &self.assembly.name, &self.type_name,) + } + + pub fn get_method_cache_key(&self) -> String { + format!( + "[{}]{}.{}", + &self.assembly.name, + &self.type_name, + self.method_name.as_ref().map_or("", |m| m.as_str()), + ) + } + + pub fn full_name(&self) -> String { + format!( + "{}.{}", + &self.type_name, + self.method_name.as_ref().map_or("", |m| m.as_str()) + ) + } +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Clone)] +pub struct TargetMethodReference { + assembly: String, + #[serde(rename = "type")] + type_name: String, + #[serde(rename = "method")] + method_name: String, + #[serde(default = "version_max", deserialize_with = "deserialize_max_version")] + maximum_version: Version, + #[serde(default = "version_min")] + minimum_version: Version, + signature_types: Option>, +} + +/// deserializes a [Version], defaulting any missing values to [u16::MAX] +fn deserialize_max_version<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct VersionVisitor; + impl<'de> Visitor<'de> for VersionVisitor { + type Value = Version; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Self::Value::parse(value, u16::MAX).map_err(|e| E::custom(format!("{:?}", e))) + } + } + + deserializer.deserialize_str(VersionVisitor) +} + +fn version_max() -> Version { + Version::MAX +} +fn version_min() -> Version { + Version::MIN +} + +impl TargetMethodReference { + pub fn assembly(&self) -> &str { + &self.assembly + } + + pub fn method_name(&self) -> &str { + &self.method_name + } + + pub fn type_name(&self) -> &str { + &self.type_name + } + + pub fn signature_types(&self) -> Option<&[String]> { + self.signature_types.as_ref().map(|s| s.as_slice()) + } + + pub fn is_valid_for_assembly(&self, assembly_name: &str, version: &Version) -> bool { + if &self.assembly != assembly_name { + return false; + } + + if &self.minimum_version > version { + return false; + } + + if &self.maximum_version < version { + return false; + } + + true + } +} + +/// The method replacement +#[derive(Debug, Eq, PartialEq, Deserialize, Clone)] +pub struct MethodReplacement { + /// The caller + #[serde(default)] + #[serde(deserialize_with = "empty_struct_is_none")] + caller: Option, + /// The target for instrumentation + target: Option, + /// The wrapper providing the instrumentation + wrapper: Option, +} + +impl MethodReplacement { + pub fn caller(&self) -> Option<&CallerMethodReference> { + self.caller.as_ref() + } + + pub fn target(&self) -> Option<&TargetMethodReference> { + self.target.as_ref() + } + + pub fn wrapper(&self) -> Option<&WrapperMethodReference> { + self.wrapper.as_ref() + } +} + +/// Deserializes a T to Option::Some(T) and an empty struct to Option::None +fn empty_struct_is_none<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: DeserializeOwned, + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum EmptyOption { + Data(T), + Empty {}, + } + + match EmptyOption::deserialize(deserializer)? { + EmptyOption::Data(data) => Ok(Some(data)), + EmptyOption::Empty {} => Ok(None), + } +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Clone)] +pub struct IntegrationMethod { + pub(crate) name: String, + pub(crate) method_replacement: MethodReplacement, +} + +#[derive(Debug, Eq, PartialEq, Deserialize)] +pub struct Integration { + pub(crate) name: String, + pub(crate) method_replacements: Vec, +} + +#[derive(Debug, Clone)] +pub struct ModuleWrapperTokens { + failed_wrapper_keys: HashSet, + wrapper_refs: HashMap, + wrapper_parent_type: HashMap, +} + +impl ModuleWrapperTokens { + pub fn new() -> Self { + Self { + failed_wrapper_keys: HashSet::new(), + wrapper_refs: HashMap::new(), + wrapper_parent_type: HashMap::new(), + } + } + + pub fn is_failed_wrapper_member_key(&self, key: &str) -> bool { + self.failed_wrapper_keys.contains(key) + } + + pub fn contains_wrapper_member_ref(&self, key: &str) -> bool { + self.wrapper_refs.contains_key(key) + } + + pub fn get_wrapper_member_ref(&self, key: &str) -> Option { + self.wrapper_refs.get(key).copied() + } + + pub fn get_wrapper_parent_type_ref(&self, key: &str) -> Option { + self.wrapper_parent_type.get(key).copied() + } + + pub fn set_wrapper_parent_type_ref>(&mut self, key: S, type_ref: mdTypeRef) { + self.wrapper_parent_type.insert(key.into(), type_ref); + } + + pub fn set_failed_wrapper_member_key>(&mut self, key: S) { + self.failed_wrapper_keys.insert(key.into()); + } + + pub fn set_wrapper_member_ref>(&mut self, key: S, member_ref: mdMemberRef) { + self.wrapper_refs.insert(key.into(), member_ref); + } +} + +#[derive(Debug, Clone)] +pub struct ModuleMetadata { + pub import: IMetaDataImport2, + pub emit: IMetaDataEmit2, + pub assembly_import: IMetaDataAssemblyImport, + pub assembly_emit: IMetaDataAssemblyEmit, + pub assembly_name: String, + pub app_domain_id: AppDomainID, + pub module_version_id: GUID, + pub integrations: Vec, + pub(crate) cor_assembly_property: AssemblyMetaData, +} + +impl ModuleMetadata { + pub fn new( + import: IMetaDataImport2, + emit: IMetaDataEmit2, + assembly_import: IMetaDataAssemblyImport, + assembly_emit: IMetaDataAssemblyEmit, + assembly_name: String, + app_domain_id: AppDomainID, + module_version_id: GUID, + integrations: Vec, + cor_assembly_property: AssemblyMetaData, + ) -> Self { + Self { + import, + emit, + assembly_import, + assembly_emit, + assembly_name, + app_domain_id, + module_version_id, + integrations, + cor_assembly_property, + } + } + + pub fn get_method_replacements_for_caller( + &self, + caller: &FunctionInfo, + ) -> Vec { + self.integrations + .iter() + .filter_map(|i| { + if let Some(caller_ref) = &i.method_replacement.caller { + if caller_ref.type_name.is_empty() + || caller + .type_info + .as_ref() + .map_or(false, |t| t.name == caller_ref.type_name) + && caller_ref.method_name.is_empty() + || caller.name == caller_ref.method_name + { + Some(&i.method_replacement) + } else { + None + } + } else { + Some(&i.method_replacement) + } + }) + .cloned() + .collect() + } +} + +pub struct MetadataBuilder<'a> { + module_metadata: &'a ModuleMetadata, + module_wrapper_tokens: &'a mut ModuleWrapperTokens, + module: mdModule, +} + +impl<'a> MetadataBuilder<'a> { + pub fn new( + module_metadata: &'a ModuleMetadata, + module_wrapper_tokens: &'a mut ModuleWrapperTokens, + module: mdModule, + ) -> Self { + Self { + module_metadata, + module_wrapper_tokens, + module, + } + } + + pub fn emit_assembly_ref(&self, assembly_reference: &AssemblyReference) -> Result<(), HRESULT> { + let (sz_locale, cb_locale) = if &assembly_reference.locale == "neutral" { + (std::ptr::null_mut() as *mut WCHAR, 0) + } else { + let wstr = U16CString::from_str(&assembly_reference.locale).unwrap(); + let len = wstr.len() as ULONG; + (wstr.into_vec().as_mut_ptr(), len) + }; + + let assembly_metadata = ASSEMBLYMETADATA { + usMajorVersion: assembly_reference.version.major, + usMinorVersion: assembly_reference.version.minor, + usBuildNumber: assembly_reference.version.build, + usRevisionNumber: assembly_reference.version.revision, + szLocale: sz_locale, + cbLocale: cb_locale, + rProcessor: std::ptr::null_mut(), + ulProcessor: 0, + rOS: std::ptr::null_mut(), + ulOS: 0, + }; + + let public_key_bytes = assembly_reference.public_key.into_bytes(); + let assembly_ref = self + .module_metadata + .assembly_emit + .define_assembly_ref( + &public_key_bytes, + &assembly_reference.name, + &assembly_metadata, + &[], + CorAssemblyFlags::empty(), + ) + .map_err(|e| { + log::warn!( + "DefineAssemblyRef failed for assembly {} on module={} version_id={:?}", + &assembly_reference.name, + &self.module_metadata.assembly_name, + &self.module_metadata.module_version_id + ); + e + })?; + + Ok(()) + } + + pub fn find_wrapper_type_ref( + &mut self, + wrapper: &WrapperMethodReference, + ) -> Result { + let cache_key = wrapper.get_type_cache_key(); + if let Some(type_ref) = self + .module_wrapper_tokens + .get_wrapper_parent_type_ref(&cache_key) + { + return Ok(type_ref); + } + + // check if the type is defined in this module's assembly + let type_ref = if self.module_metadata.assembly_name == wrapper.assembly.name { + self.module_metadata + .emit + .define_type_ref_by_name(self.module, &wrapper.type_name) + } else { + match self + .module_metadata + .assembly_import + .find_assembly_ref(&wrapper.assembly.name) + { + Some(assembly_ref) => { + match self + .module_metadata + .import + .find_type_ref(assembly_ref, &wrapper.type_name) + { + Ok(t) => Ok(t), + Err(e) => { + if e == CLDB_E_RECORD_NOTFOUND { + self.module_metadata + .emit + .define_type_ref_by_name(assembly_ref, &wrapper.type_name) + } else { + log::warn!("error defining type ref for {}", &wrapper.type_name); + Err(e) + } + } + } + } + None => { + log::warn!( + "Assembly reference not found for {}", + &wrapper.assembly.name + ); + return Err(E_FAIL); + } + } + }?; + + self.module_wrapper_tokens + .set_wrapper_parent_type_ref(cache_key, type_ref); + Ok(type_ref) + } + + pub fn store_wrapper_method_ref( + &mut self, + wrapper: &WrapperMethodReference, + ) -> Result<(), HRESULT> { + let cache_key = wrapper.get_method_cache_key(); + if self + .module_wrapper_tokens + .contains_wrapper_member_ref(&cache_key) + { + return Ok(()); + } + + let type_ref = self.find_wrapper_type_ref(wrapper).map_err(|e| { + log::warn!("failed finding wrapper method ref {}", e); + self.module_wrapper_tokens + .set_failed_wrapper_member_key(&cache_key); + e + })?; + + let mut member_ref = mdMemberRefNil; + if let Some(signature) = &wrapper.method_signature { + if signature.len() > 0 { + if let Some(method_name) = &wrapper.method_name { + match self.module_metadata.import.find_member_ref( + type_ref, + method_name, + &signature.data, + ) { + Ok(m) => member_ref = m, + Err(e) => { + if e == CLDB_E_RECORD_NOTFOUND { + match self.module_metadata.emit.define_member_ref( + type_ref, + method_name, + &signature.data, + ) { + Ok(m) => member_ref = m, + Err(e) => { + self.module_wrapper_tokens + .set_failed_wrapper_member_key(&cache_key); + return Err(e); + } + } + } else { + self.module_wrapper_tokens + .set_failed_wrapper_member_key(&cache_key); + return Err(e); + } + } + } + } + } + } + + self.module_wrapper_tokens + .set_wrapper_member_ref(&cache_key, member_ref); + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct FunctionInfo { + pub id: mdToken, + pub name: String, + pub type_info: Option, + pub is_generic: bool, + pub signature: MethodSignature, + pub function_spec_signature: Option, + pub method_def_id: mdToken, + pub method_signature: FunctionMethodSignature, +} + +impl FunctionInfo { + pub fn new( + id: mdToken, + name: String, + is_generic: bool, + type_info: Option, + signature: MethodSignature, + function_spec_signature: Option, + method_def_id: mdToken, + method_signature: FunctionMethodSignature, + ) -> Self { + Self { + id, + name, + type_info, + is_generic, + signature, + function_spec_signature, + method_def_id, + method_signature, + } + } + + /// Full type and function name + pub fn full_name(&self) -> String { + match &self.type_info { + Some(t) => format!("{}.{}", &t.name, &self.name), + None => format!(".{}", &self.name), + } + } +} + +#[derive(Debug, Clone)] +pub struct FunctionMethodSignature { + pub data: Vec, +} + +impl FunctionMethodSignature { + pub fn new(data: Vec) -> Self { + Self { data } + } + + fn parse_type_def_or_ref_encoded(signature: &[u8]) -> Option<(usize, ULONG, ULONG)> { + if let Some((number, num_idx)) = parse_number(signature) { + let index_type = number & 0x03; + let index = number >> 2; + Some((num_idx, index_type, index)) + } else { + None + } + } + + fn parse_type(signature: &[u8]) -> Option<(usize, usize)> { + let start_idx = 0; + let mut idx = start_idx; + if let Some(elem_type) = CorElementType::from_u8(signature[idx]) { + idx += 1; + match elem_type { + CorElementType::ELEMENT_TYPE_VOID + | CorElementType::ELEMENT_TYPE_BOOLEAN + | CorElementType::ELEMENT_TYPE_CHAR + | CorElementType::ELEMENT_TYPE_I1 + | CorElementType::ELEMENT_TYPE_U1 + | CorElementType::ELEMENT_TYPE_I2 + | CorElementType::ELEMENT_TYPE_U2 + | CorElementType::ELEMENT_TYPE_I4 + | CorElementType::ELEMENT_TYPE_U4 + | CorElementType::ELEMENT_TYPE_I8 + | CorElementType::ELEMENT_TYPE_U8 + | CorElementType::ELEMENT_TYPE_R4 + | CorElementType::ELEMENT_TYPE_R8 + | CorElementType::ELEMENT_TYPE_STRING + | CorElementType::ELEMENT_TYPE_OBJECT => Some((start_idx, idx)), + CorElementType::ELEMENT_TYPE_PTR => None, + CorElementType::ELEMENT_TYPE_CLASS | CorElementType::ELEMENT_TYPE_VALUETYPE => { + Self::parse_type_def_or_ref_encoded(&signature[idx..]) + .map(|(type_idx, _, _)| (start_idx, idx + type_idx)) + } + CorElementType::ELEMENT_TYPE_SZARRAY => { + if signature.len() == 1 { + return None; + } + + if signature[idx] == CorElementType::ELEMENT_TYPE_CMOD_OPT as u8 + || signature[idx] == CorElementType::ELEMENT_TYPE_CMOD_REQD as u8 + { + None + } else { + Self::parse_type(&signature[idx..]).map(|(_, e)| (start_idx, idx + e)) + } + } + CorElementType::ELEMENT_TYPE_GENERICINST => { + if signature.len() == 1 { + return None; + } + + if signature[idx] != CorElementType::ELEMENT_TYPE_CLASS as u8 + && signature[idx] != CorElementType::ELEMENT_TYPE_VALUETYPE as u8 + { + return None; + } + + idx += 1; + + if let Some((type_idx, _, _)) = + Self::parse_type_def_or_ref_encoded(&signature[idx..]) + { + idx += type_idx; + } else { + return None; + } + + let num; + if let Some((number, num_idx)) = + crate::profiler::sig::parse_number(&signature[idx..]) + { + idx += num_idx; + num = number; + } else { + return None; + } + + for _ in 0..num { + if let Some((_, end_idx)) = Self::parse_type(&signature[idx..]) { + idx += end_idx; + } else { + return None; + } + } + + Some((start_idx, idx)) + } + CorElementType::ELEMENT_TYPE_VAR | CorElementType::ELEMENT_TYPE_MVAR => { + parse_number(&signature[idx..]).map(|(_, num_idx)| (start_idx, idx + num_idx)) + } + _ => None, + } + } else { + None + } + } + + fn parse_return_type(signature: &[u8]) -> Option<(usize, usize)> { + let start_idx = 0; + let mut idx = start_idx; + if let Some(elem_type) = CorElementType::from_u8(signature[idx]) { + if elem_type == CorElementType::ELEMENT_TYPE_CMOD_OPT + || elem_type == CorElementType::ELEMENT_TYPE_CMOD_REQD + { + return None; + } + + if elem_type == CorElementType::ELEMENT_TYPE_TYPEDBYREF { + return None; + } + + if elem_type == CorElementType::ELEMENT_TYPE_VOID { + idx += 1; + return Some((start_idx, idx)); + } + + if elem_type == CorElementType::ELEMENT_TYPE_BYREF { + idx += 1; + } + + Self::parse_type(&signature[idx..]).map(|(_, end_idx)| (start_idx, idx + end_idx)) + } else { + None + } + } + + pub(crate) fn calling_convention(&self) -> Option { + if self.data.is_empty() { + None + } else { + CorCallingConvention::from_bits(self.data[0]) + } + } + + fn parse_param(signature: &[u8]) -> Option<(usize, usize)> { + let mut idx = 0; + if signature[idx] == CorElementType::ELEMENT_TYPE_CMOD_OPT as u8 + || signature[idx] == CorElementType::ELEMENT_TYPE_CMOD_REQD as u8 + { + return None; + } + + if signature[idx] == CorElementType::ELEMENT_TYPE_TYPEDBYREF as u8 { + return None; + } + + if signature[idx] == CorElementType::ELEMENT_TYPE_BYREF as u8 { + idx += 1; + } + + Self::parse_type(&signature[idx..]).map(|(s, e)| (idx, idx + e)) + } + + pub fn try_parse(&self) -> Option { + if let Some(calling_convention) = self.calling_convention() { + let mut idx = 1; + + // generic type parameters length + let type_arg_len = if calling_convention.is_generic() { + if let Some((number, num_idx)) = parse_number(&self.data[idx..]) { + idx += num_idx; + number as u8 + } else { + return None; + } + } else { + 0 + }; + + // parameters length + let arg_len = if let Some((number, num_idx)) = parse_number(&self.data[idx..]) { + idx += num_idx; + number as u8 + } else { + return None; + }; + + let ret_type = match Self::parse_return_type(&self.data[idx..]) { + Some((_, end_idx)) => { + let ret_type = (idx, idx + end_idx); + idx += end_idx; + ret_type + } + None => return None, + }; + + let mut sentinel_found = false; + let mut params = vec![]; + for _ in 0..arg_len { + if self.data[idx] == CorElementType::ELEMENT_TYPE_SENTINEL as u8 { + if sentinel_found { + return None; + } + + sentinel_found = true; + idx += 1; + } + + if let Some(param) = Self::parse_param(&self.data[idx..]) { + params.push((idx, idx + param.1)); + idx += param.1; + } else { + return None; + } + } + + Some(ParsedFunctionMethodSignature { + data: self.data.clone(), + type_arg_len, + arg_len, + ret_type, + args: params, + }) + } else { + None + } + } +} + +pub struct FunctionMethodArgument<'a> { + data: &'a [u8], +} + +impl<'a> FunctionMethodArgument<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self { data } + } + + pub fn signature(&self) -> &[u8] { + self.data + } + + pub fn get_type_flags(&self) -> (CorElementType, MethodArgumentTypeFlag) { + let mut idx = 0; + if self.data[idx] == CorElementType::ELEMENT_TYPE_VOID as u8 { + return ( + CorElementType::ELEMENT_TYPE_VOID, + MethodArgumentTypeFlag::VOID, + ); + } + + let mut flags = MethodArgumentTypeFlag::empty(); + if self.data[idx] == CorElementType::ELEMENT_TYPE_BYREF as u8 { + flags |= MethodArgumentTypeFlag::BY_REF; + idx += 1; + } + + let element_type = CorElementType::from_u8(self.data[idx]).unwrap(); + match element_type { + CorElementType::ELEMENT_TYPE_BOOLEAN + | CorElementType::ELEMENT_TYPE_CHAR + | CorElementType::ELEMENT_TYPE_I1 + | CorElementType::ELEMENT_TYPE_U1 + | CorElementType::ELEMENT_TYPE_I2 + | CorElementType::ELEMENT_TYPE_U2 + | CorElementType::ELEMENT_TYPE_I4 + | CorElementType::ELEMENT_TYPE_U4 + | CorElementType::ELEMENT_TYPE_I8 + | CorElementType::ELEMENT_TYPE_U8 + | CorElementType::ELEMENT_TYPE_R4 + | CorElementType::ELEMENT_TYPE_R8 + | CorElementType::ELEMENT_TYPE_I + | CorElementType::ELEMENT_TYPE_U + | CorElementType::ELEMENT_TYPE_VALUETYPE + | CorElementType::ELEMENT_TYPE_VAR + | CorElementType::ELEMENT_TYPE_MVAR => { + flags |= MethodArgumentTypeFlag::BOXED_TYPE; + } + CorElementType::ELEMENT_TYPE_GENERICINST => { + idx += 1; + if self.data[idx] == CorElementType::ELEMENT_TYPE_VALUETYPE as u8 { + flags |= MethodArgumentTypeFlag::BOXED_TYPE; + } + } + _ => (), + } + + (element_type, flags) + } + + pub fn get_type_tok( + &self, + metadata_emit: &IMetaDataEmit2, + cor_lib_assembly_ref: mdAssemblyRef, + ) -> Result { + let mut idx = 0; + if self.data[idx] == CorElementType::ELEMENT_TYPE_BYREF as u8 { + idx += 1; + } + + let element_type = CorElementType::from_u8(self.data[idx]).unwrap(); + match element_type { + CorElementType::ELEMENT_TYPE_BOOLEAN => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Boolean") + } + CorElementType::ELEMENT_TYPE_CHAR => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Char") + } + CorElementType::ELEMENT_TYPE_I1 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.SByte") + } + CorElementType::ELEMENT_TYPE_U1 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Byte") + } + CorElementType::ELEMENT_TYPE_I2 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Int16") + } + CorElementType::ELEMENT_TYPE_U2 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.UInt16") + } + CorElementType::ELEMENT_TYPE_I4 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.In32") + } + CorElementType::ELEMENT_TYPE_U4 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.UInt32") + } + CorElementType::ELEMENT_TYPE_I8 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Int64") + } + CorElementType::ELEMENT_TYPE_U8 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.UInt64") + } + CorElementType::ELEMENT_TYPE_R4 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Single") + } + CorElementType::ELEMENT_TYPE_R8 => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Double") + } + CorElementType::ELEMENT_TYPE_I => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.IntPtr") + } + CorElementType::ELEMENT_TYPE_U => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.UIntPtr") + } + CorElementType::ELEMENT_TYPE_STRING => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.String") + } + CorElementType::ELEMENT_TYPE_OBJECT => { + metadata_emit.define_type_ref_by_name(cor_lib_assembly_ref, "System.Object") + } + CorElementType::ELEMENT_TYPE_CLASS | CorElementType::ELEMENT_TYPE_VALUETYPE => { + idx += 1; + let (token, len) = uncompress_token(&self.data[idx..]); + Ok(token) + } + CorElementType::ELEMENT_TYPE_GENERICINST + | CorElementType::ELEMENT_TYPE_SZARRAY + | CorElementType::ELEMENT_TYPE_MVAR + | CorElementType::ELEMENT_TYPE_VAR => { + metadata_emit.get_token_from_type_spec(&self.data[idx..]) + } + _ => Ok(mdTokenNil), + } + } +} + +#[derive(Debug)] +pub struct ParsedFunctionMethodSignature { + pub type_arg_len: u8, + pub arg_len: u8, + pub ret_type: (usize, usize), + pub args: Vec<(usize, usize)>, + pub data: Vec, +} + +impl ParsedFunctionMethodSignature { + pub fn arguments(&self) -> Vec { + self.args + .iter() + .map(|(s, e)| FunctionMethodArgument::new(&self.data[*s..*e])) + .collect() + } + + pub fn return_type(&self) -> FunctionMethodArgument { + FunctionMethodArgument::new(&self.data[self.ret_type.0..self.ret_type.1]) + } +} + +#[derive(Debug, Clone)] +pub struct TypeInfo { + pub id: mdToken, + pub name: String, + pub type_spec: mdTypeSpec, + pub token_type: CorTokenType, + pub extends_from: Option>, + pub is_value_type: bool, + pub is_generic: bool, + pub parent_type: Option>, +} + +/// A .NET version +#[derive(Clone, Eq, Debug)] +#[repr(C)] +pub struct Version { + pub major: u16, + pub minor: u16, + pub build: u16, + pub revision: u16, +} + +impl Version { + pub(crate) const MAX: Version = Version { + major: u16::MAX, + minor: u16::MAX, + build: u16::MAX, + revision: u16::MAX, + }; + + pub(crate) const MIN: Version = Version { + major: 0, + minor: 0, + build: 0, + revision: 0, + }; + + pub const fn new(major: u16, minor: u16, build: u16, revision: u16) -> Self { + Version { + major, + minor, + build, + revision, + } + } + + pub fn parse(version: &str, default_missing_value: u16) -> Result { + if version.is_empty() { + return Err(Error::InvalidVersion); + } + + let parts = version.split('.').collect::>(); + if parts.len() > 4 { + return Err(Error::InvalidVersion); + } + + let major = match parts[0] { + "*" => u16::MAX, + m => m.parse::().map_err(|_| Error::InvalidVersion)?, + }; + + let minor = if parts.len() > 1 { + match parts[1] { + "*" => u16::MAX, + m => m.parse::().map_err(|_| Error::InvalidVersion)?, + } + } else { + default_missing_value + }; + let build = if parts.len() > 2 { + match parts[2] { + "*" => u16::MAX, + m => m.parse::().map_err(|_| Error::InvalidVersion)?, + } + } else { + default_missing_value + }; + let revision = if parts.len() > 3 { + match parts[3] { + "*" => u16::MAX, + m => m.parse::().map_err(|_| Error::InvalidVersion)?, + } + } else { + default_missing_value + }; + + Ok(Version::new(major, minor, build, revision)) + } +} + +impl FromStr for Version { + type Err = Error; + fn from_str(version: &str) -> Result { + Self::parse(version, 0) + } +} + +impl PartialEq for Version { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.major == other.major + && self.minor == other.minor + && self.build == other.build + && self.revision == other.revision + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Version) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Version) -> Ordering { + match self.major.cmp(&other.major) { + Ordering::Equal => {} + r => return r, + } + + match self.minor.cmp(&other.minor) { + Ordering::Equal => {} + r => return r, + } + + match self.build.cmp(&other.build) { + Ordering::Equal => {} + r => return r, + } + + self.revision.cmp(&other.revision) + } +} + +impl Default for Version { + fn default() -> Self { + Version::new(0, 0, 0, 0) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{}.{}.{}.{}", + self.major, self.minor, self.build, self.revision + ) + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} + +/// Assembly public key +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct PublicKey { + bytes: Vec, + hash_algorithm: Option, +} + +impl PublicKey { + pub fn new(bytes: Vec, hash_algorithm: u32) -> Self { + Self { + bytes, + hash_algorithm: HashAlgorithmType::from_u32(hash_algorithm), + } + } + + /// the public key bytes + pub fn bytes(&self) -> &[u8] { + self.bytes.as_slice() + } + + /// the hash algorithm of the public key + pub fn hash_algorithm(&self) -> Option { + self.hash_algorithm + } + + /// the hex encoded public key + pub fn public_key(&self) -> String { + hex::encode(self.bytes()) + } + + /// the low 8 bytes of the SHA-1 hash of the originator’s public key in the assembly reference + pub fn public_key_token(&self) -> String { + if self.bytes.is_empty() { + return String::new(); + } + + match &self.hash_algorithm { + Some(HashAlgorithmType::Sha1) => { + let mut sha1 = Sha1::new(); + sha1.input(&self.bytes); + let mut buf: Vec = repeat(0).take((sha1.output_bits() + 7) / 8).collect(); + sha1.result(&mut buf); + buf.reverse(); + hex::encode(buf[0..8].as_ref()) + } + _ => String::new(), + } + } +} + +#[derive(Debug)] +pub struct WrapperMethodRef { + pub type_ref: mdTypeRef, + pub method_ref: mdMemberRef, +} + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct AssemblyMetaData { + pub name: String, + pub locale: String, + pub assembly_token: mdAssembly, + pub public_key: PublicKey, + pub version: Version, + pub assembly_flags: CorAssemblyFlags, +} + +#[derive(Debug, Eq, Copy, Clone, PartialEq, FromPrimitive)] +pub enum HashAlgorithmType { + Md5 = 32771, + None = 0, + Sha1 = 32772, + Sha256 = 32780, + Sha384 = 32781, + Sha512 = 32782, +} + +bitflags! { + pub struct MethodArgumentTypeFlag: u32 { + const BY_REF = 1; + const VOID = 2; + const BOXED_TYPE = 4; + } +} + +#[cfg(test)] +pub mod tests { + use crate::profiler::types::{ + AssemblyReference, Integration, MethodSignature, PublicKeyToken, Version, + }; + use std::{error::Error, fs::File, io::BufReader, path::PathBuf}; + + fn deserialize_and_assert(json: &str, expected: Version) -> Result<(), Box> { + let version: Version = serde_yaml::from_str(json)?; + assert_eq!(expected, version); + Ok(()) + } + + #[test] + fn deserialize_version_with_major() -> Result<(), Box> { + deserialize_and_assert("\"5\"", Version::new(5, 0, 0, 0)) + } + + #[test] + fn deserialize_version_with_major_minor() -> Result<(), Box> { + deserialize_and_assert("\"5.5\"", Version::new(5, 5, 0, 0)) + } + + #[test] + fn deserialize_version_with_major_minor_build() -> Result<(), Box> { + deserialize_and_assert("\"5.5.5\"", Version::new(5, 5, 5, 0)) + } + + #[test] + fn deserialize_version_with_major_minor_build_revision() -> Result<(), Box> { + deserialize_and_assert("\"5.5.5.5\"", Version::new(5, 5, 5, 5)) + } + + #[test] + fn deserialize_version_with_major_stars() -> Result<(), Box> { + deserialize_and_assert("\"5.*.*.*\"", Version::new(5, u16::MAX, u16::MAX, u16::MAX)) + } + + #[test] + fn deserialize_method_signature() -> Result<(), Box> { + let json = "\"00 08 1C 1C 1C 1C 1C 1C 08 08 0A\""; + let method_signature: MethodSignature = serde_yaml::from_str(json)?; + assert_eq!( + MethodSignature { + data: vec![0, 8, 28, 28, 28, 28, 28, 28, 8, 8, 10] + }, + method_signature + ); + Ok(()) + } + + #[test] + fn deserialize_assembly_reference() -> Result<(), Box> { + let json = + "\"Elastic.Apm, Version=1.9.0.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22\""; + let assembly_reference: AssemblyReference = serde_yaml::from_str(json)?; + + let expected_assembly_reference = AssemblyReference { + name: "Elastic.Apm".into(), + version: Version::new(1, 9, 0, 0), + locale: "neutral".into(), + public_key: PublicKeyToken("ae7400d2c189cf22".into()), + }; + + assert_eq!(expected_assembly_reference, assembly_reference); + Ok(()) + } + + #[test] + fn deserialize_integration_from_yml() -> Result<(), Box> { + let json = r#"--- +name: AdoNet +method_replacements: +- caller: {} + target: + assembly: System.Data + type: System.Data.Common.DbCommand + method: ExecuteNonQueryAsync + signature_types: + - System.Threading.Tasks.Task`1 + - System.Threading.CancellationToken + minimum_version: 4.0.0 + maximum_version: 4.*.* + wrapper: + assembly: Elastic.Apm.Profiler.Managed, Version=1.9.0.0, Culture=neutral, PublicKeyToken=ae7400d2c189cf22 + type: Elastic.Apm.Profiler.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration + action: CallTargetModification"#; + + let integration: Integration = serde_yaml::from_str(json)?; + + assert_eq!(&integration.name, "AdoNet"); + assert_eq!(integration.method_replacements.len(), 1); + + let method_replacement = &integration.method_replacements[0]; + + assert!(method_replacement.caller.is_none()); + + assert!(method_replacement.target.is_some()); + let target = method_replacement.target.as_ref().unwrap(); + assert_eq!(&target.assembly, "System.Data"); + assert_eq!(&target.type_name, "System.Data.Common.DbCommand"); + assert_eq!(&target.method_name, "ExecuteNonQueryAsync"); + assert_eq!( + target + .signature_types + .as_ref() + .unwrap() + .iter() + .map(String::as_str) + .collect::>(), + vec![ + "System.Threading.Tasks.Task`1", + "System.Threading.CancellationToken" + ] + ); + assert_eq!(target.minimum_version, Version::new(4, 0, 0, 0)); + assert_eq!( + target.maximum_version, + Version::new(4, u16::MAX, u16::MAX, u16::MAX) + ); + + assert!(method_replacement.wrapper.is_some()); + let wrapper = method_replacement.wrapper.as_ref().unwrap(); + assert_eq!( + &wrapper.type_name, + "Elastic.Apm.Profiler.Integrations.AdoNet.CommandExecuteNonQueryAsyncIntegration" + ); + assert_eq!(&wrapper.action, "CallTargetModification"); + + Ok(()) + } + + #[test] + fn deserialize_integrations_from_yml() -> Result<(), Box> { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("../Elastic.Apm.Profiler.Managed/integrations.yml"); + let file = File::open(path)?; + let reader = BufReader::new(file); + let integrations: Vec = serde_yaml::from_reader(reader)?; + assert!(integrations.len() > 0); + Ok(()) + } + + #[test] + fn public_key_token_into_bytes() { + let public_key_token = PublicKeyToken::new("ae7400d2c189cf22"); + let bytes = public_key_token.into_bytes(); + assert_eq!(vec![174, 116, 0, 210, 193, 137, 207, 34], bytes); + } +} diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj b/test/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj index c31b92426..e384ea652 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/Elastic.Apm.AspNetFullFramework.Tests.csproj @@ -4,15 +4,8 @@ Elastic.Apm.AspNetFullFramework.Tests Elastic.Apm.AspNetFullFramework.Tests - - - - - - - diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs index 53d041536..250a7ff7c 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/IisAdministration.cs @@ -267,16 +267,19 @@ private void EnsureAccessToSqliteInterop() var sqliteInteropDll = new FileInfo(Path.Combine(appBinPath, processorArch, "SQLite.Interop.dll")); if (sqliteInteropDll.Exists) { - // Getting file security details may require elevated privileges, depending on where the repository is cloned. - // Running the IDE (or cmd line) in Administrator mode should resolve - var accessControl = sqliteInteropDll.GetAccessControl(AccessControlSections.All); - var account = new NTAccount("IIS_IUSRS"); - accessControl.AddAccessRule(new FileSystemAccessRule( - account, - FileSystemRights.ReadAndExecute, - AccessControlType.Allow)); - - sqliteInteropDll.SetAccessControl(accessControl); + if (OperatingSystem.IsWindows()) + { + // Getting file security details may require elevated privileges, depending on where the repository is cloned. + // Running the IDE (or cmd line) in Administrator mode should resolve + var accessControl = sqliteInteropDll.GetAccessControl(AccessControlSections.All); + var account = new NTAccount("IIS_IUSRS"); + accessControl.AddAccessRule(new FileSystemAccessRule( + account, + FileSystemRights.ReadAndExecute, + AccessControlType.Allow)); + + sqliteInteropDll.SetAccessControl(accessControl); + } } } } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsEnabledDetector.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsEnabledDetector.cs index 7418c2d12..5cee93de4 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsEnabledDetector.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsEnabledDetector.cs @@ -32,7 +32,7 @@ internal static class TestsEnabledDetector private static string DetectReasonWhyTestsAreSkipped() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return $"{ReasonPrefix} OS is not Windows. {ReasonSuffix}"; + if (!TestEnvironment.IsWindows) return $"{ReasonPrefix} OS is not Windows. {ReasonSuffix}"; if (!CheckIisVersion(out var reason)) return $"{ReasonPrefix} {reason}. {ReasonSuffix}"; diff --git a/test/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs b/test/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs index 8e35558e7..b8b1e1d00 100644 --- a/test/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs +++ b/test/Elastic.Apm.Benchmarks/Helpers/GitInfo.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using Elastic.Apm.Tests.Utilities; namespace Elastic.Apm.Benchmarks.Helpers { @@ -22,7 +23,7 @@ public GitInfo() UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, - FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "git.exe" : "git", + FileName = TestEnvironment.IsWindows ? "git.exe" : "git", WorkingDirectory = Environment.CurrentDirectory }; diff --git a/test/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj b/test/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj index 7d30e7194..aebd7a350 100644 --- a/test/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj +++ b/test/Elastic.Apm.MongoDb.Tests/Elastic.Apm.MongoDb.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + netcoreapp3.1 diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/AdoNetTestData.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/AdoNetTestData.cs new file mode 100644 index 000000000..b00412068 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/AdoNetTestData.cs @@ -0,0 +1,43 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Elastic.Apm.Tests.Utilities; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + public class AdoNetTestData : IEnumerable + { + public const int DbRunnerExpectedTotalSpans = DbRunnerExpectedRunAllAsyncSpans + DbRunnerExpectedRunBaseTypesAsyncSpans; + public const int DbRunnerExpectedRunAllAsyncSpans = 111; + public const int DbRunnerExpectedRunBaseTypesAsyncSpans = 68; + + // frequent commands are executed to retrieve session parameters. we should ignore these + public const int OracleProviderExpectedSpans = 63; + public const string OracleProviderSpanNameStart = "DECLARE err_code VARCHAR2(2000); err_msg VARCHAR2(2000);"; + + public IEnumerator GetEnumerator() + { + // TODO: Add x64/x86 options. macOS and Linux do not support x86 + + yield return new object[] { "net5.0" }; + yield return new object[] { "netcoreapp3.1" }; + + // macOS only supports netcoreapp3.1 and up + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + yield return new object[] { "netcoreapp3.0" }; + yield return new object[] { "netcoreapp2.1" }; + } + + if (TestEnvironment.IsWindows) + yield return new object[] { "net461" }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlCommandTests.cs new file mode 100644 index 000000000..9cf0b8fdc --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlCommandTests.cs @@ -0,0 +1,84 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Docker; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [Collection("MySql")] + public class MySqlCommandTests + { + private readonly MySqlFixture _fixture; + private readonly ITestOutputHelper _output; + + public MySqlCommandTests(MySqlFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + [DockerTheory] + [ClassData(typeof(AdoNetTestData))] + public async Task CaptureAutoInstrumentedSpans(string targetFramework) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(CaptureAutoInstrumentedSpans)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + using (var profiledApplication = new ProfiledApplication("MySqlDataSample")) + { + IDictionary environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["MYSQL_CONNECTION_STRING"] = _fixture.ConnectionString, + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => _output.WriteLine(line.Line), + exception => _output.WriteLine($"{exception}")); + } + + // RunAllAsync transaction + // RunBaseTypesAsync transaction + apmServer.ReceivedData.Transactions.Should().HaveCount(2); + + // The first MySqlCommand on an opened MySqlConnection executes an additional + // command that the profiler instrumentation will create a span for. Since there + // are two connections opened, 1 for RunAllAsync and 1 for RunBaseTypesAsync, + // expect 2 additional spans + apmServer.ReceivedData.Spans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedTotalSpans + 2); + + var genericTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunAllAsync"); + genericTransaction.Should().NotBeNull(); + + var genericSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == genericTransaction.Id).ToList(); + genericSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunAllAsyncSpans + 1); + + var baseTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunBaseTypesAsync"); + baseTransaction.Should().NotBeNull(); + + var baseSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == baseTransaction.Id).ToList(); + baseSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunBaseTypesAsyncSpans + 1); + + await apmServer.StopAsync(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs new file mode 100644 index 000000000..5881bec3a --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlFixture.cs @@ -0,0 +1,49 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading.Tasks; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [CollectionDefinition("MySql")] + public class MySqlCollection : ICollectionFixture + { + } + + public class MySqlFixture : IAsyncLifetime + { + private readonly MySqlTestcontainer _container; + private const string MySqlPassword = "Password123"; + private const string MySqlDatabaseName = "db"; + private const string MySqlUsername = "mysql"; + + public MySqlFixture() + { + var builder = new TestcontainersBuilder() + .WithDatabase(new MySqlTestcontainerConfiguration + { + Database = MySqlDatabaseName, + Username = MySqlUsername, + Password = MySqlPassword + }); + + _container = builder.Build(); + } + + public async Task InitializeAsync() + { + await _container.StartAsync(); + ConnectionString = _container.ConnectionString; + } + + public async Task DisposeAsync() => await _container.DisposeAsync(); + + public string ConnectionString { get; private set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs new file mode 100644 index 000000000..2092c7edc --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs @@ -0,0 +1,77 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Docker; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [Collection("Postgres")] + public class NpgSqlCommandTests + { + private readonly PostgreSqlFixture _fixture; + private readonly ITestOutputHelper _output; + + public NpgSqlCommandTests(PostgreSqlFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + [DockerTheory] + [ClassData(typeof(AdoNetTestData))] + public async Task CaptureAutoInstrumentedSpans(string targetFramework) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(CaptureAutoInstrumentedSpans)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + using (var profiledApplication = new ProfiledApplication("NpgsqlSample")) + { + IDictionary environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["POSTGRES_CONNECTION_STRING"] = _fixture.ConnectionString, + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => _output.WriteLine(line.Line), + exception => _output.WriteLine($"{exception}")); + } + + apmServer.ReceivedData.Transactions.Should().HaveCount(2); + apmServer.ReceivedData.Spans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedTotalSpans); + + var genericTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunAllAsync"); + genericTransaction.Should().NotBeNull(); + + var genericSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == genericTransaction.Id).ToList(); + genericSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunAllAsyncSpans); + + var baseTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunBaseTypesAsync"); + baseTransaction.Should().NotBeNull(); + + var baseSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == baseTransaction.Id).ToList(); + baseSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunBaseTypesAsyncSpans); + + await apmServer.StopAsync(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCommandTests.cs new file mode 100644 index 000000000..ba9afc8b3 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCommandTests.cs @@ -0,0 +1,86 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Docker; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [Collection("Oracle")] + public class OracleManagedDataAccessCommandTests + { + private readonly OracleSqlFixture _fixture; + private readonly ITestOutputHelper _output; + + public OracleManagedDataAccessCommandTests(OracleSqlFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + [DockerTheory] + [InlineData("net461")] + public async Task CaptureAutoInstrumentedSpans(string targetFramework) + { + if (!TestEnvironment.IsWindows) + return; + + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(CaptureAutoInstrumentedSpans)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + using (var profiledApplication = new ProfiledApplication("OracleManagedDataAccessSample")) + { + IDictionary environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ORACLE_CONNECTION_STRING"] = _fixture.ConnectionString, + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + // to fix ORA-01882 Timezone region not found on CI. + ["TZ"] = "GMT" + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => _output.WriteLine(line.Line), + exception => _output.WriteLine($"{exception}")); + } + + apmServer.ReceivedData.Transactions.Should().HaveCount(2); + apmServer.ReceivedData.Spans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedTotalSpans + AdoNetTestData.OracleProviderExpectedSpans); + + var testSpans = apmServer.ReceivedData.Spans + .Where(s => !s.Name.StartsWith(AdoNetTestData.OracleProviderSpanNameStart)) + .ToList(); + + var genericTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunAllAsync"); + genericTransaction.Should().NotBeNull(); + + var genericSpans = testSpans.Where(s => s.TransactionId == genericTransaction.Id).ToList(); + genericSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunAllAsyncSpans); + + var baseTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunBaseTypesAsync"); + baseTransaction.Should().NotBeNull(); + + var baseSpans = testSpans.Where(s => s.TransactionId == baseTransaction.Id).ToList(); + baseSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunBaseTypesAsyncSpans); + + await apmServer.StopAsync(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCoreCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCoreCommandTests.cs new file mode 100644 index 000000000..ba786445c --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCoreCommandTests.cs @@ -0,0 +1,86 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Docker; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [Collection("Oracle")] + public class OracleManagedDataAccessCoreCommandTests + { + private readonly OracleSqlFixture _fixture; + private readonly ITestOutputHelper _output; + + public OracleManagedDataAccessCoreCommandTests(OracleSqlFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + [DockerTheory] + [ClassData(typeof(AdoNetTestData))] + public async Task CaptureAutoInstrumentedSpans(string targetFramework) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(CaptureAutoInstrumentedSpans)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + using (var profiledApplication = new ProfiledApplication("OracleManagedDataAccessCoreSample")) + { + IDictionary environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ORACLE_CONNECTION_STRING"] = _fixture.ConnectionString, + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + // to fix ORA-01882 Timezone region not found on CI. + ["TZ"] = "GMT" + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => _output.WriteLine(line.Line), + exception => _output.WriteLine($"{exception}")); + } + + apmServer.ReceivedData.Transactions.Should().HaveCount(2); + + apmServer.ReceivedData.Spans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedTotalSpans + AdoNetTestData.OracleProviderExpectedSpans); + + var testSpans = apmServer.ReceivedData.Spans + .Where(s => !s.Name.StartsWith(AdoNetTestData.OracleProviderSpanNameStart)) + .ToList(); + + testSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedTotalSpans); + + var genericTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunAllAsync"); + genericTransaction.Should().NotBeNull(); + + var genericSpans = testSpans.Where(s => s.TransactionId == genericTransaction.Id).ToList(); + genericSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunAllAsyncSpans); + + var baseTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunBaseTypesAsync"); + baseTransaction.Should().NotBeNull(); + + var baseSpans = testSpans.Where(s => s.TransactionId == baseTransaction.Id).ToList(); + baseSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunBaseTypesAsyncSpans); + + await apmServer.StopAsync(); + } + } +} \ No newline at end of file diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleSqlFixture.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleSqlFixture.cs new file mode 100644 index 000000000..1d014808d --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleSqlFixture.cs @@ -0,0 +1,41 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading.Tasks; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [CollectionDefinition("Oracle")] + public class OracleCollection : ICollectionFixture + { + } + + public class OracleSqlFixture : IAsyncLifetime + { + private readonly OracleTestcontainer _container; + + public OracleSqlFixture() + { + var builder = new TestcontainersBuilder() + .WithDatabase(new OracleTestcontainerConfiguration()); + + _container = builder.Build(); + } + + public async Task InitializeAsync() + { + await _container.StartAsync(); + ConnectionString = _container.ConnectionString; + } + + public async Task DisposeAsync() => await _container.DisposeAsync(); + + public string ConnectionString { get; private set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs new file mode 100644 index 000000000..be0758166 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/PostgreSqlFixture.cs @@ -0,0 +1,49 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading.Tasks; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [CollectionDefinition("Postgres")] + public class PostgresCollection : ICollectionFixture + { + } + + public class PostgreSqlFixture : IAsyncLifetime + { + private readonly PostgreSqlTestcontainer _container; + private const string PostgresUserName = "postgres"; + private const string PostgresPassword = "mysecretpassword"; + private const string PostgresDatabase = "db"; + + public PostgreSqlFixture() + { + var postgresBuilder = new TestcontainersBuilder() + .WithDatabase(new PostgreSqlTestcontainerConfiguration + { + Database = PostgresDatabase, + Username = PostgresUserName, + Password = PostgresPassword + }); + + _container = postgresBuilder.Build(); + } + + public async Task InitializeAsync() + { + await _container.StartAsync(); + ConnectionString = _container.ConnectionString; + } + + public async Task DisposeAsync() => await _container.DisposeAsync(); + + public string ConnectionString { get; private set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlCommandTests.cs new file mode 100644 index 000000000..3b316a2dd --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlCommandTests.cs @@ -0,0 +1,78 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Docker; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [Collection("SqlServer")] + public class SqlCommandTests + { + private readonly SqlServerFixture _fixture; + private readonly ITestOutputHelper _output; + + public SqlCommandTests(SqlServerFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + [DockerTheory] + [ClassData(typeof(AdoNetTestData))] + public async Task CaptureAutoInstrumentedSpans(string targetFramework) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(CaptureAutoInstrumentedSpans)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + using (var profiledApplication = new ProfiledApplication("SqlClientSample")) + { + IDictionary environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_SERVICE_NAME"] = $"SqlClientSample-{targetFramework}", + ["SQLSERVER_CONNECTION_STRING"] = _fixture.ConnectionString, + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => _output.WriteLine(line.Line), + exception => _output.WriteLine($"{exception}")); + } + + apmServer.ReceivedData.Transactions.Should().HaveCount(2); + apmServer.ReceivedData.Spans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedTotalSpans); + + var genericTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunAllAsync"); + genericTransaction.Should().NotBeNull(); + + var genericSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == genericTransaction.Id).ToList(); + genericSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunAllAsyncSpans); + + var baseTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunBaseTypesAsync"); + baseTransaction.Should().NotBeNull(); + + var baseSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == baseTransaction.Id).ToList(); + baseSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunBaseTypesAsyncSpans); + + await apmServer.StopAsync(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs new file mode 100644 index 000000000..f24feba15 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlServerFixture.cs @@ -0,0 +1,44 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading.Tasks; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + [CollectionDefinition("SqlServer")] + public class SqlServerCollection : ICollectionFixture + { + } + + public class SqlServerFixture : IAsyncLifetime + { + private readonly MsSqlTestcontainer _container; + + public SqlServerFixture() + { + var containerBuilder = new TestcontainersBuilder() + .WithDatabase(new MsSqlTestcontainerConfiguration + { + Password = "StrongPassword(!)!!!1" + }); + + _container = containerBuilder.Build(); + } + + public string ConnectionString { get; private set; } + + public async Task InitializeAsync() + { + await _container.StartAsync(); + ConnectionString = _container.ConnectionString; + } + + public async Task DisposeAsync() => await _container.DisposeAsync(); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqliteCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqliteCommandTests.cs new file mode 100644 index 000000000..a8cccc620 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqliteCommandTests.cs @@ -0,0 +1,69 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.AdoNet +{ + public class SqliteCommandTests + { + private readonly ITestOutputHelper _output; + + public SqliteCommandTests(ITestOutputHelper output) => _output = output; + + [Theory] + [ClassData(typeof(AdoNetTestData))] + public async Task CaptureAutoInstrumentedSpans(string targetFramework) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(CaptureAutoInstrumentedSpans)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + using (var profiledApplication = new ProfiledApplication("SqliteSample")) + { + IDictionary environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => _output.WriteLine(line.Line), + exception => _output.WriteLine($"{exception}")); + } + + apmServer.ReceivedData.Transactions.Should().HaveCount(2); + apmServer.ReceivedData.Spans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedTotalSpans); + + var genericTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunAllAsync"); + genericTransaction.Should().NotBeNull(); + + var genericSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == genericTransaction.Id).ToList(); + genericSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunAllAsyncSpans); + + var baseTransaction = apmServer.ReceivedData.Transactions.FirstOrDefault(t => t.Name == "RunBaseTypesAsync"); + baseTransaction.Should().NotBeNull(); + + var baseSpans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == baseTransaction.Id).ToList(); + baseSpans.Should().HaveCount(AdoNetTestData.DbRunnerExpectedRunBaseTypesAsyncSpans); + + await apmServer.StopAsync(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/Continuations/TaskContinuationGeneratorTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/Continuations/TaskContinuationGeneratorTests.cs new file mode 100644 index 000000000..74a1642ca --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/Continuations/TaskContinuationGeneratorTests.cs @@ -0,0 +1,169 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.Continuations +{ + public class TaskContinuationGeneratorTests + { + public static TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) => + returnValue; + + [Fact] + public async Task SuccessTest() + { + var tcg = new TaskContinuationGenerator(); + var cTask = tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()); + + await cTask; + + async Task GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + } + } + + [Fact] + public async Task ExceptionTest() + { + Exception ex = null; + + // Normal + ex = await Assert.ThrowsAsync(() => GetPreviousTask()); + Assert.Equal("Internal Test Exception", ex.Message); + + // Using the continuation + var tcg = new TaskContinuationGenerator(); + ex = await Assert.ThrowsAsync(() => tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault())); + Assert.Equal("Internal Test Exception", ex.Message); + + async Task GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + throw new CustomException("Internal Test Exception"); + } + } + + [Fact] + public async Task CancelledTest() + { + // Normal + var task = GetPreviousTask(); + await Assert.ThrowsAsync(() => task); + Assert.Equal(TaskStatus.Canceled, task.Status); + + // Using the continuation + var tcg = new TaskContinuationGenerator(); + task = tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()); + await Assert.ThrowsAsync(() => task); + Assert.Equal(TaskStatus.Canceled, task.Status); + + static Task GetPreviousTask() + { + var cts = new CancellationTokenSource(); + + return Task.FromResult(true).ContinueWith( + _ => + { + cts.Cancel(); + throw new CustomCancellationException(cts.Token); + }, + cts.Token); + } + } + + [Fact] + public async Task SuccessGenericTest() + { + var tcg = new TaskContinuationGenerator, bool>(); + var cTask = tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()); + + await cTask; + + async Task GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + return true; + } + } + + [Fact] + public async Task ExceptionGenericTest() + { + Exception ex = null; + + // Normal + ex = await Assert.ThrowsAsync(() => GetPreviousTask()); + Assert.Equal("Internal Test Exception", ex.Message); + + // Using the continuation + var tcg = new TaskContinuationGenerator, bool>(); + ex = await Assert.ThrowsAsync(() => tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault())); + Assert.Equal("Internal Test Exception", ex.Message); + + async Task GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + throw new CustomException("Internal Test Exception"); + } + } + + [Fact] + public async Task CancelledGenericTest() + { + // Normal + var task = GetPreviousTask(); + await Assert.ThrowsAsync(() => task); + Assert.Equal(TaskStatus.Canceled, task.Status); + + // Using the continuation + var tcg = new TaskContinuationGenerator, bool>(); + task = tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()); + await Assert.ThrowsAsync(() => task); + + task.Status.Should().Be(TaskStatus.Canceled); + + static Task GetPreviousTask() + { + var cts = new CancellationTokenSource(); + + return Task.FromResult(true).ContinueWith( + _ => + { + cts.Cancel(); + throw new CustomCancellationException(cts.Token); + }, + cts.Token); + } + } + + internal class CustomException : Exception + { + public CustomException(string message) + : base(message) + { + } + } + + internal class CustomCancellationException : OperationCanceledException + { + public CustomCancellationException(CancellationToken token) + : base(token) + { + } + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/Continuations/ValueTaskContinuationGeneratorTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/Continuations/ValueTaskContinuationGeneratorTests.cs new file mode 100644 index 000000000..8316737b4 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/Continuations/ValueTaskContinuationGeneratorTests.cs @@ -0,0 +1,173 @@ +// Licensed to Elasticsearch B.V under the Apache 2.0 License. +// Elasticsearch B.V licenses this file, including any modifications, to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#if NETCOREAPP3_1_OR_GREATER +using System; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Apm.Profiler.Managed.CallTarget; +using Elastic.Apm.Profiler.Managed.CallTarget.Handlers.Continuations; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.Continuations +{ + public class ValueTaskContinuationGeneratorTests + { + public static TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception exception, CallTargetState state) => + returnValue; + + [Fact] + public async ValueTask SuccessTest() + { + var tcg = new ValueTaskContinuationGenerator(); + var cTask = tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()); + + await cTask; + + async ValueTask GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + } + } + + [Fact] + public async Task ExceptionTest() + { + Exception ex = null; + + // Normal + ex = await Assert.ThrowsAsync(() => GetPreviousTask().AsTask()); + Assert.Equal("Internal Test Exception", ex.Message); + + // Using the continuation + var tcg = new ValueTaskContinuationGenerator(); + ex = await Assert.ThrowsAsync(() => tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()).AsTask()); + Assert.Equal("Internal Test Exception", ex.Message); + + async ValueTask GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + throw new CustomException("Internal Test Exception"); + } + } + + [Fact] + public async Task CancelledTest() + { + // Normal + var task = GetPreviousTask(); + await Assert.ThrowsAsync(() => task.AsTask()); + Assert.True(task.IsCanceled); + + // Using the continuation + task = GetPreviousTask(); + var tcg = new ValueTaskContinuationGenerator(); + await Assert.ThrowsAsync(() => tcg.SetContinuation(this, task, null, CallTargetState.GetDefault()).AsTask()); + Assert.True(task.IsCanceled); + + ValueTask GetPreviousTask() + { + var cts = new CancellationTokenSource(); + + var task = Task.FromResult(true).ContinueWith( + _ => + { + cts.Cancel(); + throw new CustomCancellationException(cts.Token); + }, + cts.Token); + + return new ValueTask(task); + } + } + + [Fact] + public async Task SuccessGenericTest() + { + var tcg = new ValueTaskContinuationGenerator, bool>(); + var cTask = tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()); + + await cTask; + + async ValueTask GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + return true; + } + } + + [Fact] + public async Task ExceptionGenericTest() + { + Exception ex = null; + + // Normal + ex = await Assert.ThrowsAsync(() => GetPreviousTask().AsTask()); + Assert.Equal("Internal Test Exception", ex.Message); + + // Using the continuation + var tcg = new ValueTaskContinuationGenerator, bool>(); + ex = await Assert.ThrowsAsync(() => tcg.SetContinuation(this, GetPreviousTask(), null, CallTargetState.GetDefault()).AsTask()); + Assert.Equal("Internal Test Exception", ex.Message); + + async ValueTask GetPreviousTask() + { + await Task.Delay(1000).ConfigureAwait(false); + throw new CustomException("Internal Test Exception"); + } + } + + [Fact] + public async Task CancelledGenericTest() + { + // Normal + var task = GetPreviousTask(); + await Assert.ThrowsAsync(() => task.AsTask()); + Assert.True(task.IsCanceled); + + // Using the continuation + task = GetPreviousTask(); + var tcg = new ValueTaskContinuationGenerator, bool>(); + await Assert.ThrowsAsync(() => tcg.SetContinuation(this, task, null, CallTargetState.GetDefault()).AsTask()); + Assert.True(task.IsCanceled); + + ValueTask GetPreviousTask() + { + var cts = new CancellationTokenSource(); + + var task = Task.FromResult(true).ContinueWith( + _ => + { + cts.Cancel(); + throw new CustomCancellationException(cts.Token); + }, + cts.Token); + + return new ValueTask(task); + } + } + + internal class CustomException : Exception + { + public CustomException(string message) + : base(message) + { + } + } + + internal class CustomCancellationException : OperationCanceledException + { + public CustomCancellationException(CancellationToken token) + : base(token) + { + } + } + } +} +#endif diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckExplicitInterfaceTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckExplicitInterfaceTests.cs new file mode 100644 index 000000000..bbff601ee --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckExplicitInterfaceTests.cs @@ -0,0 +1,52 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class DuckExplicitInterfaceTests + { + [Fact] + public void NormalTest() + { + var targetObject = new TargetObject(); + var proxy = targetObject.DuckCast(); + + proxy.SayHi().Should().Be("Hello World"); + proxy.SayHiWithWildcard().Should().Be("Hello World (*)"); + } + + public class TargetObject : ITarget + { + string ITarget.SayHi() => "Hello World"; + + string ITarget.SayHiWithWildcard() => "Hello World (*)"; + } + + public interface ITarget + { + string SayHi(); + + string SayHiWithWildcard(); + } + + public interface IProxyDefinition + { + [Duck(ExplicitInterfaceTypeName = "Elastic.Apm.Profiler.Managed.Tests.DuckTyping.DuckExplicitInterfaceTests+ITarget")] + string SayHi(); + + [Duck(ExplicitInterfaceTypeName = "*")] + string SayHiWithWildcard(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIgnoreTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIgnoreTests.cs new file mode 100644 index 000000000..5a70efa6e --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIgnoreTests.cs @@ -0,0 +1,162 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class DuckIgnoreTests + { + [Fact] + public void NonPublicStructCopyTest() + { + PrivateStruct instance = default; + CopyStruct copy = instance.DuckCast(); + Assert.Equal((int)instance.Value, (int)copy.Value); + Assert.Equal(ValuesDuckType.Third.ToString(), copy.GetValue()); + Assert.Equal(ValuesDuckType.Third.ToString(), ((IGetValue)copy).GetValueProp); + } + +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void NonPublicStructInterfaceProxyTest() + { + PrivateStruct instance = default; + IPrivateStruct proxy = instance.DuckCast(); + Assert.Equal((int)instance.Value, (int)proxy.Value); + Assert.Equal(ValuesDuckType.Third.ToString(), proxy.GetValue()); + Assert.Equal(ValuesDuckType.Third.ToString(), proxy.GetValueProp); + } +#endif + + [Fact] + public void NonPublicStructAbstractProxyTest() + { + PrivateStruct instance = default; + AbstractPrivateProxy proxy = instance.DuckCast(); + Assert.Equal((int)instance.Value, (int)proxy.Value); + Assert.Equal(ValuesDuckType.Third.ToString(), proxy.GetValue()); + Assert.Equal(ValuesDuckType.Third.ToString(), ((IGetValue)proxy).GetValueProp); + Assert.Equal(42, proxy.GetAnswerToMeaningOfLife()); + } + + [Fact] + public void NonPublicStructVirtualProxyTest() + { + PrivateStruct instance = default; + VirtualPrivateProxy proxy = instance.DuckCast(); + Assert.Equal((int)instance.Value, (int)proxy.Value); + Assert.Equal(ValuesDuckType.Third.ToString(), proxy.GetValue()); + Assert.Equal(ValuesDuckType.Third.ToString(), ((IGetValue)proxy).GetValueProp); + Assert.Equal(42, proxy.GetAnswerToMeaningOfLife()); + } + + [DuckCopy] + public struct CopyStruct : IGetValue + { + public ValuesDuckType Value; + + string IGetValue.GetValueProp => Value.ToString(); + + public string GetValue() => Value.ToString(); + } + +#if NETCOREAPP3_0_OR_GREATER + // Interface with a default implementation + public interface IPrivateStruct + { + ValuesDuckType Value { get; } + + [DuckIgnore] + public string GetValueProp => Value.ToString(); + + [DuckIgnore] + public string GetValue() => Value.ToString(); + } +#endif + + public abstract class AbstractPrivateProxy : IGetValue + { + public abstract ValuesDuckType Value { get; } + + [DuckIgnore] + public string GetValueProp => Value.ToString(); + + [DuckIgnore] + public string GetValue() => Value.ToString(); + + public abstract int GetAnswerToMeaningOfLife(); + } + + public class VirtualPrivateProxy : IGetValue + { + public virtual ValuesDuckType Value { get; } + + [DuckIgnore] + public string GetValueProp => Value.ToString(); + + [DuckIgnore] + public string GetValue() => Value.ToString(); + + public virtual int GetAnswerToMeaningOfLife() => default; + } + + private readonly struct PrivateStruct + { + public readonly Values Value => Values.Third; + + public int GetAnswerToMeaningOfLife() => 42; + } + + public interface IGetValue + { + string GetValueProp { get; } + + string GetValue(); + } + + public enum ValuesDuckType + { + /// + /// First + /// + First, + + /// + /// Second + /// + Second, + + /// + /// Third + /// + Third + } + + public enum Values + { + /// + /// First + /// + First, + + /// + /// Second + /// + Second, + + /// + /// Third + /// + Third + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIncludeTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIncludeTests.cs new file mode 100644 index 000000000..763f9b873 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckIncludeTests.cs @@ -0,0 +1,62 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#pragma warning disable SA1201 // Elements must appear in the correct order + +using Elastic.Apm.Profiler.Managed.DuckTyping; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class DuckIncludeTests + { + [Fact] + public void ShouldOverrideToString() + { + var instance = new SomeClassWithDuckInclude(); + + var proxy = instance.DuckCast(); + + proxy.ToString().Should().Be(instance.ToString()); + } + + [Fact] + public void ShouldNotOverrideToString() + { + var instance = new SomeClassWithoutDuckInclude(); + + var proxy = instance.DuckCast(); + + proxy.ToString().Should().NotBe(instance.ToString()); + } + + public class SomeClassWithDuckInclude + { + [DuckInclude] + public override string ToString() + { + return "OK"; + } + } + + public class SomeClassWithoutDuckInclude + { + public override string ToString() + { + return "OK"; + } + } + + public interface IInterface + { + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckTypeExtensionsTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckTypeExtensionsTests.cs new file mode 100644 index 000000000..7a81b166c --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/DuckTypeExtensionsTests.cs @@ -0,0 +1,89 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading.Tasks; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class DuckTypeExtensionsTests + { + [Fact] + public void DuckCastTest() + { + var task = (Task)Task.FromResult("Hello World"); + + var iTaskString = task.DuckCast(); + var objTaskString = task.DuckCast(typeof(ITaskString)); + + iTaskString.Result.Should().Be("Hello World"); + (iTaskString.GetType() == objTaskString.GetType()).Should().BeTrue(); + } + + [Fact] + public void NullCheck() + { + object obj = null; + var iTaskString = obj.DuckCast(); + + iTaskString.Should().BeNull(); + } + + [Fact] + public void TryDuckCastTest() + { + var task = (Task)Task.FromResult("Hello World"); + + var tskResultBool = task.TryDuckCast(out var tskResult); + Assert.True(tskResultBool); + Assert.Equal("Hello World", tskResult.Result); + + var tskErrorBool = task.TryDuckCast(out var tskResultError); + Assert.False(tskErrorBool); + Assert.Null(tskResultError); + } + + [Fact] + public void DuckAsTest() + { + var task = (Task)Task.FromResult("Hello World"); + + var tskResult = task.DuckAs(); + var tskResultError = task.DuckAs(); + + tskResult.Result.Should().Be("Hello World"); + tskResultError.Should().BeNull(); + } + + [Fact] + public void DuckIsTest() + { + var task = (Task)Task.FromResult("Hello World"); + + var bOk = task.DuckIs(); + var bError = task.DuckIs(); + + bOk.Should().BeTrue(); + bError.Should().BeFalse(); + } + + public interface ITaskString + { + string Result { get; } + } + + public interface ITaskError + { + string ResultWrong { get; } + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ExceptionsTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ExceptionsTests.cs new file mode 100644 index 000000000..1f6dd4bc3 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ExceptionsTests.cs @@ -0,0 +1,601 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading.Tasks; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class ExceptionsTests + { + [Fact] + public void PropertyCantBeReadException() + { + object target = new PropertyCantBeReadExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IPropertyCantBeReadException + { + string OnlySetter { get; set; } + } + + public struct StructPropertyCantBeReadException + { + public string OnlySetter; + } + + internal class PropertyCantBeReadExceptionClass + { + public string OnlySetter + { + set { } + } + } + + // * + + [Fact] + public void PropertyCantBeWrittenException() + { + object target = new PropertyCantBeWrittenExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IPropertyCantBeWrittenException + { + string OnlyGetter { get; set; } + } + + internal class PropertyCantBeWrittenExceptionClass + { + public string OnlyGetter { get; } + } + + // * + + [Fact] + public void PropertyArgumentsLengthException() + { + object target = new PropertyArgumentsLengthExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IPropertyArgumentsLengthException + { + string Item { get; } + } + + [DuckCopy] + public struct StructPropertyArgumentsLengthException + { + public string Item; + } + + public interface ISetPropertyArgumentsLengthException + { + string Item { set; } + } + + internal class PropertyArgumentsLengthExceptionClass + { + public string this[string key] + { + get => null; + set { } + } + } + + // * + + [Fact] + public void FieldIsReadonlyException() + { + object target = new FieldIsReadonlyExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IFieldIsReadonlyException + { + [Duck(Name = "_name", Kind = DuckKind.Field)] + string Name { get; set; } + } + + internal class FieldIsReadonlyExceptionClass + { + private readonly string _name = string.Empty; + + public string AvoidCompileError => _name; + } + + // * + + [Fact] + public void PropertyOrFieldNotFoundException() + { + object[] targets = new object[] + { + new PropertyOrFieldNotFoundExceptionClass(), + (PropertyOrFieldNotFoundExceptionTargetStruct)default + }; + + foreach (object target in targets) + { + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + } + + public interface IPropertyOrFieldNotFoundException + { + string Name { get; set; } + } + + public interface IPropertyOrFieldNotFound2Exception + { + [Duck(Kind = DuckKind.Field)] + string Name { get; set; } + } + + public interface IPropertyOrFieldNotFound3Exception + { + string Name { set; } + } + + public struct PropertyOrFieldNotFoundExceptionStruct + { + public string Name; + } + + public struct PropertyOrFieldNotFound2ExceptionStruct + { + [Duck(Kind = DuckKind.Field)] + public string Name; + } + + internal class PropertyOrFieldNotFoundExceptionClass + { + } + + internal struct PropertyOrFieldNotFoundExceptionTargetStruct + { + } + +#if NET452 + // * + [Fact] + public void TypeIsNotPublicException() + { + object target = new TypeIsNotPublicExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(typeof(ITypeIsNotPublicException)); + }); + } + + internal interface ITypeIsNotPublicException + { + string Name { get; set; } + } + + internal class TypeIsNotPublicExceptionClass + { + public string Name { get; set; } + } +#endif + // * + + [Fact] + public void StructMembersCannotBeChangedException() + { + StructMembersCannotBeChangedExceptionStruct targetStruct = default; + object target = (object)targetStruct; + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IStructMembersCannotBeChangedException + { + string Name { get; set; } + } + + internal struct StructMembersCannotBeChangedExceptionStruct + { + public string Name { get; set; } + } + + // * + + [Fact] + public void StructMembersCannotBeChanged2Exception() + { + StructMembersCannotBeChanged2ExceptionStruct targetStruct = default; + object target = (object)targetStruct; + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IStructMembersCannotBeChanged2Exception + { + [Duck(Kind = DuckKind.Field)] + string Name { get; set; } + } + + internal struct StructMembersCannotBeChanged2ExceptionStruct + { +#pragma warning disable 649 + public string Name; +#pragma warning restore 649 + } + + // * + + [Fact] + public void TargetMethodNotFoundException() + { + object target = new TargetMethodNotFoundExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface ITargetMethodNotFoundException + { + public void AddTypo(string key, string value); + } + + public interface ITargetMethodNotFound2Exception + { + public void AddGeneric(object value); + } + + public interface ITargetMethodNotFound3Exception + { + [Duck(GenericParameterTypeNames = new string[] { "P1", "P2" })] + public void AddGeneric(object value); + } + + internal class TargetMethodNotFoundExceptionClass + { + public void Add(string key, string value) + { + } + + public void AddGeneric(T value) + { + } + } + + // * + + [Fact] + public void ProxyMethodParameterIsMissingException() + { + object target = new ProxyMethodParameterIsMissingExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IProxyMethodParameterIsMissingException + { + [Duck(ParameterTypeNames = new string[] { "System.String", "System.String" })] + public void Add(string key); + } + + internal class ProxyMethodParameterIsMissingExceptionClass + { + public void Add(string key, string value) + { + } + } + + // * + + [Fact] + public void ProxyAndTargetMethodParameterSignatureMismatchException() + { + object target = new ProxyAndTargetMethodParameterSignatureMismatchExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IProxyAndTargetMethodParameterSignatureMismatchException + { + [Duck(ParameterTypeNames = new string[] { "System.String", "System.String" })] + public void Add(string key, ref string value); + } + + public interface IProxyAndTargetMethodParameterSignatureMismatch2Exception + { + [Duck(ParameterTypeNames = new string[] { "System.String", "System.String" })] + public void Add(string key, out string value); + } + + internal class ProxyAndTargetMethodParameterSignatureMismatchExceptionClass + { + public void Add(string key, string value) + { + } + } + +#if NET452 + // * + [Fact] + public void ProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesException() + { + object target = new ProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesException + { + public void Add(TKey key, TValue value); + } + + internal class ProxyMethodsWithGenericParametersNotSupportedInNonPublicInstancesExceptionClass + { + public void Add(TKey key, TValue value) + { + } + } +#endif + // * + + [Fact] + public void TargetMethodAmbiguousMatchException() + { + object target = new TargetMethodAmbiguousMatchExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface ITargetMethodAmbiguousMatchException + { + public void Add(string key, object value); + + public void Add(string key, string value); + } + + internal class TargetMethodAmbiguousMatchExceptionClass + { + public void Add(string key, Task value) + { + } + + public void Add(string key, string value) + { + } + } + + // * + + [Fact] + public void ProxyTypeDefinitionIsNull() + { + Assert.Throws(() => + { + DuckType.Create(null, new object()); + }); + } + + // * + + [Fact] + public void TargetObjectInstanceIsNull() + { + Assert.Throws(() => + { + DuckType.Create(typeof(ITargetObjectInstanceIsNull), null); + }); + } + + public interface ITargetObjectInstanceIsNull + { + } + + // * + + [Fact] + public void InvalidTypeConversionException() + { + object target = new InvalidTypeConversionExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IInvalidTypeConversionException + { + float Sum(int a, int b); + } + + public class InvalidTypeConversionExceptionClass + { + public int Sum(int a, int b) + { + return a + b; + } + } + + // * + + [Fact] + public void ObjectInvalidTypeConversionException() + { + object target = new ObjectInvalidTypeConversionExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IObjectInvalidTypeConversionException + { + string Value { get; } + } + + public class ObjectInvalidTypeConversionExceptionClass + { + public int Value => 42; + } + + // * + + [Fact] + public void ObjectInvalidTypeConversion2Exception() + { + object target = new ObjectInvalidTypeConversion2ExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IObjectInvalidTypeConversion2Exception + { + int Value { get; } + } + + public class ObjectInvalidTypeConversion2ExceptionClass + { + public string Value => "Hello world"; + } + + // * + + [Fact] + public void ObjectInvalidTypeConversion3Exception() + { + object target = new ObjectInvalidTypeConversion3ExceptionClass(); + + Assert.Throws(() => + { + target.DuckCast(); + }); + } + + public interface IObjectInvalidTypeConversion3Exception + { + [Duck(Kind = DuckKind.Field)] + int Value { get; } + } + + public class ObjectInvalidTypeConversion3ExceptionClass + { +#pragma warning disable 414 +#pragma warning disable IDE0051 // Remove unused private members +#pragma warning disable SA1306 // Field names must begin with lower-case letter + private readonly string Value = "Hello world"; +#pragma warning restore SA1306 // Field names must begin with lower-case letter +#pragma warning restore IDE0051 // Remove unused private members +#pragma warning restore 414 + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureDuckType.cs new file mode 100644 index 000000000..1476d66ad --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureDuckType.cs @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ReferenceType.ProxiesDefinitions +{ + public interface IObscureDuckType + { + [Duck(Name = "_publicStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string PublicStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_internalStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string InternalStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string ProtectedStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_privateStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string PrivateStaticReadonlyReferenceTypeField { get; } + + // * + + [Duck(Name = "_publicStaticReferenceTypeField", Kind = DuckKind.Field)] + string PublicStaticReferenceTypeField { get; set; } + + [Duck(Name = "_internalStaticReferenceTypeField", Kind = DuckKind.Field)] + string InternalStaticReferenceTypeField { get; set; } + + [Duck(Name = "_protectedStaticReferenceTypeField", Kind = DuckKind.Field)] + string ProtectedStaticReferenceTypeField { get; set; } + + [Duck(Name = "_privateStaticReferenceTypeField", Kind = DuckKind.Field)] + string PrivateStaticReferenceTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string PublicReadonlyReferenceTypeField { get; } + + [Duck(Name = "_internalReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string InternalReadonlyReferenceTypeField { get; } + + [Duck(Name = "_protectedReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string ProtectedReadonlyReferenceTypeField { get; } + + [Duck(Name = "_privateReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string PrivateReadonlyReferenceTypeField { get; } + + // * + + [Duck(Name = "_publicReferenceTypeField", Kind = DuckKind.Field)] + string PublicReferenceTypeField { get; set; } + + [Duck(Name = "_internalReferenceTypeField", Kind = DuckKind.Field)] + string InternalReferenceTypeField { get; set; } + + [Duck(Name = "_protectedReferenceTypeField", Kind = DuckKind.Field)] + string ProtectedReferenceTypeField { get; set; } + + [Duck(Name = "_privateReferenceTypeField", Kind = DuckKind.Field)] + string PrivateReferenceTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs new file mode 100644 index 000000000..d396398fa --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ReferenceType.ProxiesDefinitions +{ + public interface IObscureReadonlyErrorDuckType + { + [Duck(Name = "_publicReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string PublicReadonlyReferenceTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs new file mode 100644 index 000000000..aa75c2d47 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ReferenceType.ProxiesDefinitions +{ + public interface IObscureStaticReadonlyErrorDuckType + { + [Duck(Name = "_publicStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + string PublicStaticReadonlyReferenceTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs new file mode 100644 index 000000000..c33baa5bf --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ReferenceType.ProxiesDefinitions +{ + public abstract class ObscureDuckTypeAbstractClass + { + [Duck(Name = "_publicStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PublicStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_internalStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string InternalStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string ProtectedStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_privateStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PrivateStaticReadonlyReferenceTypeField { get; } + + // * + + [Duck(Name = "_publicStaticReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PublicStaticReferenceTypeField { get; set; } + + [Duck(Name = "_internalStaticReferenceTypeField", Kind = DuckKind.Field)] + public abstract string InternalStaticReferenceTypeField { get; set; } + + [Duck(Name = "_protectedStaticReferenceTypeField", Kind = DuckKind.Field)] + public abstract string ProtectedStaticReferenceTypeField { get; set; } + + [Duck(Name = "_privateStaticReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PrivateStaticReferenceTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PublicReadonlyReferenceTypeField { get; } + + [Duck(Name = "_internalReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string InternalReadonlyReferenceTypeField { get; } + + [Duck(Name = "_protectedReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string ProtectedReadonlyReferenceTypeField { get; } + + [Duck(Name = "_privateReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PrivateReadonlyReferenceTypeField { get; } + + // * + + [Duck(Name = "_publicReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PublicReferenceTypeField { get; set; } + + [Duck(Name = "_internalReferenceTypeField", Kind = DuckKind.Field)] + public abstract string InternalReferenceTypeField { get; set; } + + [Duck(Name = "_protectedReferenceTypeField", Kind = DuckKind.Field)] + public abstract string ProtectedReferenceTypeField { get; set; } + + [Duck(Name = "_privateReferenceTypeField", Kind = DuckKind.Field)] + public abstract string PrivateReferenceTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs new file mode 100644 index 000000000..31e56aed4 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ReferenceType.ProxiesDefinitions +{ + public class ObscureDuckTypeVirtualClass + { + [Duck(Name = "_publicStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PublicStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_internalStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string InternalStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string ProtectedStaticReadonlyReferenceTypeField { get; } + + [Duck(Name = "_privateStaticReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PrivateStaticReadonlyReferenceTypeField { get; } + + // * + + [Duck(Name = "_publicStaticReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PublicStaticReferenceTypeField { get; set; } + + [Duck(Name = "_internalStaticReferenceTypeField", Kind = DuckKind.Field)] + public virtual string InternalStaticReferenceTypeField { get; set; } + + [Duck(Name = "_protectedStaticReferenceTypeField", Kind = DuckKind.Field)] + public virtual string ProtectedStaticReferenceTypeField { get; set; } + + [Duck(Name = "_privateStaticReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PrivateStaticReferenceTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PublicReadonlyReferenceTypeField { get; } + + [Duck(Name = "_internalReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string InternalReadonlyReferenceTypeField { get; } + + [Duck(Name = "_protectedReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string ProtectedReadonlyReferenceTypeField { get; } + + [Duck(Name = "_privateReadonlyReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PrivateReadonlyReferenceTypeField { get; } + + // * + + [Duck(Name = "_publicReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PublicReferenceTypeField { get; set; } + + [Duck(Name = "_internalReferenceTypeField", Kind = DuckKind.Field)] + public virtual string InternalReferenceTypeField { get; set; } + + [Duck(Name = "_protectedReferenceTypeField", Kind = DuckKind.Field)] + public virtual string ProtectedReferenceTypeField { get; set; } + + [Duck(Name = "_privateReferenceTypeField", Kind = DuckKind.Field)] + public virtual string PrivateReferenceTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ReferenceTypeFieldTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ReferenceTypeFieldTests.cs new file mode 100644 index 000000000..751879c5b --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ReferenceType/ReferenceTypeFieldTests.cs @@ -0,0 +1,290 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ReferenceType.ProxiesDefinitions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ReferenceType +{ + public class ReferenceTypeFieldTests + { + public static IEnumerable Data() + { + return new[] + { + new object[] { ObscureObject.GetFieldPublicObject() }, + new object[] { ObscureObject.GetFieldInternalObject() }, + new object[] { ObscureObject.GetFieldPrivateObject() }, + }; + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticReadonlyFieldsSetException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void ReadonlyFieldsSetException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticReadonlyFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal("10", duckInterface.PublicStaticReadonlyReferenceTypeField); + Assert.Equal("10", duckAbstract.PublicStaticReadonlyReferenceTypeField); + Assert.Equal("10", duckVirtual.PublicStaticReadonlyReferenceTypeField); + + // * + Assert.Equal("11", duckInterface.InternalStaticReadonlyReferenceTypeField); + Assert.Equal("11", duckAbstract.InternalStaticReadonlyReferenceTypeField); + Assert.Equal("11", duckVirtual.InternalStaticReadonlyReferenceTypeField); + + // * + Assert.Equal("12", duckInterface.ProtectedStaticReadonlyReferenceTypeField); + Assert.Equal("12", duckAbstract.ProtectedStaticReadonlyReferenceTypeField); + Assert.Equal("12", duckVirtual.ProtectedStaticReadonlyReferenceTypeField); + + // * + Assert.Equal("13", duckInterface.PrivateStaticReadonlyReferenceTypeField); + Assert.Equal("13", duckAbstract.PrivateStaticReadonlyReferenceTypeField); + Assert.Equal("13", duckVirtual.PrivateStaticReadonlyReferenceTypeField); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal("20", duckInterface.PublicStaticReferenceTypeField); + Assert.Equal("20", duckAbstract.PublicStaticReferenceTypeField); + Assert.Equal("20", duckVirtual.PublicStaticReferenceTypeField); + + duckInterface.PublicStaticReferenceTypeField = "42"; + Assert.Equal("42", duckInterface.PublicStaticReferenceTypeField); + Assert.Equal("42", duckAbstract.PublicStaticReferenceTypeField); + Assert.Equal("42", duckVirtual.PublicStaticReferenceTypeField); + + duckAbstract.PublicStaticReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.PublicStaticReferenceTypeField); + Assert.Equal("50", duckAbstract.PublicStaticReferenceTypeField); + Assert.Equal("50", duckVirtual.PublicStaticReferenceTypeField); + + duckVirtual.PublicStaticReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.PublicStaticReferenceTypeField); + Assert.Equal("60", duckAbstract.PublicStaticReferenceTypeField); + Assert.Equal("60", duckVirtual.PublicStaticReferenceTypeField); + + // * + + Assert.Equal("21", duckInterface.InternalStaticReferenceTypeField); + Assert.Equal("21", duckAbstract.InternalStaticReferenceTypeField); + Assert.Equal("21", duckVirtual.InternalStaticReferenceTypeField); + + duckInterface.InternalStaticReferenceTypeField = "42"; + Assert.Equal("42", duckInterface.InternalStaticReferenceTypeField); + Assert.Equal("42", duckAbstract.InternalStaticReferenceTypeField); + Assert.Equal("42", duckVirtual.InternalStaticReferenceTypeField); + + duckAbstract.InternalStaticReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.InternalStaticReferenceTypeField); + Assert.Equal("50", duckAbstract.InternalStaticReferenceTypeField); + Assert.Equal("50", duckVirtual.InternalStaticReferenceTypeField); + + duckVirtual.InternalStaticReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.InternalStaticReferenceTypeField); + Assert.Equal("60", duckAbstract.InternalStaticReferenceTypeField); + Assert.Equal("60", duckVirtual.InternalStaticReferenceTypeField); + + // * + + Assert.Equal("22", duckInterface.ProtectedStaticReferenceTypeField); + Assert.Equal("22", duckAbstract.ProtectedStaticReferenceTypeField); + Assert.Equal("22", duckVirtual.ProtectedStaticReferenceTypeField); + + duckInterface.ProtectedStaticReferenceTypeField = "42"; + Assert.Equal("42", duckInterface.ProtectedStaticReferenceTypeField); + Assert.Equal("42", duckAbstract.ProtectedStaticReferenceTypeField); + Assert.Equal("42", duckVirtual.ProtectedStaticReferenceTypeField); + + duckAbstract.ProtectedStaticReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.ProtectedStaticReferenceTypeField); + Assert.Equal("50", duckAbstract.ProtectedStaticReferenceTypeField); + Assert.Equal("50", duckVirtual.ProtectedStaticReferenceTypeField); + + duckVirtual.ProtectedStaticReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.ProtectedStaticReferenceTypeField); + Assert.Equal("60", duckAbstract.ProtectedStaticReferenceTypeField); + Assert.Equal("60", duckVirtual.ProtectedStaticReferenceTypeField); + + // * + + Assert.Equal("23", duckInterface.PrivateStaticReferenceTypeField); + Assert.Equal("23", duckAbstract.PrivateStaticReferenceTypeField); + Assert.Equal("23", duckVirtual.PrivateStaticReferenceTypeField); + + duckInterface.PrivateStaticReferenceTypeField = "42"; + Assert.Equal("42", duckInterface.PrivateStaticReferenceTypeField); + Assert.Equal("42", duckAbstract.PrivateStaticReferenceTypeField); + Assert.Equal("42", duckVirtual.PrivateStaticReferenceTypeField); + + duckAbstract.PrivateStaticReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.PrivateStaticReferenceTypeField); + Assert.Equal("50", duckAbstract.PrivateStaticReferenceTypeField); + Assert.Equal("50", duckVirtual.PrivateStaticReferenceTypeField); + + duckVirtual.PrivateStaticReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.PrivateStaticReferenceTypeField); + Assert.Equal("60", duckAbstract.PrivateStaticReferenceTypeField); + Assert.Equal("60", duckVirtual.PrivateStaticReferenceTypeField); + } + + [Theory] + [MemberData(nameof(Data))] + public void ReadonlyFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal("30", duckInterface.PublicReadonlyReferenceTypeField); + Assert.Equal("30", duckAbstract.PublicReadonlyReferenceTypeField); + Assert.Equal("30", duckVirtual.PublicReadonlyReferenceTypeField); + + // * + Assert.Equal("31", duckInterface.InternalReadonlyReferenceTypeField); + Assert.Equal("31", duckAbstract.InternalReadonlyReferenceTypeField); + Assert.Equal("31", duckVirtual.InternalReadonlyReferenceTypeField); + + // * + Assert.Equal("32", duckInterface.ProtectedReadonlyReferenceTypeField); + Assert.Equal("32", duckAbstract.ProtectedReadonlyReferenceTypeField); + Assert.Equal("32", duckVirtual.ProtectedReadonlyReferenceTypeField); + + // * + Assert.Equal("33", duckInterface.PrivateReadonlyReferenceTypeField); + Assert.Equal("33", duckAbstract.PrivateReadonlyReferenceTypeField); + Assert.Equal("33", duckVirtual.PrivateReadonlyReferenceTypeField); + } + + [Theory] + [MemberData(nameof(Data))] + public void Fields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal("40", duckInterface.PublicReferenceTypeField); + Assert.Equal("40", duckAbstract.PublicReferenceTypeField); + Assert.Equal("40", duckVirtual.PublicReferenceTypeField); + + duckInterface.PublicReferenceTypeField = "42"; + Assert.Equal("42", duckInterface.PublicReferenceTypeField); + Assert.Equal("42", duckAbstract.PublicReferenceTypeField); + Assert.Equal("42", duckVirtual.PublicReferenceTypeField); + + duckAbstract.PublicReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.PublicReferenceTypeField); + Assert.Equal("50", duckAbstract.PublicReferenceTypeField); + Assert.Equal("50", duckVirtual.PublicReferenceTypeField); + + duckVirtual.PublicReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.PublicReferenceTypeField); + Assert.Equal("60", duckAbstract.PublicReferenceTypeField); + Assert.Equal("60", duckVirtual.PublicReferenceTypeField); + + // * + + Assert.Equal("41", duckInterface.InternalReferenceTypeField); + Assert.Equal("41", duckAbstract.InternalReferenceTypeField); + Assert.Equal("41", duckVirtual.InternalReferenceTypeField); + + duckInterface.InternalReferenceTypeField = "42"; + Assert.Equal("42", duckInterface.InternalReferenceTypeField); + Assert.Equal("42", duckAbstract.InternalReferenceTypeField); + Assert.Equal("42", duckVirtual.InternalReferenceTypeField); + + duckAbstract.InternalReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.InternalReferenceTypeField); + Assert.Equal("50", duckAbstract.InternalReferenceTypeField); + Assert.Equal("50", duckVirtual.InternalReferenceTypeField); + + duckVirtual.InternalReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.InternalReferenceTypeField); + Assert.Equal("60", duckAbstract.InternalReferenceTypeField); + Assert.Equal("60", duckVirtual.InternalReferenceTypeField); + + // * + + Assert.Equal("42", duckInterface.ProtectedReferenceTypeField); + Assert.Equal("42", duckAbstract.ProtectedReferenceTypeField); + Assert.Equal("42", duckVirtual.ProtectedReferenceTypeField); + + duckInterface.ProtectedReferenceTypeField = "45"; + Assert.Equal("45", duckInterface.ProtectedReferenceTypeField); + Assert.Equal("45", duckAbstract.ProtectedReferenceTypeField); + Assert.Equal("45", duckVirtual.ProtectedReferenceTypeField); + + duckAbstract.ProtectedReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.ProtectedReferenceTypeField); + Assert.Equal("50", duckAbstract.ProtectedReferenceTypeField); + Assert.Equal("50", duckVirtual.ProtectedReferenceTypeField); + + duckVirtual.ProtectedReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.ProtectedReferenceTypeField); + Assert.Equal("60", duckAbstract.ProtectedReferenceTypeField); + Assert.Equal("60", duckVirtual.ProtectedReferenceTypeField); + + // * + + Assert.Equal("43", duckInterface.PrivateReferenceTypeField); + Assert.Equal("43", duckAbstract.PrivateReferenceTypeField); + Assert.Equal("43", duckVirtual.PrivateReferenceTypeField); + + duckInterface.PrivateReferenceTypeField = "42"; + Assert.Equal("42", duckInterface.PrivateReferenceTypeField); + Assert.Equal("42", duckAbstract.PrivateReferenceTypeField); + Assert.Equal("42", duckVirtual.PrivateReferenceTypeField); + + duckAbstract.PrivateReferenceTypeField = "50"; + Assert.Equal("50", duckInterface.PrivateReferenceTypeField); + Assert.Equal("50", duckAbstract.PrivateReferenceTypeField); + Assert.Equal("50", duckVirtual.PrivateReferenceTypeField); + + duckVirtual.PrivateReferenceTypeField = "60"; + Assert.Equal("60", duckInterface.PrivateReferenceTypeField); + Assert.Equal("60", duckAbstract.PrivateReferenceTypeField); + Assert.Equal("60", duckVirtual.PrivateReferenceTypeField); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IDummyFieldObject.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IDummyFieldObject.cs new file mode 100644 index 000000000..15835c955 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IDummyFieldObject.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining.ProxiesDefinitions +{ + public interface IDummyFieldObject + { + [Duck(Kind = DuckKind.Field)] + int MagicNumber { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureDuckType.cs new file mode 100644 index 000000000..4d3f831bc --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureDuckType.cs @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining.ProxiesDefinitions +{ + public interface IObscureDuckType + { + [Duck(Name = "_publicStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PublicStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_internalStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject InternalStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject ProtectedStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_privateStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PrivateStaticReadonlySelfTypeField { get; } + + // * + + [Duck(Name = "_publicStaticSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PublicStaticSelfTypeField { get; set; } + + [Duck(Name = "_internalStaticSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject InternalStaticSelfTypeField { get; set; } + + [Duck(Name = "_protectedStaticSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject ProtectedStaticSelfTypeField { get; set; } + + [Duck(Name = "_privateStaticSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PrivateStaticSelfTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PublicReadonlySelfTypeField { get; } + + [Duck(Name = "_internalReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject InternalReadonlySelfTypeField { get; } + + [Duck(Name = "_protectedReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject ProtectedReadonlySelfTypeField { get; } + + [Duck(Name = "_privateReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PrivateReadonlySelfTypeField { get; } + + // * + + [Duck(Name = "_publicSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PublicSelfTypeField { get; set; } + + [Duck(Name = "_internalSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject InternalSelfTypeField { get; set; } + + [Duck(Name = "_protectedSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject ProtectedSelfTypeField { get; set; } + + [Duck(Name = "_privateSelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PrivateSelfTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs new file mode 100644 index 000000000..ed7376310 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining.ProxiesDefinitions +{ + public interface IObscureReadonlyErrorDuckType + { + [Duck(Name = "_publicReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PublicReadonlySelfTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs new file mode 100644 index 000000000..a8cdce5c2 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining.ProxiesDefinitions +{ + public interface IObscureStaticReadonlyErrorDuckType + { + [Duck(Name = "_publicStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + IDummyFieldObject PublicStaticReadonlySelfTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs new file mode 100644 index 000000000..60c176298 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining.ProxiesDefinitions +{ + public abstract class ObscureDuckTypeAbstractClass + { + [Duck(Name = "_publicStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PublicStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_internalStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject InternalStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject ProtectedStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_privateStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PrivateStaticReadonlySelfTypeField { get; } + + // * + + [Duck(Name = "_publicStaticSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PublicStaticSelfTypeField { get; set; } + + [Duck(Name = "_internalStaticSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject InternalStaticSelfTypeField { get; set; } + + [Duck(Name = "_protectedStaticSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject ProtectedStaticSelfTypeField { get; set; } + + [Duck(Name = "_privateStaticSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PrivateStaticSelfTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PublicReadonlySelfTypeField { get; } + + [Duck(Name = "_internalReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject InternalReadonlySelfTypeField { get; } + + [Duck(Name = "_protectedReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject ProtectedReadonlySelfTypeField { get; } + + [Duck(Name = "_privateReadonlySelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PrivateReadonlySelfTypeField { get; } + + // * + + [Duck(Name = "_publicSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PublicSelfTypeField { get; set; } + + [Duck(Name = "_internalSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject InternalSelfTypeField { get; set; } + + [Duck(Name = "_protectedSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject ProtectedSelfTypeField { get; set; } + + [Duck(Name = "_privateSelfTypeField", Kind = DuckKind.Field)] + public abstract IDummyFieldObject PrivateSelfTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs new file mode 100644 index 000000000..abe8c3ca4 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining.ProxiesDefinitions +{ + public class ObscureDuckTypeVirtualClass + { + [Duck(Name = "_publicStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PublicStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_internalStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject InternalStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject ProtectedStaticReadonlySelfTypeField { get; } + + [Duck(Name = "_privateStaticReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PrivateStaticReadonlySelfTypeField { get; } + + // * + + [Duck(Name = "_publicStaticSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PublicStaticSelfTypeField { get; set; } + + [Duck(Name = "_internalStaticSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject InternalStaticSelfTypeField { get; set; } + + [Duck(Name = "_protectedStaticSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject ProtectedStaticSelfTypeField { get; set; } + + [Duck(Name = "_privateStaticSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PrivateStaticSelfTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PublicReadonlySelfTypeField { get; } + + [Duck(Name = "_internalReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject InternalReadonlySelfTypeField { get; } + + [Duck(Name = "_protectedReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject ProtectedReadonlySelfTypeField { get; } + + [Duck(Name = "_privateReadonlySelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PrivateReadonlySelfTypeField { get; } + + // * + + [Duck(Name = "_publicSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PublicSelfTypeField { get; set; } + + [Duck(Name = "_internalSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject InternalSelfTypeField { get; set; } + + [Duck(Name = "_protectedSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject ProtectedSelfTypeField { get; set; } + + [Duck(Name = "_privateSelfTypeField", Kind = DuckKind.Field)] + public virtual IDummyFieldObject PrivateSelfTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/TypeChainingFieldTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/TypeChainingFieldTests.cs new file mode 100644 index 000000000..d682cf08d --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/TypeChaining/TypeChainingFieldTests.cs @@ -0,0 +1,236 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining.ProxiesDefinitions; +using Xunit; + +#pragma warning disable SA1201 // Elements must appear in the correct order + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.TypeChaining +{ + public class TypeChainingFieldTests + { + public static IEnumerable Data() + { + return new[] + { + new object[] { ObscureObject.GetFieldPublicObject() }, + new object[] { ObscureObject.GetFieldInternalObject() }, + new object[] { ObscureObject.GetFieldPrivateObject() }, + }; + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticReadonlyFieldsSetException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void ReadonlyFieldsSetException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticReadonlyFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + + Assert.Equal(42, duckInterface.PublicStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.PublicStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.PublicStaticReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PublicStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PublicStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PublicStaticReadonlySelfTypeField).Instance); + + // * + + Assert.Equal(42, duckInterface.InternalStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.InternalStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.InternalStaticReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.InternalStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.InternalStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.InternalStaticReadonlySelfTypeField).Instance); + + // * + + Assert.Equal(42, duckInterface.ProtectedStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.ProtectedStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.ProtectedStaticReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.ProtectedStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.ProtectedStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.ProtectedStaticReadonlySelfTypeField).Instance); + + // * + + Assert.Equal(42, duckInterface.PrivateStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.PrivateStaticReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.PrivateStaticReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PrivateStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PrivateStaticReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PrivateStaticReadonlySelfTypeField).Instance); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + IDummyFieldObject newDummy = null; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.PublicStaticSelfTypeField = newDummy; + + Assert.Equal(42, duckInterface.PublicStaticSelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.PublicStaticSelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.PublicStaticSelfTypeField.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 52 }).DuckCast(); + duckInterface.InternalStaticSelfTypeField = newDummy; + + Assert.Equal(52, duckInterface.InternalStaticSelfTypeField.MagicNumber); + Assert.Equal(52, duckAbstract.InternalStaticSelfTypeField.MagicNumber); + Assert.Equal(52, duckVirtual.InternalStaticSelfTypeField.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 62 }).DuckCast(); + duckAbstract.ProtectedStaticSelfTypeField = newDummy; + + Assert.Equal(62, duckInterface.ProtectedStaticSelfTypeField.MagicNumber); + Assert.Equal(62, duckAbstract.ProtectedStaticSelfTypeField.MagicNumber); + Assert.Equal(62, duckVirtual.ProtectedStaticSelfTypeField.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 72 }).DuckCast(); + duckAbstract.PrivateStaticSelfTypeField = newDummy; + + Assert.Equal(72, duckInterface.PrivateStaticSelfTypeField.MagicNumber); + Assert.Equal(72, duckAbstract.PrivateStaticSelfTypeField.MagicNumber); + Assert.Equal(72, duckVirtual.PrivateStaticSelfTypeField.MagicNumber); + } + + [Theory] + [MemberData(nameof(Data))] + public void ReadonlyFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + + Assert.Equal(42, duckInterface.PublicReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.PublicReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.PublicReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PublicReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PublicReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PublicReadonlySelfTypeField).Instance); + + // * + + Assert.Equal(42, duckInterface.InternalReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.InternalReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.InternalReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.InternalReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.InternalReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.InternalReadonlySelfTypeField).Instance); + + // * + + Assert.Equal(42, duckInterface.ProtectedReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.ProtectedReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.ProtectedReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.ProtectedReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.ProtectedReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.ProtectedReadonlySelfTypeField).Instance); + + // * + + Assert.Equal(42, duckInterface.PrivateReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.PrivateReadonlySelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.PrivateReadonlySelfTypeField.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PrivateReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PrivateReadonlySelfTypeField).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PrivateReadonlySelfTypeField).Instance); + } + + [Theory] + [MemberData(nameof(Data))] + public void Fields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + IDummyFieldObject newDummy = null; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.PublicSelfTypeField = newDummy; + + Assert.Equal(42, duckInterface.PublicSelfTypeField.MagicNumber); + Assert.Equal(42, duckAbstract.PublicSelfTypeField.MagicNumber); + Assert.Equal(42, duckVirtual.PublicSelfTypeField.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 52 }).DuckCast(); + duckInterface.InternalSelfTypeField = newDummy; + + Assert.Equal(52, duckInterface.InternalSelfTypeField.MagicNumber); + Assert.Equal(52, duckAbstract.InternalSelfTypeField.MagicNumber); + Assert.Equal(52, duckVirtual.InternalSelfTypeField.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 62 }).DuckCast(); + duckInterface.ProtectedSelfTypeField = newDummy; + + Assert.Equal(62, duckInterface.ProtectedSelfTypeField.MagicNumber); + Assert.Equal(62, duckAbstract.ProtectedSelfTypeField.MagicNumber); + Assert.Equal(62, duckVirtual.ProtectedSelfTypeField.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 72 }).DuckCast(); + duckInterface.PrivateSelfTypeField = newDummy; + + Assert.Equal(72, duckInterface.PrivateSelfTypeField.MagicNumber); + Assert.Equal(72, duckAbstract.PrivateSelfTypeField.MagicNumber); + Assert.Equal(72, duckVirtual.PrivateSelfTypeField.MagicNumber); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureDuckType.cs new file mode 100644 index 000000000..e390db186 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureDuckType.cs @@ -0,0 +1,85 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ValueType.ProxiesDefinitions +{ + public interface IObscureDuckType + { + [Duck(Name = "_publicStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + int PublicStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_internalStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + int InternalStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + int ProtectedStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_privateStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + int PrivateStaticReadonlyValueTypeField { get; } + + // * + + [Duck(Name = "_publicStaticValueTypeField", Kind = DuckKind.Field)] + int PublicStaticValueTypeField { get; set; } + + [Duck(Name = "_internalStaticValueTypeField", Kind = DuckKind.Field)] + int InternalStaticValueTypeField { get; set; } + + [Duck(Name = "_protectedStaticValueTypeField", Kind = DuckKind.Field)] + int ProtectedStaticValueTypeField { get; set; } + + [Duck(Name = "_privateStaticValueTypeField", Kind = DuckKind.Field)] + int PrivateStaticValueTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlyValueTypeField", Kind = DuckKind.Field)] + int PublicReadonlyValueTypeField { get; } + + [Duck(Name = "_internalReadonlyValueTypeField", Kind = DuckKind.Field)] + int InternalReadonlyValueTypeField { get; } + + [Duck(Name = "_protectedReadonlyValueTypeField", Kind = DuckKind.Field)] + int ProtectedReadonlyValueTypeField { get; } + + [Duck(Name = "_privateReadonlyValueTypeField", Kind = DuckKind.Field)] + int PrivateReadonlyValueTypeField { get; } + + // * + + [Duck(Name = "_publicValueTypeField", Kind = DuckKind.Field)] + int PublicValueTypeField { get; set; } + + [Duck(Name = "_internalValueTypeField", Kind = DuckKind.Field)] + int InternalValueTypeField { get; set; } + + [Duck(Name = "_protectedValueTypeField", Kind = DuckKind.Field)] + int ProtectedValueTypeField { get; set; } + + [Duck(Name = "_privateValueTypeField", Kind = DuckKind.Field)] + int PrivateValueTypeField { get; set; } + + // * + + [Duck(Name = "_publicStaticNullableIntField", Kind = DuckKind.Field)] + int? PublicStaticNullableIntField { get; set; } + + [Duck(Name = "_privateStaticNullableIntField", Kind = DuckKind.Field)] + int? PrivateStaticNullableIntField { get; set; } + + [Duck(Name = "_publicNullableIntField", Kind = DuckKind.Field)] + int? PublicNullableIntField { get; set; } + + [Duck(Name = "_privateNullableIntField", Kind = DuckKind.Field)] + int? PrivateNullableIntField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs new file mode 100644 index 000000000..e951f1519 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureReadonlyErrorDuckType.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ValueType.ProxiesDefinitions +{ + public interface IObscureReadonlyErrorDuckType + { + [Duck(Name = "_publicReadonlyValueTypeField", Kind = DuckKind.Field)] + int PublicReadonlyValueTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs new file mode 100644 index 000000000..e7f03d90b --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/IObscureStaticReadonlyErrorDuckType.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ValueType.ProxiesDefinitions +{ + public interface IObscureStaticReadonlyErrorDuckType + { + [Duck(Name = "_publicStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + int PublicStaticReadonlyValueTypeField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs new file mode 100644 index 000000000..0b36232c4 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs @@ -0,0 +1,85 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ValueType.ProxiesDefinitions +{ + public abstract class ObscureDuckTypeAbstractClass + { + [Duck(Name = "_publicStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int PublicStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_internalStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int InternalStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int ProtectedStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_privateStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int PrivateStaticReadonlyValueTypeField { get; } + + // * + + [Duck(Name = "_publicStaticValueTypeField", Kind = DuckKind.Field)] + public abstract int PublicStaticValueTypeField { get; set; } + + [Duck(Name = "_internalStaticValueTypeField", Kind = DuckKind.Field)] + public abstract int InternalStaticValueTypeField { get; set; } + + [Duck(Name = "_protectedStaticValueTypeField", Kind = DuckKind.Field)] + public abstract int ProtectedStaticValueTypeField { get; set; } + + [Duck(Name = "_privateStaticValueTypeField", Kind = DuckKind.Field)] + public abstract int PrivateStaticValueTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int PublicReadonlyValueTypeField { get; } + + [Duck(Name = "_internalReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int InternalReadonlyValueTypeField { get; } + + [Duck(Name = "_protectedReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int ProtectedReadonlyValueTypeField { get; } + + [Duck(Name = "_privateReadonlyValueTypeField", Kind = DuckKind.Field)] + public abstract int PrivateReadonlyValueTypeField { get; } + + // * + + [Duck(Name = "_publicValueTypeField", Kind = DuckKind.Field)] + public abstract int PublicValueTypeField { get; set; } + + [Duck(Name = "_internalValueTypeField", Kind = DuckKind.Field)] + public abstract int InternalValueTypeField { get; set; } + + [Duck(Name = "_protectedValueTypeField", Kind = DuckKind.Field)] + public abstract int ProtectedValueTypeField { get; set; } + + [Duck(Name = "_privateValueTypeField", Kind = DuckKind.Field)] + public abstract int PrivateValueTypeField { get; set; } + + // * + + [Duck(Name = "_publicStaticNullableIntField", Kind = DuckKind.Field)] + public abstract int? PublicStaticNullableIntField { get; set; } + + [Duck(Name = "_privateStaticNullableIntField", Kind = DuckKind.Field)] + public abstract int? PrivateStaticNullableIntField { get; set; } + + [Duck(Name = "_publicNullableIntField", Kind = DuckKind.Field)] + public abstract int? PublicNullableIntField { get; set; } + + [Duck(Name = "_privateNullableIntField", Kind = DuckKind.Field)] + public abstract int? PrivateNullableIntField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs new file mode 100644 index 000000000..ce87d35b8 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs @@ -0,0 +1,85 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ValueType.ProxiesDefinitions +{ + public class ObscureDuckTypeVirtualClass + { + [Duck(Name = "_publicStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int PublicStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_internalStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int InternalStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_protectedStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int ProtectedStaticReadonlyValueTypeField { get; } + + [Duck(Name = "_privateStaticReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int PrivateStaticReadonlyValueTypeField { get; } + + // * + + [Duck(Name = "_publicStaticValueTypeField", Kind = DuckKind.Field)] + public virtual int PublicStaticValueTypeField { get; set; } + + [Duck(Name = "_internalStaticValueTypeField", Kind = DuckKind.Field)] + public virtual int InternalStaticValueTypeField { get; set; } + + [Duck(Name = "_protectedStaticValueTypeField", Kind = DuckKind.Field)] + public virtual int ProtectedStaticValueTypeField { get; set; } + + [Duck(Name = "_privateStaticValueTypeField", Kind = DuckKind.Field)] + public virtual int PrivateStaticValueTypeField { get; set; } + + // * + + [Duck(Name = "_publicReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int PublicReadonlyValueTypeField { get; } + + [Duck(Name = "_internalReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int InternalReadonlyValueTypeField { get; } + + [Duck(Name = "_protectedReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int ProtectedReadonlyValueTypeField { get; } + + [Duck(Name = "_privateReadonlyValueTypeField", Kind = DuckKind.Field)] + public virtual int PrivateReadonlyValueTypeField { get; } + + // * + + [Duck(Name = "_publicValueTypeField", Kind = DuckKind.Field)] + public virtual int PublicValueTypeField { get; set; } + + [Duck(Name = "_internalValueTypeField", Kind = DuckKind.Field)] + public virtual int InternalValueTypeField { get; set; } + + [Duck(Name = "_protectedValueTypeField", Kind = DuckKind.Field)] + public virtual int ProtectedValueTypeField { get; set; } + + [Duck(Name = "_privateValueTypeField", Kind = DuckKind.Field)] + public virtual int PrivateValueTypeField { get; set; } + + // * + + [Duck(Name = "_publicStaticNullableIntField", Kind = DuckKind.Field)] + public virtual int? PublicStaticNullableIntField { get; set; } + + [Duck(Name = "_privateStaticNullableIntField", Kind = DuckKind.Field)] + public virtual int? PrivateStaticNullableIntField { get; set; } + + [Duck(Name = "_publicNullableIntField", Kind = DuckKind.Field)] + public virtual int? PublicNullableIntField { get; set; } + + [Duck(Name = "_privateNullableIntField", Kind = DuckKind.Field)] + public virtual int? PrivateNullableIntField { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ValueTypeFieldTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ValueTypeFieldTests.cs new file mode 100644 index 000000000..86e6d7e50 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Fields/ValueType/ValueTypeFieldTests.cs @@ -0,0 +1,381 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ValueType.ProxiesDefinitions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Fields.ValueType +{ + public class ValueTypeFieldTests + { + public static IEnumerable Data() + { + return new[] + { + new object[] { ObscureObject.GetFieldPublicObject() }, + new object[] { ObscureObject.GetFieldInternalObject() }, + new object[] { ObscureObject.GetFieldPrivateObject() }, + }; + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticReadonlyFieldsSetException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void ReadonlyFieldsSetException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticReadonlyFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal(10, duckInterface.PublicStaticReadonlyValueTypeField); + Assert.Equal(10, duckAbstract.PublicStaticReadonlyValueTypeField); + Assert.Equal(10, duckVirtual.PublicStaticReadonlyValueTypeField); + + // * + Assert.Equal(11, duckInterface.InternalStaticReadonlyValueTypeField); + Assert.Equal(11, duckAbstract.InternalStaticReadonlyValueTypeField); + Assert.Equal(11, duckVirtual.InternalStaticReadonlyValueTypeField); + + // * + Assert.Equal(12, duckInterface.ProtectedStaticReadonlyValueTypeField); + Assert.Equal(12, duckAbstract.ProtectedStaticReadonlyValueTypeField); + Assert.Equal(12, duckVirtual.ProtectedStaticReadonlyValueTypeField); + + // * + Assert.Equal(13, duckInterface.PrivateStaticReadonlyValueTypeField); + Assert.Equal(13, duckAbstract.PrivateStaticReadonlyValueTypeField); + Assert.Equal(13, duckVirtual.PrivateStaticReadonlyValueTypeField); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal(20, duckInterface.PublicStaticValueTypeField); + Assert.Equal(20, duckAbstract.PublicStaticValueTypeField); + Assert.Equal(20, duckVirtual.PublicStaticValueTypeField); + + duckInterface.PublicStaticValueTypeField = 42; + Assert.Equal(42, duckInterface.PublicStaticValueTypeField); + Assert.Equal(42, duckAbstract.PublicStaticValueTypeField); + Assert.Equal(42, duckVirtual.PublicStaticValueTypeField); + + duckAbstract.PublicStaticValueTypeField = 50; + Assert.Equal(50, duckInterface.PublicStaticValueTypeField); + Assert.Equal(50, duckAbstract.PublicStaticValueTypeField); + Assert.Equal(50, duckVirtual.PublicStaticValueTypeField); + + duckVirtual.PublicStaticValueTypeField = 60; + Assert.Equal(60, duckInterface.PublicStaticValueTypeField); + Assert.Equal(60, duckAbstract.PublicStaticValueTypeField); + Assert.Equal(60, duckVirtual.PublicStaticValueTypeField); + + // * + + Assert.Equal(21, duckInterface.InternalStaticValueTypeField); + Assert.Equal(21, duckAbstract.InternalStaticValueTypeField); + Assert.Equal(21, duckVirtual.InternalStaticValueTypeField); + + duckInterface.InternalStaticValueTypeField = 42; + Assert.Equal(42, duckInterface.InternalStaticValueTypeField); + Assert.Equal(42, duckAbstract.InternalStaticValueTypeField); + Assert.Equal(42, duckVirtual.InternalStaticValueTypeField); + + duckAbstract.InternalStaticValueTypeField = 50; + Assert.Equal(50, duckInterface.InternalStaticValueTypeField); + Assert.Equal(50, duckAbstract.InternalStaticValueTypeField); + Assert.Equal(50, duckVirtual.InternalStaticValueTypeField); + + duckVirtual.InternalStaticValueTypeField = 60; + Assert.Equal(60, duckInterface.InternalStaticValueTypeField); + Assert.Equal(60, duckAbstract.InternalStaticValueTypeField); + Assert.Equal(60, duckVirtual.InternalStaticValueTypeField); + + // * + + Assert.Equal(22, duckInterface.ProtectedStaticValueTypeField); + Assert.Equal(22, duckAbstract.ProtectedStaticValueTypeField); + Assert.Equal(22, duckVirtual.ProtectedStaticValueTypeField); + + duckInterface.ProtectedStaticValueTypeField = 42; + Assert.Equal(42, duckInterface.ProtectedStaticValueTypeField); + Assert.Equal(42, duckAbstract.ProtectedStaticValueTypeField); + Assert.Equal(42, duckVirtual.ProtectedStaticValueTypeField); + + duckAbstract.ProtectedStaticValueTypeField = 50; + Assert.Equal(50, duckInterface.ProtectedStaticValueTypeField); + Assert.Equal(50, duckAbstract.ProtectedStaticValueTypeField); + Assert.Equal(50, duckVirtual.ProtectedStaticValueTypeField); + + duckVirtual.ProtectedStaticValueTypeField = 60; + Assert.Equal(60, duckInterface.ProtectedStaticValueTypeField); + Assert.Equal(60, duckAbstract.ProtectedStaticValueTypeField); + Assert.Equal(60, duckVirtual.ProtectedStaticValueTypeField); + + // * + + Assert.Equal(23, duckInterface.PrivateStaticValueTypeField); + Assert.Equal(23, duckAbstract.PrivateStaticValueTypeField); + Assert.Equal(23, duckVirtual.PrivateStaticValueTypeField); + + duckInterface.PrivateStaticValueTypeField = 42; + Assert.Equal(42, duckInterface.PrivateStaticValueTypeField); + Assert.Equal(42, duckAbstract.PrivateStaticValueTypeField); + Assert.Equal(42, duckVirtual.PrivateStaticValueTypeField); + + duckAbstract.PrivateStaticValueTypeField = 50; + Assert.Equal(50, duckInterface.PrivateStaticValueTypeField); + Assert.Equal(50, duckAbstract.PrivateStaticValueTypeField); + Assert.Equal(50, duckVirtual.PrivateStaticValueTypeField); + + duckVirtual.PrivateStaticValueTypeField = 60; + Assert.Equal(60, duckInterface.PrivateStaticValueTypeField); + Assert.Equal(60, duckAbstract.PrivateStaticValueTypeField); + Assert.Equal(60, duckVirtual.PrivateStaticValueTypeField); + } + + [Theory] + [MemberData(nameof(Data))] + public void ReadonlyFields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal(30, duckInterface.PublicReadonlyValueTypeField); + Assert.Equal(30, duckAbstract.PublicReadonlyValueTypeField); + Assert.Equal(30, duckVirtual.PublicReadonlyValueTypeField); + + // * + Assert.Equal(31, duckInterface.InternalReadonlyValueTypeField); + Assert.Equal(31, duckAbstract.InternalReadonlyValueTypeField); + Assert.Equal(31, duckVirtual.InternalReadonlyValueTypeField); + + // * + Assert.Equal(32, duckInterface.ProtectedReadonlyValueTypeField); + Assert.Equal(32, duckAbstract.ProtectedReadonlyValueTypeField); + Assert.Equal(32, duckVirtual.ProtectedReadonlyValueTypeField); + + // * + Assert.Equal(33, duckInterface.PrivateReadonlyValueTypeField); + Assert.Equal(33, duckAbstract.PrivateReadonlyValueTypeField); + Assert.Equal(33, duckVirtual.PrivateReadonlyValueTypeField); + } + + [Theory] + [MemberData(nameof(Data))] + public void Fields(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal(40, duckInterface.PublicValueTypeField); + Assert.Equal(40, duckAbstract.PublicValueTypeField); + Assert.Equal(40, duckVirtual.PublicValueTypeField); + + duckInterface.PublicValueTypeField = 42; + Assert.Equal(42, duckInterface.PublicValueTypeField); + Assert.Equal(42, duckAbstract.PublicValueTypeField); + Assert.Equal(42, duckVirtual.PublicValueTypeField); + + duckAbstract.PublicValueTypeField = 50; + Assert.Equal(50, duckInterface.PublicValueTypeField); + Assert.Equal(50, duckAbstract.PublicValueTypeField); + Assert.Equal(50, duckVirtual.PublicValueTypeField); + + duckVirtual.PublicValueTypeField = 60; + Assert.Equal(60, duckInterface.PublicValueTypeField); + Assert.Equal(60, duckAbstract.PublicValueTypeField); + Assert.Equal(60, duckVirtual.PublicValueTypeField); + + // * + + Assert.Equal(41, duckInterface.InternalValueTypeField); + Assert.Equal(41, duckAbstract.InternalValueTypeField); + Assert.Equal(41, duckVirtual.InternalValueTypeField); + + duckInterface.InternalValueTypeField = 42; + Assert.Equal(42, duckInterface.InternalValueTypeField); + Assert.Equal(42, duckAbstract.InternalValueTypeField); + Assert.Equal(42, duckVirtual.InternalValueTypeField); + + duckAbstract.InternalValueTypeField = 50; + Assert.Equal(50, duckInterface.InternalValueTypeField); + Assert.Equal(50, duckAbstract.InternalValueTypeField); + Assert.Equal(50, duckVirtual.InternalValueTypeField); + + duckVirtual.InternalValueTypeField = 60; + Assert.Equal(60, duckInterface.InternalValueTypeField); + Assert.Equal(60, duckAbstract.InternalValueTypeField); + Assert.Equal(60, duckVirtual.InternalValueTypeField); + + // * + + Assert.Equal(42, duckInterface.ProtectedValueTypeField); + Assert.Equal(42, duckAbstract.ProtectedValueTypeField); + Assert.Equal(42, duckVirtual.ProtectedValueTypeField); + + duckInterface.ProtectedValueTypeField = 45; + Assert.Equal(45, duckInterface.ProtectedValueTypeField); + Assert.Equal(45, duckAbstract.ProtectedValueTypeField); + Assert.Equal(45, duckVirtual.ProtectedValueTypeField); + + duckAbstract.ProtectedValueTypeField = 50; + Assert.Equal(50, duckInterface.ProtectedValueTypeField); + Assert.Equal(50, duckAbstract.ProtectedValueTypeField); + Assert.Equal(50, duckVirtual.ProtectedValueTypeField); + + duckVirtual.ProtectedValueTypeField = 60; + Assert.Equal(60, duckInterface.ProtectedValueTypeField); + Assert.Equal(60, duckAbstract.ProtectedValueTypeField); + Assert.Equal(60, duckVirtual.ProtectedValueTypeField); + + // * + + Assert.Equal(43, duckInterface.PrivateValueTypeField); + Assert.Equal(43, duckAbstract.PrivateValueTypeField); + Assert.Equal(43, duckVirtual.PrivateValueTypeField); + + duckInterface.PrivateValueTypeField = 42; + Assert.Equal(42, duckInterface.PrivateValueTypeField); + Assert.Equal(42, duckAbstract.PrivateValueTypeField); + Assert.Equal(42, duckVirtual.PrivateValueTypeField); + + duckAbstract.PrivateValueTypeField = 50; + Assert.Equal(50, duckInterface.PrivateValueTypeField); + Assert.Equal(50, duckAbstract.PrivateValueTypeField); + Assert.Equal(50, duckVirtual.PrivateValueTypeField); + + duckVirtual.PrivateValueTypeField = 60; + Assert.Equal(60, duckInterface.PrivateValueTypeField); + Assert.Equal(60, duckAbstract.PrivateValueTypeField); + Assert.Equal(60, duckVirtual.PrivateValueTypeField); + } + + [Theory] + [MemberData(nameof(Data))] + public void NullableOfKnown(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Null(duckInterface.PublicStaticNullableIntField); + Assert.Null(duckAbstract.PublicStaticNullableIntField); + Assert.Null(duckVirtual.PublicStaticNullableIntField); + + duckInterface.PublicStaticNullableIntField = 42; + Assert.Equal(42, duckInterface.PublicStaticNullableIntField); + Assert.Equal(42, duckAbstract.PublicStaticNullableIntField); + Assert.Equal(42, duckVirtual.PublicStaticNullableIntField); + + duckAbstract.PublicStaticNullableIntField = 50; + Assert.Equal(50, duckInterface.PublicStaticNullableIntField); + Assert.Equal(50, duckAbstract.PublicStaticNullableIntField); + Assert.Equal(50, duckVirtual.PublicStaticNullableIntField); + + duckVirtual.PublicStaticNullableIntField = null; + Assert.Null(duckInterface.PublicStaticNullableIntField); + Assert.Null(duckAbstract.PublicStaticNullableIntField); + Assert.Null(duckVirtual.PublicStaticNullableIntField); + + // * + + Assert.Null(duckInterface.PrivateStaticNullableIntField); + Assert.Null(duckAbstract.PrivateStaticNullableIntField); + Assert.Null(duckVirtual.PrivateStaticNullableIntField); + + duckInterface.PrivateStaticNullableIntField = 42; + Assert.Equal(42, duckInterface.PrivateStaticNullableIntField); + Assert.Equal(42, duckAbstract.PrivateStaticNullableIntField); + Assert.Equal(42, duckVirtual.PrivateStaticNullableIntField); + + duckAbstract.PrivateStaticNullableIntField = 50; + Assert.Equal(50, duckInterface.PrivateStaticNullableIntField); + Assert.Equal(50, duckAbstract.PrivateStaticNullableIntField); + Assert.Equal(50, duckVirtual.PrivateStaticNullableIntField); + + duckVirtual.PrivateStaticNullableIntField = null; + Assert.Null(duckInterface.PrivateStaticNullableIntField); + Assert.Null(duckAbstract.PrivateStaticNullableIntField); + Assert.Null(duckVirtual.PrivateStaticNullableIntField); + + // * + + Assert.Null(duckInterface.PublicNullableIntField); + Assert.Null(duckAbstract.PublicNullableIntField); + Assert.Null(duckVirtual.PublicNullableIntField); + + duckInterface.PublicNullableIntField = 42; + Assert.Equal(42, duckInterface.PublicNullableIntField); + Assert.Equal(42, duckAbstract.PublicNullableIntField); + Assert.Equal(42, duckVirtual.PublicNullableIntField); + + duckAbstract.PublicNullableIntField = 50; + Assert.Equal(50, duckInterface.PublicNullableIntField); + Assert.Equal(50, duckAbstract.PublicNullableIntField); + Assert.Equal(50, duckVirtual.PublicNullableIntField); + + duckVirtual.PublicNullableIntField = null; + Assert.Null(duckInterface.PublicNullableIntField); + Assert.Null(duckAbstract.PublicNullableIntField); + Assert.Null(duckVirtual.PublicNullableIntField); + + // * + + Assert.Null(duckInterface.PrivateNullableIntField); + Assert.Null(duckAbstract.PrivateNullableIntField); + Assert.Null(duckVirtual.PrivateNullableIntField); + + duckInterface.PrivateNullableIntField = 42; + Assert.Equal(42, duckInterface.PrivateNullableIntField); + Assert.Equal(42, duckAbstract.PrivateNullableIntField); + Assert.Equal(42, duckVirtual.PrivateNullableIntField); + + duckAbstract.PrivateNullableIntField = 50; + Assert.Equal(50, duckInterface.PrivateNullableIntField); + Assert.Equal(50, duckAbstract.PrivateNullableIntField); + Assert.Equal(50, duckVirtual.PrivateNullableIntField); + + duckVirtual.PrivateNullableIntField = null; + Assert.Null(duckInterface.PrivateNullableIntField); + Assert.Null(duckAbstract.PrivateNullableIntField); + Assert.Null(duckVirtual.PrivateNullableIntField); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/MethodTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/MethodTests.cs new file mode 100644 index 000000000..efd8849a7 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/MethodTests.cs @@ -0,0 +1,388 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods +{ + public class MethodTests + { + public static IEnumerable Data() + { + return new[] + { + new object[] { ObscureObject.GetPropertyPublicObject() }, + new object[] { ObscureObject.GetPropertyInternalObject() }, + new object[] { ObscureObject.GetPropertyPrivateObject() }, + }; + } + + [Theory] + [MemberData(nameof(Data))] + public void ReturnMethods(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // Integers + Assert.Equal(20, duckInterface.Sum(10, 10)); + Assert.Equal(20, duckAbstract.Sum(10, 10)); + Assert.Equal(20, duckVirtual.Sum(10, 10)); + + // Float + Assert.Equal(20f, duckInterface.Sum(10f, 10f)); + Assert.Equal(20f, duckAbstract.Sum(10f, 10f)); + Assert.Equal(20f, duckVirtual.Sum(10f, 10f)); + + // Double + Assert.Equal(20d, duckInterface.Sum(10d, 10d)); + Assert.Equal(20d, duckAbstract.Sum(10d, 10d)); + Assert.Equal(20d, duckVirtual.Sum(10d, 10d)); + + // Short + Assert.Equal((short)20, duckInterface.Sum((short)10, (short)10)); + Assert.Equal((short)20, duckAbstract.Sum((short)10, (short)10)); + Assert.Equal((short)20, duckVirtual.Sum((short)10, (short)10)); + + // Enum + Assert.Equal(TestEnum2.Segundo, duckInterface.ShowEnum(TestEnum2.Segundo)); + Assert.Equal(TestEnum2.Segundo, duckAbstract.ShowEnum(TestEnum2.Segundo)); + Assert.Equal(TestEnum2.Segundo, duckVirtual.ShowEnum(TestEnum2.Segundo)); + + // Internal Sum + Assert.Equal(20, duckInterface.InternalSum(10, 10)); + Assert.Equal(20, duckAbstract.InternalSum(10, 10)); + Assert.Equal(20, duckVirtual.InternalSum(10, 10)); + + var dummy = new ObscureObject.DummyFieldObject { MagicNumber = 987654 }; + var dummyInt = dummy.DuckCast(); + Assert.Equal(dummy.MagicNumber, duckInterface.Bypass(dummyInt).MagicNumber); + Assert.Equal(dummy.MagicNumber, duckAbstract.Bypass(dummyInt).MagicNumber); + Assert.Equal(dummy.MagicNumber, duckVirtual.Bypass(dummyInt).MagicNumber); + } + + [Theory] + [MemberData(nameof(Data))] + public void VoidMethods(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // Void with object + duckInterface.Add("Key01", new ObscureObject.DummyFieldObject()); + duckAbstract.Add("Key02", new ObscureObject.DummyFieldObject()); + duckVirtual.Add("Key03", new ObscureObject.DummyFieldObject()); + + // Void with int + duckInterface.Add("KeyInt01", 42); + duckAbstract.Add("KeyInt02", 42); + duckVirtual.Add("KeyInt03", 42); + + // Void with string + duckInterface.Add("KeyString01", "Value01"); + duckAbstract.Add("KeyString02", "Value02"); + duckVirtual.Add("KeyString03", "Value03"); + } + + [Theory] + [MemberData(nameof(Data))] + public void RefParametersMethods(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // Ref parameter + int value = 4; + duckInterface.Pow2(ref value); + duckAbstract.Pow2(ref value); + duckVirtual.Pow2(ref value); + Assert.Equal(65536, value); + + value = 4; + duckInterface.GetReference(ref value); + duckAbstract.GetReference(ref value); + duckVirtual.GetReference(ref value); + Assert.Equal(65536, value); + + // Ref object parameter + object objValue = 4; + object objValue2 = objValue; + duckInterface.GetReferenceObject(ref objValue); + duckAbstract.GetReferenceObject(ref objValue); + duckVirtual.GetReferenceObject(ref objValue); + Assert.Equal(65536, (int)objValue); + + // Ref DuckType + IDummyFieldObject refDuckType; + refDuckType = null; + Assert.True(duckInterface.TryGetReference(ref refDuckType)); + Assert.Equal(100, refDuckType.MagicNumber); + Assert.True(duckAbstract.TryGetReference(ref refDuckType)); + Assert.Equal(101, refDuckType.MagicNumber); + Assert.True(duckVirtual.TryGetReference(ref refDuckType)); + Assert.Equal(102, refDuckType.MagicNumber); + + // Ref object + object refObject; + refObject = null; + Assert.True(duckInterface.TryGetReferenceObject(ref refObject)); + Assert.Equal(100, refObject.DuckCast().MagicNumber); + Assert.True(duckAbstract.TryGetReferenceObject(ref refObject)); + Assert.Equal(101, refObject.DuckCast().MagicNumber); + Assert.True(duckVirtual.TryGetReferenceObject(ref refObject)); + Assert.Equal(102, refObject.DuckCast().MagicNumber); + + // Private internal parameter type with duck type output + refDuckType = null; + Assert.True(duckInterface.TryGetPrivateReference(ref refDuckType)); + Assert.Equal(100, refDuckType.MagicNumber); + Assert.True(duckAbstract.TryGetPrivateReference(ref refDuckType)); + Assert.Equal(101, refDuckType.MagicNumber); + Assert.True(duckVirtual.TryGetPrivateReference(ref refDuckType)); + Assert.Equal(102, refDuckType.MagicNumber); + + // Private internal parameter type object output + refObject = null; + Assert.True(duckInterface.TryGetPrivateReferenceObject(ref refObject)); + Assert.Equal(100, refObject.DuckCast().MagicNumber); + Assert.True(duckAbstract.TryGetPrivateReferenceObject(ref refObject)); + Assert.Equal(101, refObject.DuckCast().MagicNumber); + Assert.True(duckVirtual.TryGetPrivateReferenceObject(ref refObject)); + Assert.Equal(102, refObject.DuckCast().MagicNumber); + } + + [Theory] + [MemberData(nameof(Data))] + public void OutParametersMethods(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // Out parameter + int outValue; + duckInterface.GetOutput(out outValue); + Assert.Equal(42, outValue); + duckAbstract.GetOutput(out outValue); + Assert.Equal(42, outValue); + duckVirtual.GetOutput(out outValue); + Assert.Equal(42, outValue); + + // Out object parameter + object outObjectValue; + duckInterface.GetOutputObject(out outObjectValue); + Assert.Equal(42, (int)outObjectValue); + duckAbstract.GetOutputObject(out outObjectValue); + Assert.Equal(42, (int)outObjectValue); + duckVirtual.GetOutputObject(out outObjectValue); + Assert.Equal(42, (int)outObjectValue); + + // Duck type output + IDummyFieldObject outDuckType; + Assert.True(duckInterface.TryGetObscure(out outDuckType)); + Assert.NotNull(outDuckType); + Assert.Equal(99, outDuckType.MagicNumber); + + Assert.True(duckAbstract.TryGetObscure(out outDuckType)); + Assert.NotNull(outDuckType); + Assert.Equal(99, outDuckType.MagicNumber); + + Assert.True(duckVirtual.TryGetObscure(out outDuckType)); + Assert.NotNull(outDuckType); + Assert.Equal(99, outDuckType.MagicNumber); + + // Object output + object outObject; + Assert.True(duckInterface.TryGetObscureObject(out outObject)); + Assert.NotNull(outObject); + Assert.Equal(99, outObject.DuckCast().MagicNumber); + + Assert.True(duckAbstract.TryGetObscureObject(out outObject)); + Assert.NotNull(outObject); + Assert.Equal(99, outObject.DuckCast().MagicNumber); + + Assert.True(duckVirtual.TryGetObscureObject(out outObject)); + Assert.NotNull(outObject); + Assert.Equal(99, outObject.DuckCast().MagicNumber); + + // Private internal parameter type with duck type output + Assert.True(duckInterface.TryGetPrivateObscure(out outDuckType)); + Assert.NotNull(outDuckType); + Assert.Equal(99, outDuckType.MagicNumber); + + Assert.True(duckAbstract.TryGetPrivateObscure(out outDuckType)); + Assert.NotNull(outDuckType); + Assert.Equal(99, outDuckType.MagicNumber); + + Assert.True(duckVirtual.TryGetPrivateObscure(out outDuckType)); + Assert.NotNull(outDuckType); + Assert.Equal(99, outDuckType.MagicNumber); + + // Private internal parameter type object output + Assert.True(duckInterface.TryGetPrivateObscureObject(out outObject)); + Assert.NotNull(outObject); + Assert.Equal(99, outObject.DuckCast().MagicNumber); + + Assert.True(duckAbstract.TryGetPrivateObscureObject(out outObject)); + Assert.NotNull(outObject); + Assert.Equal(99, outObject.DuckCast().MagicNumber); + + Assert.True(duckVirtual.TryGetPrivateObscureObject(out outObject)); + Assert.NotNull(outObject); + Assert.Equal(99, outObject.DuckCast().MagicNumber); + } + + [Fact] + public void DictionaryDuckTypeExample() + { + Dictionary dictionary = new Dictionary(); + + var duckInterface = dictionary.DuckCast(); + + duckInterface.Add("Key01", "Value01"); + duckInterface.Add("Key02", "Value02"); + duckInterface.Add("K", "V"); + + Assert.True(duckInterface.ContainsKey("K")); + if (duckInterface.ContainsKey("K")) + { + Assert.True(duckInterface.Remove("K")); + } + + if (duckInterface.TryGetValue("Key01", out string value)) + { + Assert.Equal("Value01", value); + } + + Assert.Equal("Value02", duckInterface["Key02"]); + + Assert.Equal(2, duckInterface.Count); + + foreach (KeyValuePair val in duckInterface) + { + Assert.NotNull(val.Key); + } + + if (duckInterface.TryGetValueInObject("Key02", out object objValue)) + { + Assert.NotNull(objValue); + } + + if (duckInterface.TryGetValueInDuckChaining("Key02", out IDictioValue dictioValue)) + { + Assert.NotNull(dictioValue); + } + } + + [Theory] + [MemberData(nameof(Data))] + public void DefaultGenericsMethods(object obscureObject) + { +#if NET452 + if (!obscureObject.GetType().IsPublic && !obscureObject.GetType().IsNestedPublic) + { + Assert.Throws( + () => + { + obscureObject.DuckCast(); + }); + Assert.Throws( + () => + { + obscureObject.DuckCast(); + }); + Assert.Throws( + () => + { + obscureObject.DuckCast(); + }); + return; + } +#endif + + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // GetDefault int + Assert.Equal(0, duckInterface.GetDefault()); + Assert.Equal(0, duckAbstract.GetDefault()); + Assert.Equal(0, duckVirtual.GetDefault()); + + // GetDefault double + Assert.Equal(0d, duckInterface.GetDefault()); + Assert.Equal(0d, duckAbstract.GetDefault()); + Assert.Equal(0d, duckVirtual.GetDefault()); + + // GetDefault string + Assert.Null(duckInterface.GetDefault()); + Assert.Null(duckAbstract.GetDefault()); + Assert.Null(duckVirtual.GetDefault()); + + // Wrap ints + Tuple wrapper = duckInterface.Wrap(10, 20); + Assert.Equal(10, wrapper.Item1); + Assert.Equal(20, wrapper.Item2); + + // Wrap string + Tuple wrapper2 = duckAbstract.Wrap("Hello", "World"); + Assert.Equal("Hello", wrapper2.Item1); + Assert.Equal("World", wrapper2.Item2); + + // Wrap object + Tuple wrapper3 = duckAbstract.Wrap(null, "World"); + Assert.Null(wrapper3.Item1); + Assert.Equal("World", wrapper3.Item2); + } + + [Theory] + [MemberData(nameof(Data))] + public void GenericsWithAttributeResolution(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAttribute = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Tuple result; + + Assert.Equal(0, duckInterface.GetDefaultInt()); + Assert.Null(duckInterface.GetDefaultString()); + + result = duckInterface.WrapIntString(42, "All"); + Assert.Equal(42, result.Item1); + Assert.Equal("All", result.Item2); + + // ... + + Assert.Equal(0, duckAttribute.GetDefaultInt()); + Assert.Null(duckAttribute.GetDefaultString()); + + result = duckAttribute.WrapIntString(42, "All"); + Assert.Equal(42, result.Item1); + Assert.Equal("All", result.Item2); + + // ... + + Assert.Equal(0, duckVirtual.GetDefaultInt()); + Assert.Null(duckVirtual.GetDefaultString()); + + result = duckVirtual.WrapIntString(42, "All"); + Assert.Equal(42, result.Item1); + Assert.Equal("All", result.Item2); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/AbstractGenericsWithAttribute.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/AbstractGenericsWithAttribute.cs new file mode 100644 index 000000000..37786e3f0 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/AbstractGenericsWithAttribute.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public abstract class AbstractGenericsWithAttribute + { + [Duck(Name = "GetDefault", GenericParameterTypeNames = new[] { "System.Int32" })] + public abstract int GetDefaultInt(); + + [Duck(Name = "GetDefault", GenericParameterTypeNames = new[] { "System.String" })] + public abstract string GetDefaultString(); + + [Duck(Name = "Wrap", GenericParameterTypeNames = new[] { "System.Int32", "System.String" })] + public abstract Tuple WrapIntString(int a, string b); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/DefaultGenericMethodDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/DefaultGenericMethodDuckTypeAbstractClass.cs new file mode 100644 index 000000000..6accf9ca4 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/DefaultGenericMethodDuckTypeAbstractClass.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public abstract class DefaultGenericMethodDuckTypeAbstractClass + { + public abstract T GetDefault(); + + public abstract Tuple Wrap(T1 a, T2 b); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/DefaultGenericMethodDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/DefaultGenericMethodDuckTypeVirtualClass.cs new file mode 100644 index 000000000..41a034574 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/DefaultGenericMethodDuckTypeVirtualClass.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public class DefaultGenericMethodDuckTypeVirtualClass + { + public virtual T GetDefault() => default; + + public virtual Tuple Wrap(T1 a, T2 b) => null; + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDefaultGenericMethodDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDefaultGenericMethodDuckType.cs new file mode 100644 index 000000000..d02bd6960 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDefaultGenericMethodDuckType.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public interface IDefaultGenericMethodDuckType + { + T GetDefault(); + + Tuple Wrap(T1 a, T2 b); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDictioDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDictioDuckType.cs new file mode 100644 index 000000000..40062e648 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDictioDuckType.cs @@ -0,0 +1,42 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public interface IDictioDuckType + { + public ICollection Keys { get; } + + public ICollection Values { get; } + + int Count { get; } + + public string this[string key] { get; set; } + + void Add(string key, string value); + + bool ContainsKey(string key); + + bool Remove(string key); + + bool TryGetValue(string key, out string value); + + [Duck(Name = "TryGetValue")] + bool TryGetValueInObject(string key, out object value); + + [Duck(Name = "TryGetValue")] + bool TryGetValueInDuckChaining(string key, out IDictioValue value); + + IEnumerator> GetEnumerator(); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDictioValue.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDictioValue.cs new file mode 100644 index 000000000..e18b4f111 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDictioValue.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public interface IDictioValue + { + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDummyFieldObject.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDummyFieldObject.cs new file mode 100644 index 000000000..801706b83 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IDummyFieldObject.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public interface IDummyFieldObject + { + [Duck(Kind = DuckKind.Field)] + int MagicNumber { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IGenericsWithAttribute.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IGenericsWithAttribute.cs new file mode 100644 index 000000000..8270cc631 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IGenericsWithAttribute.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public interface IGenericsWithAttribute + { + [Duck(Name = "GetDefault", GenericParameterTypeNames = new[] { "System.Int32" })] + int GetDefaultInt(); + + [Duck(Name = "GetDefault", GenericParameterTypeNames = new[] { "System.String" })] + string GetDefaultString(); + + [Duck(Name = "Wrap", GenericParameterTypeNames = new[] { "System.Int32", "System.String" })] + Tuple WrapIntString(int a, string b); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IObscureDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IObscureDuckType.cs new file mode 100644 index 000000000..e4124eefb --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/IObscureDuckType.cs @@ -0,0 +1,74 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public interface IObscureDuckType + { +#if INTERFACE_DEFAULTS + int Sum(int a, int b) => a + b; +#else + int Sum(int a, int b); +#endif + + float Sum(float a, float b); + + double Sum(double a, double b); + + short Sum(short a, short b); + + TestEnum2 ShowEnum(TestEnum2 val); + + object InternalSum(int a, int b); + + [Duck(ParameterTypeNames = new string[] { "System.String", "Elastic.Apm.Profiler.Managed.Tests.DuckTyping.ObscureObject+DummyFieldObject, Elastic.Apm.Profiler.Managed.Tests" })] + void Add(string name, object obj); + + void Add(string name, int obj); + + void Add(string name, string obj = "none"); + + void Pow2(ref int value); + + void GetOutput(out int value); + + [Duck(Name = "GetOutput")] + void GetOutputObject(out object value); + + bool TryGetObscure(out IDummyFieldObject obj); + + [Duck(Name = "TryGetObscure")] + bool TryGetObscureObject(out object obj); + + void GetReference(ref int value); + + [Duck(Name = "GetReference")] + void GetReferenceObject(ref object value); + + bool TryGetReference(ref IDummyFieldObject obj); + + [Duck(Name = "TryGetReference")] + bool TryGetReferenceObject(ref object obj); + + bool TryGetPrivateObscure(out IDummyFieldObject obj); + + [Duck(Name = "TryGetPrivateObscure")] + bool TryGetPrivateObscureObject(out object obj); + + bool TryGetPrivateReference(ref IDummyFieldObject obj); + + [Duck(Name = "TryGetPrivateReference")] + bool TryGetPrivateReferenceObject(ref object obj); + + IDummyFieldObject Bypass(IDummyFieldObject obj); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs new file mode 100644 index 000000000..e23e47bb1 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs @@ -0,0 +1,75 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public abstract class ObscureDuckTypeAbstractClass + { + public abstract int Sum(int a, int b); + + public abstract float Sum(float a, float b); + + public abstract double Sum(double a, double b); + + public abstract short Sum(short a, short b); + + public abstract TestEnum2 ShowEnum(TestEnum2 val); + + public abstract object InternalSum(int a, int b); + + [Duck(ParameterTypeNames = new string[] { "System.String", "Elastic.Apm.Profiler.Managed.Tests.DuckTyping.ObscureObject+DummyFieldObject, Elastic.Apm.Profiler.Managed.Tests" })] + public abstract void Add(string name, object obj); + + public abstract void Add(string name, int obj); + + public abstract void Add(string name, string obj = "none"); + + public abstract void Pow2(ref int value); + + public abstract void GetOutput(out int value); + + [Duck(Name = "GetOutput")] + public abstract void GetOutputObject(out object value); + + public abstract bool TryGetObscure(out IDummyFieldObject obj); + + [Duck(Name = "TryGetObscure")] + public abstract bool TryGetObscureObject(out object obj); + + public abstract void GetReference(ref int value); + + [Duck(Name = "GetReference")] + public abstract void GetReferenceObject(ref object value); + + public abstract bool TryGetReference(ref IDummyFieldObject obj); + + [Duck(Name = "TryGetReference")] + public abstract bool TryGetReferenceObject(ref object obj); + + public abstract bool TryGetPrivateObscure(out IDummyFieldObject obj); + + [Duck(Name = "TryGetPrivateObscure")] + public abstract bool TryGetPrivateObscureObject(out object obj); + + public abstract bool TryGetPrivateReference(ref IDummyFieldObject obj); + + [Duck(Name = "TryGetPrivateReference")] + public abstract bool TryGetPrivateReferenceObject(ref object obj); + + public void NormalMethod() + { + // . + } + + public abstract IDummyFieldObject Bypass(IDummyFieldObject obj); + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs new file mode 100644 index 000000000..8280207f0 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs @@ -0,0 +1,121 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public class ObscureDuckTypeVirtualClass + { + public virtual int Sum(int a, int b) => default; + + public virtual float Sum(float a, float b) => default; + + public virtual double Sum(double a, double b) => default; + + public virtual short Sum(short a, short b) => default; + + public virtual TestEnum2 ShowEnum(TestEnum2 val) => default; + + public virtual object InternalSum(int a, int b) => default; + + [Duck(ParameterTypeNames = new string[] { "System.String", "Elastic.Apm.Profiler.Managed.Tests.DuckTyping.ObscureObject+DummyFieldObject, Elastic.Apm.Profiler.Managed.Tests" })] + public virtual void Add(string name, object obj) + { + } + + public virtual void Add(string name, int obj) + { + } + + public virtual void Add(string name, string obj = "none") + { + } + + public virtual void Pow2(ref int value) + { + } + + public virtual void GetOutput(out int value) + { + value = default; + } + + [Duck(Name = "GetOutput")] + public virtual void GetOutputObject(out object value) + { + value = default; + } + + public virtual bool TryGetObscure(out IDummyFieldObject obj) + { + obj = default; + return false; + } + + [Duck(Name = "TryGetObscure")] + public virtual bool TryGetObscureObject(out object obj) + { + obj = default; + return false; + } + + public virtual void GetReference(ref int value) + { + } + + [Duck(Name = "GetReference")] + public virtual void GetReferenceObject(ref object value) + { + } + + public virtual bool TryGetReference(ref IDummyFieldObject obj) + { + return false; + } + + [Duck(Name = "TryGetReference")] + public virtual bool TryGetReferenceObject(ref object obj) + { + return false; + } + + public virtual bool TryGetPrivateObscure(out IDummyFieldObject obj) + { + obj = default; + return false; + } + + [Duck(Name = "TryGetPrivateObscure")] + public virtual bool TryGetPrivateObscureObject(out object obj) + { + obj = default; + return false; + } + + public virtual bool TryGetPrivateReference(ref IDummyFieldObject obj) + { + return false; + } + + [Duck(Name = "TryGetPrivateReference")] + public virtual bool TryGetPrivateReferenceObject(ref object obj) + { + return false; + } + + public void NormalMethod() + { + // . + } + + public virtual IDummyFieldObject Bypass(IDummyFieldObject obj) => null; + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/TestEnum2.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/TestEnum2.cs new file mode 100644 index 000000000..3c9e06b3f --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/TestEnum2.cs @@ -0,0 +1,18 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public enum TestEnum2 + { + Primero, + Segundo + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/VirtualGenericsWithAttribute.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/VirtualGenericsWithAttribute.cs new file mode 100644 index 000000000..0a6b219ae --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Methods/ProxiesDefinitions/VirtualGenericsWithAttribute.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Methods.ProxiesDefinitions +{ + public class VirtualGenericsWithAttribute + { + [Duck(Name = "GetDefault", GenericParameterTypeNames = new[] { "System.Int32" })] + public virtual int GetDefaultInt() => 100; + + [Duck(Name = "GetDefault", GenericParameterTypeNames = new[] { "System.String" })] + public virtual string GetDefaultString() => string.Empty; + + [Duck(Name = "Wrap", GenericParameterTypeNames = new[] { "System.Int32", "System.String" })] + public virtual Tuple WrapIntString(int a, string b) => null; + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ObscureObject.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ObscureObject.cs new file mode 100644 index 000000000..92ef87f94 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/ObscureObject.cs @@ -0,0 +1,1016 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class ObscureObject + { + private static FieldPublicObject fieldPublicObject = new FieldPublicObject(); + private static FieldInternalObject fieldInternalObject = new FieldInternalObject(); + private static FieldPrivateObject fieldPrivateObject = new FieldPrivateObject(); + + private static PropertyPublicObject propertyPublicObject = new PropertyPublicObject(); + private static PropertyInternalObject propertyInternalObject = new PropertyInternalObject(); + private static PropertyPrivateObject propertyPrivateObject = new PropertyPrivateObject(); + + public static object GetFieldPublicObject() => fieldPublicObject; + + public static object GetFieldInternalObject() => fieldInternalObject; + + public static object GetFieldPrivateObject() => fieldPrivateObject; + + public static object GetPropertyPublicObject() => propertyPublicObject; + + public static object GetPropertyInternalObject() => propertyInternalObject; + + public static object GetPropertyPrivateObject() => propertyPrivateObject; + + // *** + public enum TestEnum + { + First, + Second + } + + // *** + + public class DummyFieldObject + { + internal static DummyFieldObject Default = new DummyFieldObject(); + + public int MagicNumber = 42; + + internal TypesTuple this[TypesTuple index] + { + get => index; + set { } + } + } + + private class PrivateDummyFieldObject + { + internal static PrivateDummyFieldObject Default = new PrivateDummyFieldObject(); + + public int MagicNumber = 42; + + internal TypesTuple this[TypesTuple index] + { + get => index; + set { } + } + } + + // *** + + public class FieldPublicObject + { + public static readonly int _publicStaticReadonlyValueTypeField = 10; + internal static readonly int _internalStaticReadonlyValueTypeField = 11; + protected static readonly int _protectedStaticReadonlyValueTypeField = 12; + private static readonly int _privateStaticReadonlyValueTypeField = 13; + + public static int _publicStaticValueTypeField = 20; + internal static int _internalStaticValueTypeField = 21; + protected static int _protectedStaticValueTypeField = 22; + private static int _privateStaticValueTypeField = 23; + + public readonly int _publicReadonlyValueTypeField = 30; + internal readonly int _internalReadonlyValueTypeField = 31; + protected readonly int _protectedReadonlyValueTypeField = 32; + private readonly int _privateReadonlyValueTypeField = 33; + + public int _publicValueTypeField = 40; + internal int _internalValueTypeField = 41; + protected int _protectedValueTypeField = 42; + private int _privateValueTypeField = 43; + + // *** + + public static readonly string _publicStaticReadonlyReferenceTypeField = "10"; + internal static readonly string _internalStaticReadonlyReferenceTypeField = "11"; + protected static readonly string _protectedStaticReadonlyReferenceTypeField = "12"; + private static readonly string _privateStaticReadonlyReferenceTypeField = "13"; + + public static string _publicStaticReferenceTypeField = "20"; + internal static string _internalStaticReferenceTypeField = "21"; + protected static string _protectedStaticReferenceTypeField = "22"; + private static string _privateStaticReferenceTypeField = "23"; + + public readonly string _publicReadonlyReferenceTypeField = "30"; + internal readonly string _internalReadonlyReferenceTypeField = "31"; + protected readonly string _protectedReadonlyReferenceTypeField = "32"; + private readonly string _privateReadonlyReferenceTypeField = "33"; + + public string _publicReferenceTypeField = "40"; + internal string _internalReferenceTypeField = "41"; + protected string _protectedReferenceTypeField = "42"; + private string _privateReferenceTypeField = "43"; + + // *** + + public static readonly DummyFieldObject _publicStaticReadonlySelfTypeField = DummyFieldObject.Default; + internal static readonly DummyFieldObject _internalStaticReadonlySelfTypeField = DummyFieldObject.Default; + protected static readonly DummyFieldObject _protectedStaticReadonlySelfTypeField = DummyFieldObject.Default; + private static readonly DummyFieldObject _privateStaticReadonlySelfTypeField = DummyFieldObject.Default; + + public static DummyFieldObject _publicStaticSelfTypeField = DummyFieldObject.Default; + internal static DummyFieldObject _internalStaticSelfTypeField = DummyFieldObject.Default; + protected static DummyFieldObject _protectedStaticSelfTypeField = DummyFieldObject.Default; + private static DummyFieldObject _privateStaticSelfTypeField = DummyFieldObject.Default; + + public readonly DummyFieldObject _publicReadonlySelfTypeField = DummyFieldObject.Default; + internal readonly DummyFieldObject _internalReadonlySelfTypeField = DummyFieldObject.Default; + protected readonly DummyFieldObject _protectedReadonlySelfTypeField = DummyFieldObject.Default; + private readonly DummyFieldObject _privateReadonlySelfTypeField = DummyFieldObject.Default; + + public DummyFieldObject _publicSelfTypeField = DummyFieldObject.Default; + internal DummyFieldObject _internalSelfTypeField = DummyFieldObject.Default; + protected DummyFieldObject _protectedSelfTypeField = DummyFieldObject.Default; + private DummyFieldObject _privateSelfTypeField = DummyFieldObject.Default; + + // *** + + public static int? _publicStaticNullableIntField = null; + private static int? _privateStaticNullableIntField = null; + public int? _publicNullableIntField = null; + private int? _privateNullableIntField = null; + } + + internal class FieldInternalObject + { + public static readonly int _publicStaticReadonlyValueTypeField = 10; + internal static readonly int _internalStaticReadonlyValueTypeField = 11; + protected static readonly int _protectedStaticReadonlyValueTypeField = 12; + private static readonly int _privateStaticReadonlyValueTypeField = 13; + + public static int _publicStaticValueTypeField = 20; + internal static int _internalStaticValueTypeField = 21; + protected static int _protectedStaticValueTypeField = 22; + private static int _privateStaticValueTypeField = 23; + + public readonly int _publicReadonlyValueTypeField = 30; + internal readonly int _internalReadonlyValueTypeField = 31; + protected readonly int _protectedReadonlyValueTypeField = 32; + private readonly int _privateReadonlyValueTypeField = 33; + + public int _publicValueTypeField = 40; + internal int _internalValueTypeField = 41; + protected int _protectedValueTypeField = 42; + private int _privateValueTypeField = 43; + + // *** + + public static readonly string _publicStaticReadonlyReferenceTypeField = "10"; + internal static readonly string _internalStaticReadonlyReferenceTypeField = "11"; + protected static readonly string _protectedStaticReadonlyReferenceTypeField = "12"; + private static readonly string _privateStaticReadonlyReferenceTypeField = "13"; + + public static string _publicStaticReferenceTypeField = "20"; + internal static string _internalStaticReferenceTypeField = "21"; + protected static string _protectedStaticReferenceTypeField = "22"; + private static string _privateStaticReferenceTypeField = "23"; + + public readonly string _publicReadonlyReferenceTypeField = "30"; + internal readonly string _internalReadonlyReferenceTypeField = "31"; + protected readonly string _protectedReadonlyReferenceTypeField = "32"; + private readonly string _privateReadonlyReferenceTypeField = "33"; + + public string _publicReferenceTypeField = "40"; + internal string _internalReferenceTypeField = "41"; + protected string _protectedReferenceTypeField = "42"; + private string _privateReferenceTypeField = "43"; + + // *** + + public static readonly DummyFieldObject _publicStaticReadonlySelfTypeField = DummyFieldObject.Default; + internal static readonly DummyFieldObject _internalStaticReadonlySelfTypeField = DummyFieldObject.Default; + protected static readonly DummyFieldObject _protectedStaticReadonlySelfTypeField = DummyFieldObject.Default; + private static readonly DummyFieldObject _privateStaticReadonlySelfTypeField = DummyFieldObject.Default; + + public static DummyFieldObject _publicStaticSelfTypeField = DummyFieldObject.Default; + internal static DummyFieldObject _internalStaticSelfTypeField = DummyFieldObject.Default; + protected static DummyFieldObject _protectedStaticSelfTypeField = DummyFieldObject.Default; + private static DummyFieldObject _privateStaticSelfTypeField = DummyFieldObject.Default; + + public readonly DummyFieldObject _publicReadonlySelfTypeField = DummyFieldObject.Default; + internal readonly DummyFieldObject _internalReadonlySelfTypeField = DummyFieldObject.Default; + protected readonly DummyFieldObject _protectedReadonlySelfTypeField = DummyFieldObject.Default; + private readonly DummyFieldObject _privateReadonlySelfTypeField = DummyFieldObject.Default; + + public DummyFieldObject _publicSelfTypeField = DummyFieldObject.Default; + internal DummyFieldObject _internalSelfTypeField = DummyFieldObject.Default; + protected DummyFieldObject _protectedSelfTypeField = DummyFieldObject.Default; + private DummyFieldObject _privateSelfTypeField = DummyFieldObject.Default; + + // *** + + public static int? _publicStaticNullableIntField = null; + private static int? _privateStaticNullableIntField = null; + public int? _publicNullableIntField = null; + private int? _privateNullableIntField = null; + } + + private class FieldPrivateObject + { + public static readonly int _publicStaticReadonlyValueTypeField = 10; + internal static readonly int _internalStaticReadonlyValueTypeField = 11; + protected static readonly int _protectedStaticReadonlyValueTypeField = 12; + private static readonly int _privateStaticReadonlyValueTypeField = 13; + + public static int _publicStaticValueTypeField = 20; + internal static int _internalStaticValueTypeField = 21; + protected static int _protectedStaticValueTypeField = 22; + private static int _privateStaticValueTypeField = 23; + + public readonly int _publicReadonlyValueTypeField = 30; + internal readonly int _internalReadonlyValueTypeField = 31; + protected readonly int _protectedReadonlyValueTypeField = 32; + private readonly int _privateReadonlyValueTypeField = 33; + + public int _publicValueTypeField = 40; + internal int _internalValueTypeField = 41; + protected int _protectedValueTypeField = 42; + private int _privateValueTypeField = 43; + + // *** + + public static readonly string _publicStaticReadonlyReferenceTypeField = "10"; + internal static readonly string _internalStaticReadonlyReferenceTypeField = "11"; + protected static readonly string _protectedStaticReadonlyReferenceTypeField = "12"; + private static readonly string _privateStaticReadonlyReferenceTypeField = "13"; + + public static string _publicStaticReferenceTypeField = "20"; + internal static string _internalStaticReferenceTypeField = "21"; + protected static string _protectedStaticReferenceTypeField = "22"; + private static string _privateStaticReferenceTypeField = "23"; + + public readonly string _publicReadonlyReferenceTypeField = "30"; + internal readonly string _internalReadonlyReferenceTypeField = "31"; + protected readonly string _protectedReadonlyReferenceTypeField = "32"; + private readonly string _privateReadonlyReferenceTypeField = "33"; + + public string _publicReferenceTypeField = "40"; + internal string _internalReferenceTypeField = "41"; + protected string _protectedReferenceTypeField = "42"; + private string _privateReferenceTypeField = "43"; + + // *** + + public static readonly DummyFieldObject _publicStaticReadonlySelfTypeField = DummyFieldObject.Default; + internal static readonly DummyFieldObject _internalStaticReadonlySelfTypeField = DummyFieldObject.Default; + protected static readonly DummyFieldObject _protectedStaticReadonlySelfTypeField = DummyFieldObject.Default; + private static readonly DummyFieldObject _privateStaticReadonlySelfTypeField = DummyFieldObject.Default; + + public static DummyFieldObject _publicStaticSelfTypeField = DummyFieldObject.Default; + internal static DummyFieldObject _internalStaticSelfTypeField = DummyFieldObject.Default; + protected static DummyFieldObject _protectedStaticSelfTypeField = DummyFieldObject.Default; + private static DummyFieldObject _privateStaticSelfTypeField = DummyFieldObject.Default; + + public readonly DummyFieldObject _publicReadonlySelfTypeField = DummyFieldObject.Default; + internal readonly DummyFieldObject _internalReadonlySelfTypeField = DummyFieldObject.Default; + protected readonly DummyFieldObject _protectedReadonlySelfTypeField = DummyFieldObject.Default; + private readonly DummyFieldObject _privateReadonlySelfTypeField = DummyFieldObject.Default; + + public DummyFieldObject _publicSelfTypeField = DummyFieldObject.Default; + internal DummyFieldObject _internalSelfTypeField = DummyFieldObject.Default; + protected DummyFieldObject _protectedSelfTypeField = DummyFieldObject.Default; + private DummyFieldObject _privateSelfTypeField = DummyFieldObject.Default; + + // *** + + public static int? _publicStaticNullableIntField = null; + private static int? _privateStaticNullableIntField = null; + public int? _publicNullableIntField = null; + private int? _privateNullableIntField = null; + } + + // *** + + public class PropertyPublicObject + { + private Dictionary _dictioInt = new Dictionary(); + private Dictionary _dictioString = new Dictionary(); + + // ********* + + public static int PublicStaticGetValueType { get; } = 10; + + internal static int InternalStaticGetValueType { get; } = 11; + + protected static int ProtectedStaticGetValueType { get; } = 12; + + private static int PrivateStaticGetValueType { get; } = 13; + + // * + + public static int PublicStaticGetSetValueType { get; set; } = 20; + + internal static int InternalStaticGetSetValueType { get; set; } = 21; + + protected static int ProtectedStaticGetSetValueType { get; set; } = 22; + + private static int PrivateStaticGetSetValueType { get; set; } = 23; + + // * + + public int PublicGetValueType { get; } = 30; + + internal int InternalGetValueType { get; } = 31; + + protected int ProtectedGetValueType { get; } = 32; + + private int PrivateGetValueType { get; } = 33; + + // * + + public int PublicGetSetValueType { get; set; } = 40; + + internal int InternalGetSetValueType { get; set; } = 41; + + protected int ProtectedGetSetValueType { get; set; } = 42; + + private int PrivateGetSetValueType { get; set; } = 43; + + // ********* + + public static string PublicStaticGetReferenceType { get; } = "10"; + + internal static string InternalStaticGetReferenceType { get; } = "11"; + + protected static string ProtectedStaticGetReferenceType { get; } = "12"; + + private static string PrivateStaticGetReferenceType { get; } = "13"; + + // * + + public static string PublicStaticGetSetReferenceType { get; set; } = "20"; + + internal static string InternalStaticGetSetReferenceType { get; set; } = "21"; + + protected static string ProtectedStaticGetSetReferenceType { get; set; } = "22"; + + private static string PrivateStaticGetSetReferenceType { get; set; } = "23"; + + // * + + public string PublicGetReferenceType { get; } = "30"; + + internal string InternalGetReferenceType { get; } = "31"; + + protected string ProtectedGetReferenceType { get; } = "32"; + + private string PrivateGetReferenceType { get; } = "33"; + + // * + + public string PublicGetSetReferenceType { get; set; } = "40"; + + internal string InternalGetSetReferenceType { get; set; } = "41"; + + protected string ProtectedGetSetReferenceType { get; set; } = "42"; + + private string PrivateGetSetReferenceType { get; set; } = "43"; + + // ********* + + public static DummyFieldObject PublicStaticGetSelfType { get; } = DummyFieldObject.Default; + + internal static DummyFieldObject InternalStaticGetSelfType { get; } = DummyFieldObject.Default; + + protected static DummyFieldObject ProtectedStaticGetSelfType { get; } = DummyFieldObject.Default; + + private static DummyFieldObject PrivateStaticGetSelfType { get; } = DummyFieldObject.Default; + + // * + + public static DummyFieldObject PublicStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + internal static DummyFieldObject InternalStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + protected static DummyFieldObject ProtectedStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + private static DummyFieldObject PrivateStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + // * + + public DummyFieldObject PublicGetSelfType { get; } = DummyFieldObject.Default; + + internal DummyFieldObject InternalGetSelfType { get; } = DummyFieldObject.Default; + + protected DummyFieldObject ProtectedGetSelfType { get; } = DummyFieldObject.Default; + + private DummyFieldObject PrivateGetSelfType { get; } = DummyFieldObject.Default; + + // * + + public DummyFieldObject PublicGetSetSelfType { get; set; } = DummyFieldObject.Default; + + internal DummyFieldObject InternalGetSetSelfType { get; set; } = DummyFieldObject.Default; + + protected DummyFieldObject ProtectedGetSetSelfType { get; set; } = DummyFieldObject.Default; + + private DummyFieldObject PrivateGetSetSelfType { get; set; } = DummyFieldObject.Default; + + // *** + + private PrivateDummyFieldObject PrivateDummyGetSetSelfType { get; set; } = PrivateDummyFieldObject.Default; + + // *** + + public static int? PublicStaticNullableInt { get; set; } + + private static int? PrivateStaticNullableInt { get; set; } + + public int? PublicNullableInt { get; set; } + + private int? PrivateNullableInt { get; set; } + + // *** + + public int this[int index] + { + get => _dictioInt[index]; + set => _dictioInt[index] = value; + } + + public string this[string index] + { + get => _dictioString[index]; + set => _dictioString[index] = value; + } + + // *** + + public TaskStatus Status { get; set; } = TaskStatus.RanToCompletion; + + // *** Methods + public int Sum(int a, int b) => a + b; + + public float Sum(float a, float b) => a + b; + + public double Sum(double a, double b) => a + b; + + public short Sum(short a, short b) => (short)(a + b); + + private int InternalSum(int a, int b) => a + b; + + public TestEnum ShowEnum(TestEnum val) + { + return val; + } + + public T GetDefault() => default; + + public Tuple Wrap(T1 a, T2 b) + { + return new Tuple(a, b); + } + + public void Add(string name, DummyFieldObject obj) + { + } + + public void Add(string name, int obj) + { + } + + public void Add(string name, string obj = "none") + { + } + + public void Pow2(ref int value) + { + value *= value; + } + + public void GetOutput(out int value) + { + value = 42; + } + + private bool TryGetObscure(out DummyFieldObject obj) + { + obj = new DummyFieldObject { MagicNumber = 99 }; + return true; + } + + public void GetReference(ref int value) + { + value *= value; + } + + private bool TryGetReference(ref DummyFieldObject obj) + { + obj = new DummyFieldObject { MagicNumber = (obj?.MagicNumber ?? 99) + 1 }; + return true; + } + + private bool TryGetPrivateObscure(out PrivateDummyFieldObject obj) + { + obj = new PrivateDummyFieldObject { MagicNumber = 99 }; + return true; + } + + private bool TryGetPrivateReference(ref PrivateDummyFieldObject obj) + { + obj = new PrivateDummyFieldObject { MagicNumber = (obj?.MagicNumber ?? 99) + 1 }; + return true; + } + + private DummyFieldObject Bypass(DummyFieldObject obj) + { + return obj; + } + } + + internal class PropertyInternalObject + { + private Dictionary _dictioInt = new Dictionary(); + private Dictionary _dictioString = new Dictionary(); + + public static int PublicStaticGetValueType { get; } = 10; + + internal static int InternalStaticGetValueType { get; } = 11; + + protected static int ProtectedStaticGetValueType { get; } = 12; + + private static int PrivateStaticGetValueType { get; } = 13; + + // * + + public static int PublicStaticGetSetValueType { get; set; } = 20; + + internal static int InternalStaticGetSetValueType { get; set; } = 21; + + protected static int ProtectedStaticGetSetValueType { get; set; } = 22; + + private static int PrivateStaticGetSetValueType { get; set; } = 23; + + // * + + public int PublicGetValueType { get; } = 30; + + internal int InternalGetValueType { get; } = 31; + + protected int ProtectedGetValueType { get; } = 32; + + private int PrivateGetValueType { get; } = 33; + + // * + + public int PublicGetSetValueType { get; set; } = 40; + + internal int InternalGetSetValueType { get; set; } = 41; + + protected int ProtectedGetSetValueType { get; set; } = 42; + + private int PrivateGetSetValueType { get; set; } = 43; + + // ********* + + public static string PublicStaticGetReferenceType { get; } = "10"; + + internal static string InternalStaticGetReferenceType { get; } = "11"; + + protected static string ProtectedStaticGetReferenceType { get; } = "12"; + + private static string PrivateStaticGetReferenceType { get; } = "13"; + + // * + + public static string PublicStaticGetSetReferenceType { get; set; } = "20"; + + internal static string InternalStaticGetSetReferenceType { get; set; } = "21"; + + protected static string ProtectedStaticGetSetReferenceType { get; set; } = "22"; + + private static string PrivateStaticGetSetReferenceType { get; set; } = "23"; + + // * + + public string PublicGetReferenceType { get; } = "30"; + + internal string InternalGetReferenceType { get; } = "31"; + + protected string ProtectedGetReferenceType { get; } = "32"; + + private string PrivateGetReferenceType { get; } = "33"; + + // * + + public string PublicGetSetReferenceType { get; set; } = "40"; + + internal string InternalGetSetReferenceType { get; set; } = "41"; + + protected string ProtectedGetSetReferenceType { get; set; } = "42"; + + private string PrivateGetSetReferenceType { get; set; } = "43"; + + // ********* + + public static DummyFieldObject PublicStaticGetSelfType { get; } = DummyFieldObject.Default; + + internal static DummyFieldObject InternalStaticGetSelfType { get; } = DummyFieldObject.Default; + + protected static DummyFieldObject ProtectedStaticGetSelfType { get; } = DummyFieldObject.Default; + + private static DummyFieldObject PrivateStaticGetSelfType { get; } = DummyFieldObject.Default; + + // * + + public static DummyFieldObject PublicStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + internal static DummyFieldObject InternalStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + protected static DummyFieldObject ProtectedStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + private static DummyFieldObject PrivateStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + // * + + public DummyFieldObject PublicGetSelfType { get; } = DummyFieldObject.Default; + + internal DummyFieldObject InternalGetSelfType { get; } = DummyFieldObject.Default; + + protected DummyFieldObject ProtectedGetSelfType { get; } = DummyFieldObject.Default; + + private DummyFieldObject PrivateGetSelfType { get; } = DummyFieldObject.Default; + + // * + + public DummyFieldObject PublicGetSetSelfType { get; set; } = DummyFieldObject.Default; + + internal DummyFieldObject InternalGetSetSelfType { get; set; } = DummyFieldObject.Default; + + protected DummyFieldObject ProtectedGetSetSelfType { get; set; } = DummyFieldObject.Default; + + private DummyFieldObject PrivateGetSetSelfType { get; set; } = DummyFieldObject.Default; + + // *** + + private PrivateDummyFieldObject PrivateDummyGetSetSelfType { get; set; } = PrivateDummyFieldObject.Default; + + // *** + + public static int? PublicStaticNullableInt { get; set; } + + private static int? PrivateStaticNullableInt { get; set; } + + public int? PublicNullableInt { get; set; } + + private int? PrivateNullableInt { get; set; } + + // *** + + public int this[int index] + { + get => _dictioInt[index]; + set => _dictioInt[index] = value; + } + + public string this[string index] + { + get => _dictioString[index]; + set => _dictioString[index] = value; + } + + // *** + + public TaskStatus Status { get; set; } = TaskStatus.RanToCompletion; + + // *** Methods + public int Sum(int a, int b) => a + b; + + public float Sum(float a, float b) => a + b; + + public double Sum(double a, double b) => a + b; + + public short Sum(short a, short b) => (short)(a + b); + + private int InternalSum(int a, int b) => a + b; + + public TestEnum ShowEnum(TestEnum val) + { + return val; + } + + public T GetDefault() => default; + + public Tuple Wrap(T1 a, T2 b) + { + return new Tuple(a, b); + } + + public void Add(string name, DummyFieldObject obj) + { + } + + public void Add(string name, int obj) + { + } + + public void Add(string name, string obj = "none") + { + } + + public void Pow2(ref int value) + { + value *= value; + } + + public void GetOutput(out int value) + { + value = 42; + } + + private bool TryGetObscure(out DummyFieldObject obj) + { + obj = new DummyFieldObject { MagicNumber = 99 }; + return true; + } + + public void GetReference(ref int value) + { + value *= value; + } + + private bool TryGetReference(ref DummyFieldObject obj) + { + obj = new DummyFieldObject { MagicNumber = (obj?.MagicNumber ?? 99) + 1 }; + return true; + } + + private bool TryGetPrivateObscure(out PrivateDummyFieldObject obj) + { + obj = new PrivateDummyFieldObject { MagicNumber = 99 }; + return true; + } + + private bool TryGetPrivateReference(ref PrivateDummyFieldObject obj) + { + obj = new PrivateDummyFieldObject { MagicNumber = (obj?.MagicNumber ?? 99) + 1 }; + return true; + } + + private DummyFieldObject Bypass(DummyFieldObject obj) + { + return obj; + } + } + + private class PropertyPrivateObject + { + private Dictionary _dictioInt = new Dictionary(); + private Dictionary _dictioString = new Dictionary(); + + public static int PublicStaticGetValueType { get; } = 10; + + internal static int InternalStaticGetValueType { get; } = 11; + + protected static int ProtectedStaticGetValueType { get; } = 12; + + private static int PrivateStaticGetValueType { get; } = 13; + + // * + + public static int PublicStaticGetSetValueType { get; set; } = 20; + + internal static int InternalStaticGetSetValueType { get; set; } = 21; + + protected static int ProtectedStaticGetSetValueType { get; set; } = 22; + + private static int PrivateStaticGetSetValueType { get; set; } = 23; + + // * + + public int PublicGetValueType { get; } = 30; + + internal int InternalGetValueType { get; } = 31; + + protected int ProtectedGetValueType { get; } = 32; + + private int PrivateGetValueType { get; } = 33; + + // * + + public int PublicGetSetValueType { get; set; } = 40; + + internal int InternalGetSetValueType { get; set; } = 41; + + protected int ProtectedGetSetValueType { get; set; } = 42; + + private int PrivateGetSetValueType { get; set; } = 43; + + // ********* + + public static string PublicStaticGetReferenceType { get; } = "10"; + + internal static string InternalStaticGetReferenceType { get; } = "11"; + + protected static string ProtectedStaticGetReferenceType { get; } = "12"; + + private static string PrivateStaticGetReferenceType { get; } = "13"; + + // * + + public static string PublicStaticGetSetReferenceType { get; set; } = "20"; + + internal static string InternalStaticGetSetReferenceType { get; set; } = "21"; + + protected static string ProtectedStaticGetSetReferenceType { get; set; } = "22"; + + private static string PrivateStaticGetSetReferenceType { get; set; } = "23"; + + // * + + public string PublicGetReferenceType { get; } = "30"; + + internal string InternalGetReferenceType { get; } = "31"; + + protected string ProtectedGetReferenceType { get; } = "32"; + + private string PrivateGetReferenceType { get; } = "33"; + + // * + + public string PublicGetSetReferenceType { get; set; } = "40"; + + internal string InternalGetSetReferenceType { get; set; } = "41"; + + protected string ProtectedGetSetReferenceType { get; set; } = "42"; + + private string PrivateGetSetReferenceType { get; set; } = "43"; + + // ********* + + public static DummyFieldObject PublicStaticGetSelfType { get; } = DummyFieldObject.Default; + + internal static DummyFieldObject InternalStaticGetSelfType { get; } = DummyFieldObject.Default; + + protected static DummyFieldObject ProtectedStaticGetSelfType { get; } = DummyFieldObject.Default; + + private static DummyFieldObject PrivateStaticGetSelfType { get; } = DummyFieldObject.Default; + + // * + + public static DummyFieldObject PublicStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + internal static DummyFieldObject InternalStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + protected static DummyFieldObject ProtectedStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + private static DummyFieldObject PrivateStaticGetSetSelfType { get; set; } = DummyFieldObject.Default; + + // * + + public DummyFieldObject PublicGetSelfType { get; } = DummyFieldObject.Default; + + internal DummyFieldObject InternalGetSelfType { get; } = DummyFieldObject.Default; + + protected DummyFieldObject ProtectedGetSelfType { get; } = DummyFieldObject.Default; + + private DummyFieldObject PrivateGetSelfType { get; } = DummyFieldObject.Default; + + // * + + public DummyFieldObject PublicGetSetSelfType { get; set; } = DummyFieldObject.Default; + + internal DummyFieldObject InternalGetSetSelfType { get; set; } = DummyFieldObject.Default; + + protected DummyFieldObject ProtectedGetSetSelfType { get; set; } = DummyFieldObject.Default; + + private DummyFieldObject PrivateGetSetSelfType { get; set; } = DummyFieldObject.Default; + + // *** + + private PrivateDummyFieldObject PrivateDummyGetSetSelfType { get; set; } = PrivateDummyFieldObject.Default; + + // *** + + public static int? PublicStaticNullableInt { get; set; } + + private static int? PrivateStaticNullableInt { get; set; } + + public int? PublicNullableInt { get; set; } + + private int? PrivateNullableInt { get; set; } + + // *** + public int this[int index] + { + get => _dictioInt[index]; + set => _dictioInt[index] = value; + } + + public string this[string index] + { + get => _dictioString[index]; + set => _dictioString[index] = value; + } + + // *** + + public TaskStatus Status { get; set; } = TaskStatus.RanToCompletion; + + // *** Methods + public int Sum(int a, int b) => a + b; + + public float Sum(float a, float b) => a + b; + + public double Sum(double a, double b) => a + b; + + public short Sum(short a, short b) => (short)(a + b); + + private int InternalSum(int a, int b) => a + b; + + public TestEnum ShowEnum(TestEnum val) + { + return val; + } + + public T GetDefault() => default; + + public Tuple Wrap(T1 a, T2 b) + { + return new Tuple(a, b); + } + + public void Add(string name, DummyFieldObject obj) + { + } + + public void Add(string name, int obj) + { + } + + public void Add(string name, string obj = "none") + { + } + + public void Pow2(ref int value) + { + value *= value; + } + + public void GetOutput(out int value) + { + value = 42; + } + + private bool TryGetObscure(out DummyFieldObject obj) + { + obj = new DummyFieldObject { MagicNumber = 99 }; + return true; + } + + public void GetReference(ref int value) + { + value *= value; + } + + private bool TryGetReference(ref DummyFieldObject obj) + { + obj = new DummyFieldObject { MagicNumber = (obj?.MagicNumber ?? 99) + 1 }; + return true; + } + + private bool TryGetPrivateObscure(out PrivateDummyFieldObject obj) + { + obj = new PrivateDummyFieldObject { MagicNumber = 99 }; + return true; + } + + private bool TryGetPrivateReference(ref PrivateDummyFieldObject obj) + { + obj = new PrivateDummyFieldObject { MagicNumber = (obj?.MagicNumber ?? 99) + 1 }; + return true; + } + + private DummyFieldObject Bypass(DummyFieldObject obj) + { + return obj; + } + } + + // *** + + public struct PublicStruct + { + public int PublicGetSetValueType { get; set; } + + private int PrivateGetSetValueType { get; set; } + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IDuckTypeUnion.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IDuckTypeUnion.cs new file mode 100644 index 000000000..82560abdb --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IDuckTypeUnion.cs @@ -0,0 +1,43 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions +{ + public interface IDuckTypeUnion : + IDuckType, + IPublicReferenceType, + IInternalReferenceType, + IProtectedReferenceType, + IPrivateReferenceType + { + } + + public interface IPublicReferenceType + { + string PublicGetSetReferenceType { get; set; } + } + + public interface IInternalReferenceType + { + string InternalGetSetReferenceType { get; set; } + } + + public interface IProtectedReferenceType + { + string ProtectedGetSetReferenceType { get; set; } + } + + public interface IPrivateReferenceType + { + string PrivateGetSetReferenceType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureDuckType.cs new file mode 100644 index 000000000..c0f70bc68 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureDuckType.cs @@ -0,0 +1,64 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions +{ + public interface IObscureDuckType + { + string PublicStaticGetReferenceType { get; } + + string InternalStaticGetReferenceType { get; } + + string ProtectedStaticGetReferenceType { get; } + + string PrivateStaticGetReferenceType { get; } + + // * + + string PublicStaticGetSetReferenceType { get; set; } + + string InternalStaticGetSetReferenceType { get; set; } + + string ProtectedStaticGetSetReferenceType { get; set; } + + string PrivateStaticGetSetReferenceType { get; set; } + + // * + + string PublicGetReferenceType { get; } + + string InternalGetReferenceType { get; } + + string ProtectedGetReferenceType { get; } + + string PrivateGetReferenceType { get; } + + // * + + string PublicGetSetReferenceType { get; set; } + + string InternalGetSetReferenceType { get; set; } + + string ProtectedGetSetReferenceType { get; set; } + + string PrivateGetSetReferenceType { get; set; } + + // * + + [Duck(Name = "PublicStaticGetSetReferenceType")] + string PublicStaticOnlySet { set; } + + // * + + string this[string index] { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureErrorDuckType.cs new file mode 100644 index 000000000..c893f17fb --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureErrorDuckType.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions +{ + public interface IObscureErrorDuckType + { + string PublicGetReferenceType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureStaticErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureStaticErrorDuckType.cs new file mode 100644 index 000000000..33e892944 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/IObscureStaticErrorDuckType.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions +{ + public interface IObscureStaticErrorDuckType + { + string PublicStaticGetReferenceType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs new file mode 100644 index 000000000..d1e10d1e9 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs @@ -0,0 +1,57 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions +{ + public abstract class ObscureDuckTypeAbstractClass + { + public abstract string PublicStaticGetReferenceType { get; } + + public abstract string InternalStaticGetReferenceType { get; } + + public abstract string ProtectedStaticGetReferenceType { get; } + + public abstract string PrivateStaticGetReferenceType { get; } + + // * + + public abstract string PublicStaticGetSetReferenceType { get; set; } + + public abstract string InternalStaticGetSetReferenceType { get; set; } + + public abstract string ProtectedStaticGetSetReferenceType { get; set; } + + public abstract string PrivateStaticGetSetReferenceType { get; set; } + + // * + + public abstract string PublicGetReferenceType { get; } + + public abstract string InternalGetReferenceType { get; } + + public abstract string ProtectedGetReferenceType { get; } + + public abstract string PrivateGetReferenceType { get; } + + // * + + public abstract string PublicGetSetReferenceType { get; set; } + + public abstract string InternalGetSetReferenceType { get; set; } + + public abstract string ProtectedGetSetReferenceType { get; set; } + + public abstract string PrivateGetSetReferenceType { get; set; } + + // * + + public abstract string this[string index] { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeStruct.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeStruct.cs new file mode 100644 index 000000000..fa5100766 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeStruct.cs @@ -0,0 +1,42 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions +{ +#pragma warning disable 649 + + [DuckCopy] + internal struct ObscureDuckTypeStruct + { + public readonly string ReadonlyFieldIgnored; + + public string PublicStaticGetReferenceType; + public string InternalStaticGetReferenceType; + public string ProtectedStaticGetReferenceType; + public string PrivateStaticGetReferenceType; + + public string PublicStaticGetSetReferenceType; + public string InternalStaticGetSetReferenceType; + public string ProtectedStaticGetSetReferenceType; + public string PrivateStaticGetSetReferenceType; + + public string PublicGetReferenceType; + public string InternalGetReferenceType; + public string ProtectedGetReferenceType; + public string PrivateGetReferenceType; + + public string PublicGetSetReferenceType; + public string InternalGetSetReferenceType; + public string ProtectedGetSetReferenceType; + public string PrivateGetSetReferenceType; + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs new file mode 100644 index 000000000..eab1e32fb --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions +{ + public class ObscureDuckTypeVirtualClass + { + public virtual string PublicStaticGetReferenceType { get; } + + public virtual string InternalStaticGetReferenceType { get; } + + public virtual string ProtectedStaticGetReferenceType { get; } + + public virtual string PrivateStaticGetReferenceType { get; } + + // * + + public virtual string PublicStaticGetSetReferenceType { get; set; } + + public virtual string InternalStaticGetSetReferenceType { get; set; } + + public virtual string ProtectedStaticGetSetReferenceType { get; set; } + + public virtual string PrivateStaticGetSetReferenceType { get; set; } + + // * + + public virtual string PublicGetReferenceType { get; } + + public virtual string InternalGetReferenceType { get; } + + public virtual string ProtectedGetReferenceType { get; } + + public virtual string PrivateGetReferenceType { get; } + + // * + + public virtual string PublicGetSetReferenceType { get; set; } + + public virtual string InternalGetSetReferenceType { get; set; } + + public virtual string ProtectedGetSetReferenceType { get; set; } + + public virtual string PrivateGetSetReferenceType { get; set; } + + // * + + public virtual string this[string index] + { + get => default; + set { } + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ReferenceTypePropertyTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ReferenceTypePropertyTests.cs new file mode 100644 index 000000000..7b4df30ac --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ReferenceType/ReferenceTypePropertyTests.cs @@ -0,0 +1,398 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType.ProxiesDefinitions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ReferenceType +{ + public class ReferenceTypePropertyTests + { + public static IEnumerable Data() + { + return new[] + { + new object[] { ObscureObject.GetPropertyPublicObject() }, + new object[] { ObscureObject.GetPropertyInternalObject() }, + new object[] { ObscureObject.GetPropertyPrivateObject() }, + }; + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticGetOnlyPropertyException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void GetOnlyPropertyException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticGetOnlyProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal("10", duckInterface.PublicStaticGetReferenceType); + Assert.Equal("10", duckAbstract.PublicStaticGetReferenceType); + Assert.Equal("10", duckVirtual.PublicStaticGetReferenceType); + + // * + Assert.Equal("11", duckInterface.InternalStaticGetReferenceType); + Assert.Equal("11", duckAbstract.InternalStaticGetReferenceType); + Assert.Equal("11", duckVirtual.InternalStaticGetReferenceType); + + // * + Assert.Equal("12", duckInterface.ProtectedStaticGetReferenceType); + Assert.Equal("12", duckAbstract.ProtectedStaticGetReferenceType); + Assert.Equal("12", duckVirtual.ProtectedStaticGetReferenceType); + + // * + Assert.Equal("13", duckInterface.PrivateStaticGetReferenceType); + Assert.Equal("13", duckAbstract.PrivateStaticGetReferenceType); + Assert.Equal("13", duckVirtual.PrivateStaticGetReferenceType); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal("20", duckInterface.PublicStaticGetSetReferenceType); + Assert.Equal("20", duckAbstract.PublicStaticGetSetReferenceType); + Assert.Equal("20", duckVirtual.PublicStaticGetSetReferenceType); + + duckInterface.PublicStaticGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.PublicStaticGetSetReferenceType); + Assert.Equal("42", duckAbstract.PublicStaticGetSetReferenceType); + Assert.Equal("42", duckVirtual.PublicStaticGetSetReferenceType); + + duckAbstract.PublicStaticGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.PublicStaticGetSetReferenceType); + Assert.Equal("50", duckAbstract.PublicStaticGetSetReferenceType); + Assert.Equal("50", duckVirtual.PublicStaticGetSetReferenceType); + + duckVirtual.PublicStaticGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.PublicStaticGetSetReferenceType); + Assert.Equal("60", duckAbstract.PublicStaticGetSetReferenceType); + Assert.Equal("60", duckVirtual.PublicStaticGetSetReferenceType); + + duckInterface.PublicStaticGetSetReferenceType = "20"; + + // * + + Assert.Equal("21", duckInterface.InternalStaticGetSetReferenceType); + Assert.Equal("21", duckAbstract.InternalStaticGetSetReferenceType); + Assert.Equal("21", duckVirtual.InternalStaticGetSetReferenceType); + + duckInterface.InternalStaticGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.InternalStaticGetSetReferenceType); + Assert.Equal("42", duckAbstract.InternalStaticGetSetReferenceType); + Assert.Equal("42", duckVirtual.InternalStaticGetSetReferenceType); + + duckAbstract.InternalStaticGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.InternalStaticGetSetReferenceType); + Assert.Equal("50", duckAbstract.InternalStaticGetSetReferenceType); + Assert.Equal("50", duckVirtual.InternalStaticGetSetReferenceType); + + duckVirtual.InternalStaticGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.InternalStaticGetSetReferenceType); + Assert.Equal("60", duckAbstract.InternalStaticGetSetReferenceType); + Assert.Equal("60", duckVirtual.InternalStaticGetSetReferenceType); + + duckInterface.InternalStaticGetSetReferenceType = "21"; + + // * + + Assert.Equal("22", duckInterface.ProtectedStaticGetSetReferenceType); + Assert.Equal("22", duckAbstract.ProtectedStaticGetSetReferenceType); + Assert.Equal("22", duckVirtual.ProtectedStaticGetSetReferenceType); + + duckInterface.ProtectedStaticGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.ProtectedStaticGetSetReferenceType); + Assert.Equal("42", duckAbstract.ProtectedStaticGetSetReferenceType); + Assert.Equal("42", duckVirtual.ProtectedStaticGetSetReferenceType); + + duckAbstract.ProtectedStaticGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.ProtectedStaticGetSetReferenceType); + Assert.Equal("50", duckAbstract.ProtectedStaticGetSetReferenceType); + Assert.Equal("50", duckVirtual.ProtectedStaticGetSetReferenceType); + + duckVirtual.ProtectedStaticGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.ProtectedStaticGetSetReferenceType); + Assert.Equal("60", duckAbstract.ProtectedStaticGetSetReferenceType); + Assert.Equal("60", duckVirtual.ProtectedStaticGetSetReferenceType); + + duckInterface.ProtectedStaticGetSetReferenceType = "22"; + + // * + + Assert.Equal("23", duckInterface.PrivateStaticGetSetReferenceType); + Assert.Equal("23", duckAbstract.PrivateStaticGetSetReferenceType); + Assert.Equal("23", duckVirtual.PrivateStaticGetSetReferenceType); + + duckInterface.PrivateStaticGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.PrivateStaticGetSetReferenceType); + Assert.Equal("42", duckAbstract.PrivateStaticGetSetReferenceType); + Assert.Equal("42", duckVirtual.PrivateStaticGetSetReferenceType); + + duckAbstract.PrivateStaticGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.PrivateStaticGetSetReferenceType); + Assert.Equal("50", duckAbstract.PrivateStaticGetSetReferenceType); + Assert.Equal("50", duckVirtual.PrivateStaticGetSetReferenceType); + + duckVirtual.PrivateStaticGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.PrivateStaticGetSetReferenceType); + Assert.Equal("60", duckAbstract.PrivateStaticGetSetReferenceType); + Assert.Equal("60", duckVirtual.PrivateStaticGetSetReferenceType); + + duckInterface.PrivateStaticGetSetReferenceType = "23"; + } + + [Theory] + [MemberData(nameof(Data))] + public void GetOnlyProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal("30", duckInterface.PublicGetReferenceType); + Assert.Equal("30", duckAbstract.PublicGetReferenceType); + Assert.Equal("30", duckVirtual.PublicGetReferenceType); + + // * + Assert.Equal("31", duckInterface.InternalGetReferenceType); + Assert.Equal("31", duckAbstract.InternalGetReferenceType); + Assert.Equal("31", duckVirtual.InternalGetReferenceType); + + // * + Assert.Equal("32", duckInterface.ProtectedGetReferenceType); + Assert.Equal("32", duckAbstract.ProtectedGetReferenceType); + Assert.Equal("32", duckVirtual.ProtectedGetReferenceType); + + // * + Assert.Equal("33", duckInterface.PrivateGetReferenceType); + Assert.Equal("33", duckAbstract.PrivateGetReferenceType); + Assert.Equal("33", duckVirtual.PrivateGetReferenceType); + } + + [Theory] + [MemberData(nameof(Data))] + public void Properties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal("40", duckInterface.PublicGetSetReferenceType); + Assert.Equal("40", duckAbstract.PublicGetSetReferenceType); + Assert.Equal("40", duckVirtual.PublicGetSetReferenceType); + + duckInterface.PublicGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.PublicGetSetReferenceType); + Assert.Equal("42", duckAbstract.PublicGetSetReferenceType); + Assert.Equal("42", duckVirtual.PublicGetSetReferenceType); + + duckAbstract.PublicGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.PublicGetSetReferenceType); + Assert.Equal("50", duckAbstract.PublicGetSetReferenceType); + Assert.Equal("50", duckVirtual.PublicGetSetReferenceType); + + duckVirtual.PublicGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.PublicGetSetReferenceType); + Assert.Equal("60", duckAbstract.PublicGetSetReferenceType); + Assert.Equal("60", duckVirtual.PublicGetSetReferenceType); + + duckInterface.PublicGetSetReferenceType = "40"; + + // * + + Assert.Equal("41", duckInterface.InternalGetSetReferenceType); + Assert.Equal("41", duckAbstract.InternalGetSetReferenceType); + Assert.Equal("41", duckVirtual.InternalGetSetReferenceType); + + duckInterface.InternalGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.InternalGetSetReferenceType); + Assert.Equal("42", duckAbstract.InternalGetSetReferenceType); + Assert.Equal("42", duckVirtual.InternalGetSetReferenceType); + + duckAbstract.InternalGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.InternalGetSetReferenceType); + Assert.Equal("50", duckAbstract.InternalGetSetReferenceType); + Assert.Equal("50", duckVirtual.InternalGetSetReferenceType); + + duckVirtual.InternalGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.InternalGetSetReferenceType); + Assert.Equal("60", duckAbstract.InternalGetSetReferenceType); + Assert.Equal("60", duckVirtual.InternalGetSetReferenceType); + + duckInterface.InternalGetSetReferenceType = "41"; + + // * + + Assert.Equal("42", duckInterface.ProtectedGetSetReferenceType); + Assert.Equal("42", duckAbstract.ProtectedGetSetReferenceType); + Assert.Equal("42", duckVirtual.ProtectedGetSetReferenceType); + + duckInterface.ProtectedGetSetReferenceType = "45"; + Assert.Equal("45", duckInterface.ProtectedGetSetReferenceType); + Assert.Equal("45", duckAbstract.ProtectedGetSetReferenceType); + Assert.Equal("45", duckVirtual.ProtectedGetSetReferenceType); + + duckAbstract.ProtectedGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.ProtectedGetSetReferenceType); + Assert.Equal("50", duckAbstract.ProtectedGetSetReferenceType); + Assert.Equal("50", duckVirtual.ProtectedGetSetReferenceType); + + duckVirtual.ProtectedGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.ProtectedGetSetReferenceType); + Assert.Equal("60", duckAbstract.ProtectedGetSetReferenceType); + Assert.Equal("60", duckVirtual.ProtectedGetSetReferenceType); + + duckInterface.ProtectedGetSetReferenceType = "42"; + + // * + + Assert.Equal("43", duckInterface.PrivateGetSetReferenceType); + Assert.Equal("43", duckAbstract.PrivateGetSetReferenceType); + Assert.Equal("43", duckVirtual.PrivateGetSetReferenceType); + + duckInterface.PrivateGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.PrivateGetSetReferenceType); + Assert.Equal("42", duckAbstract.PrivateGetSetReferenceType); + Assert.Equal("42", duckVirtual.PrivateGetSetReferenceType); + + duckAbstract.PrivateGetSetReferenceType = "50"; + Assert.Equal("50", duckInterface.PrivateGetSetReferenceType); + Assert.Equal("50", duckAbstract.PrivateGetSetReferenceType); + Assert.Equal("50", duckVirtual.PrivateGetSetReferenceType); + + duckVirtual.PrivateGetSetReferenceType = "60"; + Assert.Equal("60", duckInterface.PrivateGetSetReferenceType); + Assert.Equal("60", duckAbstract.PrivateGetSetReferenceType); + Assert.Equal("60", duckVirtual.PrivateGetSetReferenceType); + + duckInterface.PrivateGetSetReferenceType = "43"; + } + + [Theory] + [MemberData(nameof(Data))] + public void Indexer(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + duckInterface["1"] = "100"; + Assert.Equal("100", duckInterface["1"]); + Assert.Equal("100", duckAbstract["1"]); + Assert.Equal("100", duckVirtual["1"]); + + duckAbstract["2"] = "200"; + Assert.Equal("200", duckInterface["2"]); + Assert.Equal("200", duckAbstract["2"]); + Assert.Equal("200", duckVirtual["2"]); + + duckVirtual["3"] = "300"; + Assert.Equal("300", duckInterface["3"]); + Assert.Equal("300", duckAbstract["3"]); + Assert.Equal("300", duckVirtual["3"]); + } + + [Theory] + [MemberData(nameof(Data))] + public void StructCopy(object obscureObject) + { + var duckStructCopy = obscureObject.DuckCast(); + + Assert.Equal("10", duckStructCopy.PublicStaticGetReferenceType); + Assert.Equal("11", duckStructCopy.InternalStaticGetReferenceType); + Assert.Equal("12", duckStructCopy.ProtectedStaticGetReferenceType); + Assert.Equal("13", duckStructCopy.PrivateStaticGetReferenceType); + + Assert.Equal("20", duckStructCopy.PublicStaticGetSetReferenceType); + Assert.Equal("21", duckStructCopy.InternalStaticGetSetReferenceType); + Assert.Equal("22", duckStructCopy.ProtectedStaticGetSetReferenceType); + Assert.Equal("23", duckStructCopy.PrivateStaticGetSetReferenceType); + + Assert.Equal("30", duckStructCopy.PublicGetReferenceType); + Assert.Equal("31", duckStructCopy.InternalGetReferenceType); + Assert.Equal("32", duckStructCopy.ProtectedGetReferenceType); + Assert.Equal("33", duckStructCopy.PrivateGetReferenceType); + + Assert.Equal("40", duckStructCopy.PublicGetSetReferenceType); + Assert.Equal("41", duckStructCopy.InternalGetSetReferenceType); + Assert.Equal("42", duckStructCopy.ProtectedGetSetReferenceType); + Assert.Equal("43", duckStructCopy.PrivateGetSetReferenceType); + } + + [Theory] + [MemberData(nameof(Data))] + public void UnionTest(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + + Assert.Equal("40", duckInterface.PublicGetSetReferenceType); + + duckInterface.PublicGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.PublicGetSetReferenceType); + + duckInterface.PublicGetSetReferenceType = "40"; + + // * + + Assert.Equal("41", duckInterface.InternalGetSetReferenceType); + + duckInterface.InternalGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.InternalGetSetReferenceType); + + duckInterface.InternalGetSetReferenceType = "41"; + + // * + + Assert.Equal("42", duckInterface.ProtectedGetSetReferenceType); + + duckInterface.ProtectedGetSetReferenceType = "45"; + Assert.Equal("45", duckInterface.ProtectedGetSetReferenceType); + + duckInterface.ProtectedGetSetReferenceType = "42"; + + // * + + Assert.Equal("43", duckInterface.PrivateGetSetReferenceType); + + duckInterface.PrivateGetSetReferenceType = "42"; + Assert.Equal("42", duckInterface.PrivateGetSetReferenceType); + + duckInterface.PrivateGetSetReferenceType = "43"; + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/DummyFieldStruct.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/DummyFieldStruct.cs new file mode 100644 index 000000000..6e8f8646d --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/DummyFieldStruct.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ +#pragma warning disable 649 + + [DuckCopy] + public struct DummyFieldStruct + { + [Duck(Kind = DuckKind.Field)] + public int MagicNumber; + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IDummyFieldObject.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IDummyFieldObject.cs new file mode 100644 index 000000000..e67fb3ba4 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IDummyFieldObject.cs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ + public interface IDummyFieldObject + { + [Duck(Kind = DuckKind.Field)] + int MagicNumber { get; set; } + + ITypesTuple this[ITypesTuple index] { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureDuckType.cs new file mode 100644 index 000000000..4d19cfad3 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureDuckType.cs @@ -0,0 +1,57 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ + public interface IObscureDuckType + { + IDummyFieldObject PublicStaticGetSelfType { get; } + + IDummyFieldObject InternalStaticGetSelfType { get; } + + IDummyFieldObject ProtectedStaticGetSelfType { get; } + + IDummyFieldObject PrivateStaticGetSelfType { get; } + + // * + + IDummyFieldObject PublicStaticGetSetSelfType { get; set; } + + IDummyFieldObject InternalStaticGetSetSelfType { get; set; } + + IDummyFieldObject ProtectedStaticGetSetSelfType { get; set; } + + IDummyFieldObject PrivateStaticGetSetSelfType { get; set; } + + // * + + IDummyFieldObject PublicGetSelfType { get; } + + IDummyFieldObject InternalGetSelfType { get; } + + IDummyFieldObject ProtectedGetSelfType { get; } + + IDummyFieldObject PrivateGetSelfType { get; } + + // * + + IDummyFieldObject PublicGetSetSelfType { get; set; } + + IDummyFieldObject InternalGetSetSelfType { get; set; } + + IDummyFieldObject ProtectedGetSetSelfType { get; set; } + + IDummyFieldObject PrivateGetSetSelfType { get; set; } + + // * + + IDummyFieldObject PrivateDummyGetSetSelfType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureErrorDuckType.cs new file mode 100644 index 000000000..07125032c --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureErrorDuckType.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ + public interface IObscureErrorDuckType + { + IDummyFieldObject PublicGetSelfType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureStaticErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureStaticErrorDuckType.cs new file mode 100644 index 000000000..f867d09c8 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/IObscureStaticErrorDuckType.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ + public interface IObscureStaticErrorDuckType + { + IDummyFieldObject PublicStaticGetSelfType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ITypesTuple.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ITypesTuple.cs new file mode 100644 index 000000000..f05f28328 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ITypesTuple.cs @@ -0,0 +1,24 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ + public interface ITypesTuple + { + [Duck(Kind = DuckKind.Field)] + Type ProxyDefinitionType { get; } + + [Duck(Kind = DuckKind.Field)] + Type TargetType { get; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs new file mode 100644 index 000000000..e722c3f8c --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs @@ -0,0 +1,53 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ + public abstract class ObscureDuckTypeAbstractClass + { + public abstract IDummyFieldObject PublicStaticGetSelfType { get; } + + public abstract IDummyFieldObject InternalStaticGetSelfType { get; } + + public abstract IDummyFieldObject ProtectedStaticGetSelfType { get; } + + public abstract IDummyFieldObject PrivateStaticGetSelfType { get; } + + // * + + public abstract IDummyFieldObject PublicStaticGetSetSelfType { get; set; } + + public abstract IDummyFieldObject InternalStaticGetSetSelfType { get; set; } + + public abstract IDummyFieldObject ProtectedStaticGetSetSelfType { get; set; } + + public abstract IDummyFieldObject PrivateStaticGetSetSelfType { get; set; } + + // * + + public abstract IDummyFieldObject PublicGetSelfType { get; } + + public abstract IDummyFieldObject InternalGetSelfType { get; } + + public abstract IDummyFieldObject ProtectedGetSelfType { get; } + + public abstract IDummyFieldObject PrivateGetSelfType { get; } + + // * + + public abstract IDummyFieldObject PublicGetSetSelfType { get; set; } + + public abstract IDummyFieldObject InternalGetSetSelfType { get; set; } + + public abstract IDummyFieldObject ProtectedGetSetSelfType { get; set; } + + public abstract IDummyFieldObject PrivateGetSetSelfType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeStruct.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeStruct.cs new file mode 100644 index 000000000..394dabf9d --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeStruct.cs @@ -0,0 +1,40 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ +#pragma warning disable 649 + + [DuckCopy] + public struct ObscureDuckTypeStruct + { + public DummyFieldStruct PublicStaticGetSelfType; + public DummyFieldStruct InternalStaticGetSelfType; + public DummyFieldStruct ProtectedStaticGetSelfType; + public DummyFieldStruct PrivateStaticGetSelfType; + + public DummyFieldStruct PublicStaticGetSetSelfType; + public DummyFieldStruct InternalStaticGetSetSelfType; + public DummyFieldStruct ProtectedStaticGetSetSelfType; + public DummyFieldStruct PrivateStaticGetSetSelfType; + + public DummyFieldStruct PublicGetSelfType; + public DummyFieldStruct InternalGetSelfType; + public DummyFieldStruct ProtectedGetSelfType; + public DummyFieldStruct PrivateGetSelfType; + + public DummyFieldStruct PublicGetSetSelfType; + public DummyFieldStruct InternalGetSetSelfType; + public DummyFieldStruct ProtectedGetSetSelfType; + public DummyFieldStruct PrivateGetSetSelfType; + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs new file mode 100644 index 000000000..5f35d6904 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs @@ -0,0 +1,53 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions +{ + public class ObscureDuckTypeVirtualClass + { + public virtual IDummyFieldObject PublicStaticGetSelfType { get; } + + public virtual IDummyFieldObject InternalStaticGetSelfType { get; } + + public virtual IDummyFieldObject ProtectedStaticGetSelfType { get; } + + public virtual IDummyFieldObject PrivateStaticGetSelfType { get; } + + // * + + public virtual IDummyFieldObject PublicStaticGetSetSelfType { get; set; } + + public virtual IDummyFieldObject InternalStaticGetSetSelfType { get; set; } + + public virtual IDummyFieldObject ProtectedStaticGetSetSelfType { get; set; } + + public virtual IDummyFieldObject PrivateStaticGetSetSelfType { get; set; } + + // * + + public virtual IDummyFieldObject PublicGetSelfType { get; } + + public virtual IDummyFieldObject InternalGetSelfType { get; } + + public virtual IDummyFieldObject ProtectedGetSelfType { get; } + + public virtual IDummyFieldObject PrivateGetSelfType { get; } + + // * + + public virtual IDummyFieldObject PublicGetSetSelfType { get; set; } + + public virtual IDummyFieldObject InternalGetSetSelfType { get; set; } + + public virtual IDummyFieldObject ProtectedGetSetSelfType { get; set; } + + public virtual IDummyFieldObject PrivateGetSetSelfType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/TypeChainingPropertyTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/TypeChainingPropertyTests.cs new file mode 100644 index 000000000..5342119c0 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/TypeChaining/TypeChainingPropertyTests.cs @@ -0,0 +1,279 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining.ProxiesDefinitions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.TypeChaining +{ + public class TypeChainingPropertyTests + { + public static IEnumerable Data() + { + return new[] + { + new object[] { ObscureObject.GetPropertyPublicObject() }, + new object[] { ObscureObject.GetPropertyInternalObject() }, + new object[] { ObscureObject.GetPropertyPrivateObject() }, + }; + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticGetOnlyPropertyException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void GetOnlyPropertyException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticGetOnlyProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + + Assert.Equal(42, duckInterface.PublicStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.PublicStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.PublicStaticGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PublicStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PublicStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PublicStaticGetSelfType).Instance); + + // * + + Assert.Equal(42, duckInterface.InternalStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.InternalStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.InternalStaticGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.InternalStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.InternalStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.InternalStaticGetSelfType).Instance); + + // * + + Assert.Equal(42, duckInterface.ProtectedStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.ProtectedStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.ProtectedStaticGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.ProtectedStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.ProtectedStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.ProtectedStaticGetSelfType).Instance); + + // * + + Assert.Equal(42, duckInterface.PrivateStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.PrivateStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.PrivateStaticGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PrivateStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PrivateStaticGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PrivateStaticGetSelfType).Instance); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + IDummyFieldObject newDummy = null; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.PublicStaticGetSetSelfType = newDummy; + + Assert.Equal(42, duckInterface.PublicStaticGetSetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.PublicStaticGetSetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.PublicStaticGetSetSelfType.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 52 }).DuckCast(); + duckInterface.InternalStaticGetSetSelfType = newDummy; + + Assert.Equal(52, duckInterface.InternalStaticGetSetSelfType.MagicNumber); + Assert.Equal(52, duckAbstract.InternalStaticGetSetSelfType.MagicNumber); + Assert.Equal(52, duckVirtual.InternalStaticGetSetSelfType.MagicNumber); + + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.InternalStaticGetSetSelfType = newDummy; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 62 }).DuckCast(); + duckAbstract.ProtectedStaticGetSetSelfType = newDummy; + + Assert.Equal(62, duckInterface.ProtectedStaticGetSetSelfType.MagicNumber); + Assert.Equal(62, duckAbstract.ProtectedStaticGetSetSelfType.MagicNumber); + Assert.Equal(62, duckVirtual.ProtectedStaticGetSetSelfType.MagicNumber); + + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckAbstract.ProtectedStaticGetSetSelfType = newDummy; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 72 }).DuckCast(); + duckAbstract.PrivateStaticGetSetSelfType = newDummy; + + Assert.Equal(72, duckInterface.PrivateStaticGetSetSelfType.MagicNumber); + Assert.Equal(72, duckAbstract.PrivateStaticGetSetSelfType.MagicNumber); + Assert.Equal(72, duckVirtual.PrivateStaticGetSetSelfType.MagicNumber); + + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckAbstract.PrivateStaticGetSetSelfType = newDummy; + } + + [Theory] + [MemberData(nameof(Data))] + public void GetOnlyProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + + Assert.Equal(42, duckInterface.PublicGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.PublicGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.PublicGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PublicGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PublicGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PublicGetSelfType).Instance); + + // * + + Assert.Equal(42, duckInterface.InternalGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.InternalGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.InternalGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.InternalGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.InternalGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.InternalGetSelfType).Instance); + + // * + + Assert.Equal(42, duckInterface.ProtectedGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.ProtectedGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.ProtectedGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.ProtectedGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.ProtectedGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.ProtectedGetSelfType).Instance); + + // * + + Assert.Equal(42, duckInterface.PrivateGetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.PrivateGetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.PrivateGetSelfType.MagicNumber); + + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckInterface.PrivateGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckAbstract.PrivateGetSelfType).Instance); + Assert.Equal(ObscureObject.DummyFieldObject.Default, ((IDuckType)duckVirtual.PrivateGetSelfType).Instance); + } + + [Theory] + [MemberData(nameof(Data))] + public void Properties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + IDummyFieldObject newDummy = null; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.PublicGetSetSelfType = newDummy; + + Assert.Equal(42, duckInterface.PublicGetSetSelfType.MagicNumber); + Assert.Equal(42, duckAbstract.PublicGetSetSelfType.MagicNumber); + Assert.Equal(42, duckVirtual.PublicGetSetSelfType.MagicNumber); + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 52 }).DuckCast(); + duckInterface.InternalGetSetSelfType = newDummy; + + Assert.Equal(52, duckInterface.InternalGetSetSelfType.MagicNumber); + Assert.Equal(52, duckAbstract.InternalGetSetSelfType.MagicNumber); + Assert.Equal(52, duckVirtual.InternalGetSetSelfType.MagicNumber); + + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.InternalGetSetSelfType = newDummy; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 62 }).DuckCast(); + duckInterface.ProtectedGetSetSelfType = newDummy; + + Assert.Equal(62, duckInterface.ProtectedGetSetSelfType.MagicNumber); + Assert.Equal(62, duckAbstract.ProtectedGetSetSelfType.MagicNumber); + Assert.Equal(62, duckVirtual.ProtectedGetSetSelfType.MagicNumber); + + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.ProtectedGetSetSelfType = newDummy; + + // * + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 72 }).DuckCast(); + duckInterface.PrivateGetSetSelfType = newDummy; + + Assert.Equal(72, duckInterface.PrivateGetSetSelfType.MagicNumber); + Assert.Equal(72, duckAbstract.PrivateGetSetSelfType.MagicNumber); + Assert.Equal(72, duckVirtual.PrivateGetSetSelfType.MagicNumber); + + newDummy = (new ObscureObject.DummyFieldObject { MagicNumber = 42 }).DuckCast(); + duckInterface.PrivateGetSetSelfType = newDummy; + } + + [Theory] + [MemberData(nameof(Data))] + public void StructCopy(object obscureObject) + { + var duckStructCopy = obscureObject.DuckCast(); + + Assert.Equal(42, duckStructCopy.PublicStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.InternalStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.ProtectedStaticGetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.PrivateStaticGetSelfType.MagicNumber); + + Assert.Equal(42, duckStructCopy.PublicStaticGetSetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.InternalStaticGetSetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.ProtectedStaticGetSetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.PrivateStaticGetSetSelfType.MagicNumber); + + Assert.Equal(42, duckStructCopy.PublicGetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.InternalGetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.ProtectedGetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.PrivateGetSelfType.MagicNumber); + + Assert.Equal(42, duckStructCopy.PublicGetSetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.InternalGetSetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.ProtectedGetSetSelfType.MagicNumber); + Assert.Equal(42, duckStructCopy.PrivateGetSetSelfType.MagicNumber); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureDuckType.cs new file mode 100644 index 000000000..2b0e8b3f9 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureDuckType.cs @@ -0,0 +1,73 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading.Tasks; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions +{ + public interface IObscureDuckType + { + int PublicStaticGetValueType { get; } + + int InternalStaticGetValueType { get; } + + int ProtectedStaticGetValueType { get; } + + int PrivateStaticGetValueType { get; } + + // * + + int PublicStaticGetSetValueType { get; set; } + + int InternalStaticGetSetValueType { get; set; } + + int ProtectedStaticGetSetValueType { get; set; } + + int PrivateStaticGetSetValueType { get; set; } + + // * + + int PublicGetValueType { get; } + + int InternalGetValueType { get; } + + int ProtectedGetValueType { get; } + + int PrivateGetValueType { get; } + + // * + + int PublicGetSetValueType { get; set; } + + int InternalGetSetValueType { get; set; } + + int ProtectedGetSetValueType { get; set; } + + int PrivateGetSetValueType { get; set; } + + // * + + int? PublicStaticNullableInt { get; set; } + + int? PrivateStaticNullableInt { get; set; } + + int? PublicNullableInt { get; set; } + + int? PrivateNullableInt { get; set; } + + // * + + TaskStatus Status { get; set; } + + // * + + int this[int index] { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureErrorDuckType.cs new file mode 100644 index 000000000..c10b09e07 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureErrorDuckType.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions +{ + public interface IObscureErrorDuckType + { + int PublicGetValueType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureStaticErrorDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureStaticErrorDuckType.cs new file mode 100644 index 000000000..9d1d92aee --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IObscureStaticErrorDuckType.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions +{ + public interface IObscureStaticErrorDuckType + { + int PublicStaticGetValueType { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IStructDuckType.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IStructDuckType.cs new file mode 100644 index 000000000..b24764b22 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/IStructDuckType.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions +{ + public interface IStructDuckType : IDuckType + { + int PublicGetSetValueType { get; } + + int PrivateGetSetValueType { get; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs new file mode 100644 index 000000000..885056878 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeAbstractClass.cs @@ -0,0 +1,73 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading.Tasks; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions +{ + public abstract class ObscureDuckTypeAbstractClass + { + public abstract int PublicStaticGetValueType { get; } + + public abstract int InternalStaticGetValueType { get; } + + public abstract int ProtectedStaticGetValueType { get; } + + public abstract int PrivateStaticGetValueType { get; } + + // * + + public abstract int PublicStaticGetSetValueType { get; set; } + + public abstract int InternalStaticGetSetValueType { get; set; } + + public abstract int ProtectedStaticGetSetValueType { get; set; } + + public abstract int PrivateStaticGetSetValueType { get; set; } + + // * + + public abstract int PublicGetValueType { get; } + + public abstract int InternalGetValueType { get; } + + public abstract int ProtectedGetValueType { get; } + + public abstract int PrivateGetValueType { get; } + + // * + + public abstract int PublicGetSetValueType { get; set; } + + public abstract int InternalGetSetValueType { get; set; } + + public abstract int ProtectedGetSetValueType { get; set; } + + public abstract int PrivateGetSetValueType { get; set; } + + // * + + public abstract int? PublicStaticNullableInt { get; set; } + + public abstract int? PrivateStaticNullableInt { get; set; } + + public abstract int? PublicNullableInt { get; set; } + + public abstract int? PrivateNullableInt { get; set; } + + // * + + public abstract TaskStatus Status { get; set; } + + // * + + public abstract int this[int index] { get; set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeStruct.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeStruct.cs new file mode 100644 index 000000000..b7633733e --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeStruct.cs @@ -0,0 +1,40 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions +{ +#pragma warning disable 649 + + [DuckCopy] + internal struct ObscureDuckTypeStruct + { + public int PublicStaticGetValueType; + public int InternalStaticGetValueType; + public int ProtectedStaticGetValueType; + public int PrivateStaticGetValueType; + + public int PublicStaticGetSetValueType; + public int InternalStaticGetSetValueType; + public int ProtectedStaticGetSetValueType; + public int PrivateStaticGetSetValueType; + + public int PublicGetValueType; + public int InternalGetValueType; + public int ProtectedGetValueType; + public int PrivateGetValueType; + + public int PublicGetSetValueType; + public int InternalGetSetValueType; + public int ProtectedGetSetValueType; + public int PrivateGetSetValueType; + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs new file mode 100644 index 000000000..9e4d14247 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ProxiesDefinitions/ObscureDuckTypeVirtualClass.cs @@ -0,0 +1,81 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading.Tasks; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions +{ + public class ObscureDuckTypeVirtualClass + { + public virtual int PublicStaticGetValueType { get; } + + public virtual int InternalStaticGetValueType { get; } + + public virtual int ProtectedStaticGetValueType { get; } + + public virtual int PrivateStaticGetValueType { get; } + + // * + + public virtual int PublicStaticGetSetValueType { get; set; } + + public virtual int InternalStaticGetSetValueType { get; set; } + + public virtual int ProtectedStaticGetSetValueType { get; set; } + + public virtual int PrivateStaticGetSetValueType { get; set; } + + // * + + public virtual int PublicGetValueType { get; } + + public virtual int InternalGetValueType { get; } + + public virtual int ProtectedGetValueType { get; } + + public virtual int PrivateGetValueType { get; } + + // * + + public virtual int PublicGetSetValueType { get; set; } + + public virtual int InternalGetSetValueType { get; set; } + + public virtual int ProtectedGetSetValueType { get; set; } + + public virtual int PrivateGetSetValueType { get; set; } + + // * + + public virtual int? PublicStaticNullableInt { get; set; } + + public virtual int? PrivateStaticNullableInt { get; set; } + + public virtual int? PublicNullableInt { get; set; } + + public virtual int? PrivateNullableInt { get; set; } + + // * + + public virtual TaskStatus Status + { + get => default; + set { } + } + + // * + + public virtual int this[int index] + { + get => default; + set { } + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ValueTypePropertyTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ValueTypePropertyTests.cs new file mode 100644 index 000000000..2800f11c3 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/Properties/ValueType/ValueTypePropertyTests.cs @@ -0,0 +1,490 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using System.Threading.Tasks; +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType.ProxiesDefinitions; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping.Properties.ValueType +{ + public partial class ValueTypePropertyTests + { + public static IEnumerable Data() + { + return new[] + { + new object[] { ObscureObject.GetPropertyPublicObject() }, + new object[] { ObscureObject.GetPropertyInternalObject() }, + new object[] { ObscureObject.GetPropertyPrivateObject() }, + }; + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticGetOnlyPropertyException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void GetOnlyPropertyException(object obscureObject) + { + Assert.Throws(() => + { + obscureObject.DuckCast(); + }); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticGetOnlyProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal(10, duckInterface.PublicStaticGetValueType); + Assert.Equal(10, duckAbstract.PublicStaticGetValueType); + Assert.Equal(10, duckVirtual.PublicStaticGetValueType); + + // * + Assert.Equal(11, duckInterface.InternalStaticGetValueType); + Assert.Equal(11, duckAbstract.InternalStaticGetValueType); + Assert.Equal(11, duckVirtual.InternalStaticGetValueType); + + // * + Assert.Equal(12, duckInterface.ProtectedStaticGetValueType); + Assert.Equal(12, duckAbstract.ProtectedStaticGetValueType); + Assert.Equal(12, duckVirtual.ProtectedStaticGetValueType); + + // * + Assert.Equal(13, duckInterface.PrivateStaticGetValueType); + Assert.Equal(13, duckAbstract.PrivateStaticGetValueType); + Assert.Equal(13, duckVirtual.PrivateStaticGetValueType); + } + + [Theory] + [MemberData(nameof(Data))] + public void StaticProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal(20, duckInterface.PublicStaticGetSetValueType); + Assert.Equal(20, duckAbstract.PublicStaticGetSetValueType); + Assert.Equal(20, duckVirtual.PublicStaticGetSetValueType); + + duckInterface.PublicStaticGetSetValueType = 42; + Assert.Equal(42, duckInterface.PublicStaticGetSetValueType); + Assert.Equal(42, duckAbstract.PublicStaticGetSetValueType); + Assert.Equal(42, duckVirtual.PublicStaticGetSetValueType); + + duckAbstract.PublicStaticGetSetValueType = 50; + Assert.Equal(50, duckInterface.PublicStaticGetSetValueType); + Assert.Equal(50, duckAbstract.PublicStaticGetSetValueType); + Assert.Equal(50, duckVirtual.PublicStaticGetSetValueType); + + duckVirtual.PublicStaticGetSetValueType = 60; + Assert.Equal(60, duckInterface.PublicStaticGetSetValueType); + Assert.Equal(60, duckAbstract.PublicStaticGetSetValueType); + Assert.Equal(60, duckVirtual.PublicStaticGetSetValueType); + + duckInterface.PublicStaticGetSetValueType = 20; + + // * + + Assert.Equal(21, duckInterface.InternalStaticGetSetValueType); + Assert.Equal(21, duckAbstract.InternalStaticGetSetValueType); + Assert.Equal(21, duckVirtual.InternalStaticGetSetValueType); + + duckInterface.InternalStaticGetSetValueType = 42; + Assert.Equal(42, duckInterface.InternalStaticGetSetValueType); + Assert.Equal(42, duckAbstract.InternalStaticGetSetValueType); + Assert.Equal(42, duckVirtual.InternalStaticGetSetValueType); + + duckAbstract.InternalStaticGetSetValueType = 50; + Assert.Equal(50, duckInterface.InternalStaticGetSetValueType); + Assert.Equal(50, duckAbstract.InternalStaticGetSetValueType); + Assert.Equal(50, duckVirtual.InternalStaticGetSetValueType); + + duckVirtual.InternalStaticGetSetValueType = 60; + Assert.Equal(60, duckInterface.InternalStaticGetSetValueType); + Assert.Equal(60, duckAbstract.InternalStaticGetSetValueType); + Assert.Equal(60, duckVirtual.InternalStaticGetSetValueType); + + duckInterface.InternalStaticGetSetValueType = 21; + + // * + + Assert.Equal(22, duckInterface.ProtectedStaticGetSetValueType); + Assert.Equal(22, duckAbstract.ProtectedStaticGetSetValueType); + Assert.Equal(22, duckVirtual.ProtectedStaticGetSetValueType); + + duckInterface.ProtectedStaticGetSetValueType = 42; + Assert.Equal(42, duckInterface.ProtectedStaticGetSetValueType); + Assert.Equal(42, duckAbstract.ProtectedStaticGetSetValueType); + Assert.Equal(42, duckVirtual.ProtectedStaticGetSetValueType); + + duckAbstract.ProtectedStaticGetSetValueType = 50; + Assert.Equal(50, duckInterface.ProtectedStaticGetSetValueType); + Assert.Equal(50, duckAbstract.ProtectedStaticGetSetValueType); + Assert.Equal(50, duckVirtual.ProtectedStaticGetSetValueType); + + duckVirtual.ProtectedStaticGetSetValueType = 60; + Assert.Equal(60, duckInterface.ProtectedStaticGetSetValueType); + Assert.Equal(60, duckAbstract.ProtectedStaticGetSetValueType); + Assert.Equal(60, duckVirtual.ProtectedStaticGetSetValueType); + + duckInterface.ProtectedStaticGetSetValueType = 22; + + // * + + Assert.Equal(23, duckInterface.PrivateStaticGetSetValueType); + Assert.Equal(23, duckAbstract.PrivateStaticGetSetValueType); + Assert.Equal(23, duckVirtual.PrivateStaticGetSetValueType); + + duckInterface.PrivateStaticGetSetValueType = 42; + Assert.Equal(42, duckInterface.PrivateStaticGetSetValueType); + Assert.Equal(42, duckAbstract.PrivateStaticGetSetValueType); + Assert.Equal(42, duckVirtual.PrivateStaticGetSetValueType); + + duckAbstract.PrivateStaticGetSetValueType = 50; + Assert.Equal(50, duckInterface.PrivateStaticGetSetValueType); + Assert.Equal(50, duckAbstract.PrivateStaticGetSetValueType); + Assert.Equal(50, duckVirtual.PrivateStaticGetSetValueType); + + duckVirtual.PrivateStaticGetSetValueType = 60; + Assert.Equal(60, duckInterface.PrivateStaticGetSetValueType); + Assert.Equal(60, duckAbstract.PrivateStaticGetSetValueType); + Assert.Equal(60, duckVirtual.PrivateStaticGetSetValueType); + + duckInterface.PrivateStaticGetSetValueType = 23; + } + + [Theory] + [MemberData(nameof(Data))] + public void GetOnlyProperties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + // * + Assert.Equal(30, duckInterface.PublicGetValueType); + Assert.Equal(30, duckAbstract.PublicGetValueType); + Assert.Equal(30, duckVirtual.PublicGetValueType); + + // * + Assert.Equal(31, duckInterface.InternalGetValueType); + Assert.Equal(31, duckAbstract.InternalGetValueType); + Assert.Equal(31, duckVirtual.InternalGetValueType); + + // * + Assert.Equal(32, duckInterface.ProtectedGetValueType); + Assert.Equal(32, duckAbstract.ProtectedGetValueType); + Assert.Equal(32, duckVirtual.ProtectedGetValueType); + + // * + Assert.Equal(33, duckInterface.PrivateGetValueType); + Assert.Equal(33, duckAbstract.PrivateGetValueType); + Assert.Equal(33, duckVirtual.PrivateGetValueType); + } + + [Theory] + [MemberData(nameof(Data))] + public void Properties(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal(40, duckInterface.PublicGetSetValueType); + Assert.Equal(40, duckAbstract.PublicGetSetValueType); + Assert.Equal(40, duckVirtual.PublicGetSetValueType); + + duckInterface.PublicGetSetValueType = 42; + Assert.Equal(42, duckInterface.PublicGetSetValueType); + Assert.Equal(42, duckAbstract.PublicGetSetValueType); + Assert.Equal(42, duckVirtual.PublicGetSetValueType); + + duckAbstract.PublicGetSetValueType = 50; + Assert.Equal(50, duckInterface.PublicGetSetValueType); + Assert.Equal(50, duckAbstract.PublicGetSetValueType); + Assert.Equal(50, duckVirtual.PublicGetSetValueType); + + duckVirtual.PublicGetSetValueType = 60; + Assert.Equal(60, duckInterface.PublicGetSetValueType); + Assert.Equal(60, duckAbstract.PublicGetSetValueType); + Assert.Equal(60, duckVirtual.PublicGetSetValueType); + + duckInterface.PublicGetSetValueType = 40; + + // * + + Assert.Equal(41, duckInterface.InternalGetSetValueType); + Assert.Equal(41, duckAbstract.InternalGetSetValueType); + Assert.Equal(41, duckVirtual.InternalGetSetValueType); + + duckInterface.InternalGetSetValueType = 42; + Assert.Equal(42, duckInterface.InternalGetSetValueType); + Assert.Equal(42, duckAbstract.InternalGetSetValueType); + Assert.Equal(42, duckVirtual.InternalGetSetValueType); + + duckAbstract.InternalGetSetValueType = 50; + Assert.Equal(50, duckInterface.InternalGetSetValueType); + Assert.Equal(50, duckAbstract.InternalGetSetValueType); + Assert.Equal(50, duckVirtual.InternalGetSetValueType); + + duckVirtual.InternalGetSetValueType = 60; + Assert.Equal(60, duckInterface.InternalGetSetValueType); + Assert.Equal(60, duckAbstract.InternalGetSetValueType); + Assert.Equal(60, duckVirtual.InternalGetSetValueType); + + duckInterface.InternalGetSetValueType = 41; + + // * + + Assert.Equal(42, duckInterface.ProtectedGetSetValueType); + Assert.Equal(42, duckAbstract.ProtectedGetSetValueType); + Assert.Equal(42, duckVirtual.ProtectedGetSetValueType); + + duckInterface.ProtectedGetSetValueType = 45; + Assert.Equal(45, duckInterface.ProtectedGetSetValueType); + Assert.Equal(45, duckAbstract.ProtectedGetSetValueType); + Assert.Equal(45, duckVirtual.ProtectedGetSetValueType); + + duckAbstract.ProtectedGetSetValueType = 50; + Assert.Equal(50, duckInterface.ProtectedGetSetValueType); + Assert.Equal(50, duckAbstract.ProtectedGetSetValueType); + Assert.Equal(50, duckVirtual.ProtectedGetSetValueType); + + duckVirtual.ProtectedGetSetValueType = 60; + Assert.Equal(60, duckInterface.ProtectedGetSetValueType); + Assert.Equal(60, duckAbstract.ProtectedGetSetValueType); + Assert.Equal(60, duckVirtual.ProtectedGetSetValueType); + + duckInterface.ProtectedGetSetValueType = 42; + + // * + + Assert.Equal(43, duckInterface.PrivateGetSetValueType); + Assert.Equal(43, duckAbstract.PrivateGetSetValueType); + Assert.Equal(43, duckVirtual.PrivateGetSetValueType); + + duckInterface.PrivateGetSetValueType = 42; + Assert.Equal(42, duckInterface.PrivateGetSetValueType); + Assert.Equal(42, duckAbstract.PrivateGetSetValueType); + Assert.Equal(42, duckVirtual.PrivateGetSetValueType); + + duckAbstract.PrivateGetSetValueType = 50; + Assert.Equal(50, duckInterface.PrivateGetSetValueType); + Assert.Equal(50, duckAbstract.PrivateGetSetValueType); + Assert.Equal(50, duckVirtual.PrivateGetSetValueType); + + duckVirtual.PrivateGetSetValueType = 60; + Assert.Equal(60, duckInterface.PrivateGetSetValueType); + Assert.Equal(60, duckAbstract.PrivateGetSetValueType); + Assert.Equal(60, duckVirtual.PrivateGetSetValueType); + + duckInterface.PrivateGetSetValueType = 43; + } + + [Theory] + [MemberData(nameof(Data))] + public void Indexer(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + duckInterface[1] = 100; + Assert.Equal(100, duckInterface[1]); + Assert.Equal(100, duckAbstract[1]); + Assert.Equal(100, duckVirtual[1]); + + duckAbstract[2] = 200; + Assert.Equal(200, duckInterface[2]); + Assert.Equal(200, duckAbstract[2]); + Assert.Equal(200, duckVirtual[2]); + + duckVirtual[3] = 300; + Assert.Equal(300, duckInterface[3]); + Assert.Equal(300, duckAbstract[3]); + Assert.Equal(300, duckVirtual[3]); + } + + [Theory] + [MemberData(nameof(Data))] + public void NullableOfKnown(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Null(duckInterface.PublicStaticNullableInt); + Assert.Null(duckAbstract.PublicStaticNullableInt); + Assert.Null(duckVirtual.PublicStaticNullableInt); + + duckInterface.PublicStaticNullableInt = 42; + Assert.Equal(42, duckInterface.PublicStaticNullableInt); + Assert.Equal(42, duckAbstract.PublicStaticNullableInt); + Assert.Equal(42, duckVirtual.PublicStaticNullableInt); + + duckAbstract.PublicStaticNullableInt = 50; + Assert.Equal(50, duckInterface.PublicStaticNullableInt); + Assert.Equal(50, duckAbstract.PublicStaticNullableInt); + Assert.Equal(50, duckVirtual.PublicStaticNullableInt); + + duckVirtual.PublicStaticNullableInt = null; + Assert.Null(duckInterface.PublicStaticNullableInt); + Assert.Null(duckAbstract.PublicStaticNullableInt); + Assert.Null(duckVirtual.PublicStaticNullableInt); + + // * + + Assert.Null(duckInterface.PrivateStaticNullableInt); + Assert.Null(duckAbstract.PrivateStaticNullableInt); + Assert.Null(duckVirtual.PrivateStaticNullableInt); + + duckInterface.PrivateStaticNullableInt = 42; + Assert.Equal(42, duckInterface.PrivateStaticNullableInt); + Assert.Equal(42, duckAbstract.PrivateStaticNullableInt); + Assert.Equal(42, duckVirtual.PrivateStaticNullableInt); + + duckAbstract.PrivateStaticNullableInt = 50; + Assert.Equal(50, duckInterface.PrivateStaticNullableInt); + Assert.Equal(50, duckAbstract.PrivateStaticNullableInt); + Assert.Equal(50, duckVirtual.PrivateStaticNullableInt); + + duckVirtual.PrivateStaticNullableInt = null; + Assert.Null(duckInterface.PrivateStaticNullableInt); + Assert.Null(duckAbstract.PrivateStaticNullableInt); + Assert.Null(duckVirtual.PrivateStaticNullableInt); + + // * + + Assert.Null(duckInterface.PublicNullableInt); + Assert.Null(duckAbstract.PublicNullableInt); + Assert.Null(duckVirtual.PublicNullableInt); + + duckInterface.PublicNullableInt = 42; + Assert.Equal(42, duckInterface.PublicNullableInt); + Assert.Equal(42, duckAbstract.PublicNullableInt); + Assert.Equal(42, duckVirtual.PublicNullableInt); + + duckAbstract.PublicNullableInt = 50; + Assert.Equal(50, duckInterface.PublicNullableInt); + Assert.Equal(50, duckAbstract.PublicNullableInt); + Assert.Equal(50, duckVirtual.PublicNullableInt); + + duckVirtual.PublicNullableInt = null; + Assert.Null(duckInterface.PublicNullableInt); + Assert.Null(duckAbstract.PublicNullableInt); + Assert.Null(duckVirtual.PublicNullableInt); + + // * + + Assert.Null(duckInterface.PrivateNullableInt); + Assert.Null(duckAbstract.PrivateNullableInt); + Assert.Null(duckVirtual.PrivateNullableInt); + + duckInterface.PrivateNullableInt = 42; + Assert.Equal(42, duckInterface.PrivateNullableInt); + Assert.Equal(42, duckAbstract.PrivateNullableInt); + Assert.Equal(42, duckVirtual.PrivateNullableInt); + + duckAbstract.PrivateNullableInt = 50; + Assert.Equal(50, duckInterface.PrivateNullableInt); + Assert.Equal(50, duckAbstract.PrivateNullableInt); + Assert.Equal(50, duckVirtual.PrivateNullableInt); + + duckVirtual.PrivateNullableInt = null; + Assert.Null(duckInterface.PrivateNullableInt); + Assert.Null(duckAbstract.PrivateNullableInt); + Assert.Null(duckVirtual.PrivateNullableInt); + } + + [Theory] + [MemberData(nameof(Data))] + public void KnownEnum(object obscureObject) + { + var duckInterface = obscureObject.DuckCast(); + var duckAbstract = obscureObject.DuckCast(); + var duckVirtual = obscureObject.DuckCast(); + + Assert.Equal(TaskStatus.RanToCompletion, duckInterface.Status); + Assert.Equal(TaskStatus.RanToCompletion, duckAbstract.Status); + Assert.Equal(TaskStatus.RanToCompletion, duckVirtual.Status); + + duckInterface.Status = TaskStatus.Running; + + Assert.Equal(TaskStatus.Running, duckInterface.Status); + Assert.Equal(TaskStatus.Running, duckAbstract.Status); + Assert.Equal(TaskStatus.Running, duckVirtual.Status); + + duckAbstract.Status = TaskStatus.Faulted; + + Assert.Equal(TaskStatus.Faulted, duckInterface.Status); + Assert.Equal(TaskStatus.Faulted, duckAbstract.Status); + Assert.Equal(TaskStatus.Faulted, duckVirtual.Status); + + duckVirtual.Status = TaskStatus.WaitingForActivation; + + Assert.Equal(TaskStatus.WaitingForActivation, duckInterface.Status); + Assert.Equal(TaskStatus.WaitingForActivation, duckAbstract.Status); + Assert.Equal(TaskStatus.WaitingForActivation, duckVirtual.Status); + } + + [Theory] + [MemberData(nameof(Data))] + public void StructCopy(object obscureObject) + { + var duckStructCopy = obscureObject.DuckCast(); + + Assert.Equal(10, duckStructCopy.PublicStaticGetValueType); + Assert.Equal(11, duckStructCopy.InternalStaticGetValueType); + Assert.Equal(12, duckStructCopy.ProtectedStaticGetValueType); + Assert.Equal(13, duckStructCopy.PrivateStaticGetValueType); + + Assert.Equal(20, duckStructCopy.PublicStaticGetSetValueType); + Assert.Equal(21, duckStructCopy.InternalStaticGetSetValueType); + Assert.Equal(22, duckStructCopy.ProtectedStaticGetSetValueType); + Assert.Equal(23, duckStructCopy.PrivateStaticGetSetValueType); + + Assert.Equal(30, duckStructCopy.PublicGetValueType); + Assert.Equal(31, duckStructCopy.InternalGetValueType); + Assert.Equal(32, duckStructCopy.ProtectedGetValueType); + Assert.Equal(33, duckStructCopy.PrivateGetValueType); + + Assert.Equal(40, duckStructCopy.PublicGetSetValueType); + Assert.Equal(41, duckStructCopy.InternalGetSetValueType); + Assert.Equal(42, duckStructCopy.ProtectedGetSetValueType); + Assert.Equal(43, duckStructCopy.PrivateGetSetValueType); + } + + [Fact] + public void StructDuckType() + { + ObscureObject.PublicStruct source = default; + source.PublicGetSetValueType = 42; + + var dest = source.DuckCast(); + Assert.Equal(source.PublicGetSetValueType, dest.PublicGetSetValueType); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/StructTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/StructTests.cs new file mode 100644 index 000000000..c5b7bf7bf --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/StructTests.cs @@ -0,0 +1,140 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class StructTests + { + [Fact] + public void NonPublicStructCopyTest() + { + PrivateStruct instance = default; + CopyStruct copy = instance.DuckCast(); + Assert.Equal(instance.Value, copy.Value); + } + + [Fact] + public void NonPublicStructInterfaceProxyTest() + { + PrivateStruct instance = default; + IPrivateStruct proxy = instance.DuckCast(); + Assert.Equal(instance.Value, proxy.Value); + } + + [Fact] + public void NonPublicStructAbstractProxyTest() + { + PrivateStruct instance = default; + AbstractPrivateProxy proxy = instance.DuckCast(); + Assert.Equal(instance.Value, proxy.Value); + } + + [Fact] + public void NonPublicStructVirtualProxyTest() + { + PrivateStruct instance = default; + VirtualPrivateProxy proxy = instance.DuckCast(); + Assert.Equal(instance.Value, proxy.Value); + } + + [DuckCopy] + public struct CopyStruct + { + public string Value; + } + + public interface IPrivateStruct + { + string Value { get; } + } + + public abstract class AbstractPrivateProxy + { + public abstract string Value { get; } + } + + public class VirtualPrivateProxy + { + public virtual string Value { get; } + } + + private readonly struct PrivateStruct + { + public readonly string Value => "Hello World"; + } + + [Fact] + public void DuckChainingStructInterfaceProxyTest() + { + PrivateDuckChainingTarget instance = new PrivateDuckChainingTarget(); + IPrivateDuckChainingTarget proxy = instance.DuckCast(); + Assert.Equal(instance.ChainingTestField.Name, proxy.ChainingTestField.Name); + Assert.Equal(instance.ChainingTest.Name, proxy.ChainingTest.Name); + Assert.Equal(instance.ChainingTestMethod().Name, proxy.ChainingTestMethod().Name); + + PublicDuckChainingTarget instance2 = new PublicDuckChainingTarget(); + IPrivateDuckChainingTarget proxy2 = instance2.DuckCast(); + Assert.Equal(instance2.ChainingTestField.Name, proxy2.ChainingTestField.Name); + Assert.Equal(instance2.ChainingTest.Name, proxy2.ChainingTest.Name); + Assert.Equal(instance2.ChainingTestMethod().Name, proxy2.ChainingTestMethod().Name); + } + + public interface IPrivateDuckChainingTarget + { + [Duck(Kind = DuckKind.Field)] + IPrivateTarget ChainingTestField { get; } + + IPrivateTarget ChainingTest { get; } + + IPrivateTarget ChainingTestMethod(); + } + + public interface IPrivateTarget + { + [Duck(Kind = DuckKind.Field)] + public string Name { get; } + } + + private class PrivateDuckChainingTarget + { +#pragma warning disable SA1401 // Fields must be private + public PrivateTarget ChainingTestField = new PrivateTarget { Name = "Hello World 1" }; +#pragma warning restore SA1401 // Fields must be private + + public PrivateTarget ChainingTest => new PrivateTarget { Name = "Hello World 2" }; + + public PrivateTarget ChainingTestMethod() => new PrivateTarget { Name = "Hello World 3" }; + } + + private struct PrivateTarget + { + public string Name; + } + + public class PublicDuckChainingTarget + { +#pragma warning disable SA1401 // Fields must be private + public PublicTarget ChainingTestField = new PublicTarget { Name = "Hello World 1" }; +#pragma warning restore SA1401 // Fields must be private + + public PublicTarget ChainingTest => new PublicTarget { Name = "Hello World 2" }; + + public PublicTarget ChainingTestMethod() => new PublicTarget { Name = "Hello World 3" }; + } + + public struct PublicTarget + { + public string Name; + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/TypesTupleTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/TypesTupleTests.cs new file mode 100644 index 000000000..c4e1221eb --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/DuckTyping/TypesTupleTests.cs @@ -0,0 +1,29 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +// +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Elastic.Apm.Profiler.Managed.DuckTyping; +using Xunit; + +namespace Elastic.Apm.Profiler.Managed.Tests.DuckTyping +{ + public class TypesTupleTests + { + [Fact] + public void EqualsTupleTest() + { + TypesTuple tuple1 = new TypesTuple(typeof(string), typeof(int)); + TypesTuple tuple2 = new TypesTuple(typeof(string), typeof(int)); + + Assert.True(tuple1.Equals(tuple2)); + Assert.True(tuple1.Equals((object)tuple2)); + Assert.False(tuple1.Equals("Hello World")); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj b/test/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj new file mode 100644 index 000000000..4ad32a6d9 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/Elastic.Apm.Profiler.Managed.Tests.csproj @@ -0,0 +1,30 @@ + + + + + net5.0 + false + + false + false + false + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/ExcludeTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/ExcludeTests.cs new file mode 100644 index 000000000..56eea3b57 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/ExcludeTests.cs @@ -0,0 +1,223 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Elastic.Apm.Cloud; +using Elastic.Apm.Logging; +using Elastic.Apm.Profiler.Managed.Tests.AdoNet; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests +{ + public class ExcludeTests + { + private readonly ITestOutputHelper _output; + + public ExcludeTests(ITestOutputHelper output) => _output = output; + + [Fact] + public async Task ShouldNotInstrumentExcludedIntegrations() + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(ShouldNotInstrumentExcludedIntegrations)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + var logs = new List(); + + using (var profiledApplication = new ProfiledApplication("SqliteSample")) + { + var environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ELASTIC_APM_PROFILER_EXCLUDE_INTEGRATIONS"] = "SqliteCommand;AdoNet", + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + }; + + profiledApplication.Start( + "net5.0", + TimeSpan.FromMinutes(2), + environmentVariables, + line => + { + if (line.Line.StartsWith("[")) + logs.Add(line.Line); + else + _output.WriteLine(line.Line); + }, + exception => _output.WriteLine($"{exception}")); + } + + logs.Should().Contain(line => line.Contains("exclude integrations that match SqliteCommand")); + logs.Should().Contain(line => line.Contains("exclude integrations that match AdoNet")); + + // count of manual spans without any auto instrumented spans + apmServer.ReceivedData.Spans.Should().HaveCount(32); + + await apmServer.StopAsync(); + } + + public static IEnumerable TargetFrameworks() + { + if (TestEnvironment.IsWindows) + { + yield return new object[] { "net5.0", "dotnet.exe" }; + yield return new object[] { "net461", "SqliteSample.exe" }; + } + else + yield return new object[] { "net5.0", "dotnet" }; + } + + [Theory] + [MemberData(nameof(TargetFrameworks))] + public async Task ShouldNotInstrumentExcludedProcess(string targetFramework, string excludeProcess) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(ShouldNotInstrumentExcludedProcess)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + var logs = new List(); + + using (var profiledApplication = new ProfiledApplication("SqliteSample")) + { + var environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + ["ELASTIC_APM_PROFILER_EXCLUDE_PROCESSES"] = excludeProcess + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => + { + if (line.Line.StartsWith("[")) + logs.Add(line.Line); + else + _output.WriteLine(line.Line); + }, + exception => _output.WriteLine($"{exception}")); + } + + logs.Should().Contain(line => + line.Contains($"process name {excludeProcess} matches excluded name {excludeProcess}. Profiler disabled")); + + // count of manual spans without any auto instrumented spans + apmServer.ReceivedData.Spans.Should().HaveCount(32); + + await apmServer.StopAsync(); + } + + [Fact] + public async Task ShouldNotInstrumentExcludedServiceName() + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(ShouldNotInstrumentExcludedServiceName)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + var logs = new List(); + var serviceName = "ServiceName"; + + using (var profiledApplication = new ProfiledApplication("SqliteSample")) + { + var environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + ["ELASTIC_APM_SERVICE_NAME"] = serviceName, + ["ELASTIC_APM_PROFILER_EXCLUDE_SERVICE_NAMES"] = serviceName + }; + + profiledApplication.Start( + "net5.0", + TimeSpan.FromMinutes(2), + environmentVariables, + line => + { + if (line.Line.StartsWith("[")) + logs.Add(line.Line); + else + _output.WriteLine(line.Line); + }, + exception => _output.WriteLine($"{exception}")); + } + + logs.Should().Contain(line => + line.Contains($"service name {serviceName} matches excluded name {serviceName}. Profiler disabled")); + + // count of manual spans without any auto instrumented spans + apmServer.ReceivedData.Spans.Should().HaveCount(32); + + await apmServer.StopAsync(); + } + + [Theory] + [InlineData("DOTNET_CLI_TELEMETRY_PROFILE", "AzureKudu")] + [InlineData("APP_POOL_ID", "~apppool")] + public async Task ShouldNotInstrumentAzureAppServiceInfrastructureOrReservedProcess(string key, string value) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(ShouldNotInstrumentExcludedServiceName)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + var logs = new List(); + + using (var profiledApplication = new ProfiledApplication("SqliteSample")) + { + var environmentVariables = new Dictionary + { + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout", + + // Azure App Service environment variables + [AzureAppServiceMetadataProvider.WebsiteOwnerName] = AzureAppServiceMetadataProvider.WebsiteOwnerName, + [AzureAppServiceMetadataProvider.WebsiteInstanceId] = AzureAppServiceMetadataProvider.WebsiteInstanceId, + [AzureAppServiceMetadataProvider.WebsiteResourceGroup] = AzureAppServiceMetadataProvider.WebsiteResourceGroup, + [AzureAppServiceMetadataProvider.WebsiteSiteName] = AzureAppServiceMetadataProvider.WebsiteSiteName, + + // Azure App Service infra/reserved process environment variable + [key] = value + }; + + profiledApplication.Start( + "net5.0", + TimeSpan.FromMinutes(2), + environmentVariables, + line => + { + if (line.Line.StartsWith("[")) + logs.Add(line.Line); + else + _output.WriteLine(line.Line); + }, + exception => _output.WriteLine($"{exception}")); + } + + logs.Should().Contain(line => line.Contains($"Profiler disabled")); + + // count of manual spans without any auto instrumented spans + apmServer.ReceivedData.Spans.Should().HaveCount(32); + + await apmServer.StopAsync(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs b/test/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs new file mode 100644 index 000000000..2424883bc --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaFixture.cs @@ -0,0 +1,116 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.Kafka +{ + public class FixedKafkaTestcontainerConfiguration : KafkaTestcontainerConfiguration + { + private const string KafkaImage = "confluentinc/cp-kafka:6.0.1"; + + private const string StartupScriptPath = "/testcontainers_start.sh"; + + private const int KafkaPort = 9092; + + private const int BrokerPort = 9093; + + private const int ZookeeperPort = 2181; + + public override Func StartupCallback => (container, ct) => + { + var writer = new StringWriter(); + writer.NewLine = "\n"; + writer.WriteLine("#!/bin/sh"); + writer.WriteLine($"echo 'clientPort={ZookeeperPort}' > zookeeper.properties"); + writer.WriteLine("echo 'dataDir=/var/lib/zookeeper/data' >> zookeeper.properties"); + writer.WriteLine("echo 'dataLogDir=/var/lib/zookeeper/log' >> zookeeper.properties"); + writer.WriteLine("zookeeper-server-start zookeeper.properties &"); + writer.WriteLine($"export KAFKA_ADVERTISED_LISTENERS='PLAINTEXT://{container.Hostname}:{container.GetMappedPublicPort(this.DefaultPort)},BROKER://localhost:{BrokerPort}'"); + writer.WriteLine(". /etc/confluent/docker/bash-config"); + writer.WriteLine("/etc/confluent/docker/configure"); + writer.WriteLine("/etc/confluent/docker/launch"); + return container.CopyFileAsync(StartupScriptPath, Encoding.UTF8.GetBytes(writer.ToString()), 0x1ff, ct: ct); + }; + + } + + [CollectionDefinition("Kafka")] + public class KafkaCollection : ICollectionFixture + { + } + + public class KafkaFixture : IAsyncLifetime + { + internal const int BrokerPort = 9093; + + private readonly KafkaTestcontainer _container; + + public KafkaFixture(IMessageSink messageSink) + { + var builder = new TestcontainersBuilder() + .WithKafka(new FixedKafkaTestcontainerConfiguration()); + + _container = builder.Build(); + } + + public async Task InitializeAsync() + { + await _container.StartAsync(); + + // update advertised.listeners config value, which appears to be broken as it is not updated by the KafkaTestcontainerConfiguration + var brokerAdvertisedListener = $"BROKER://localhost:{BrokerPort}"; + var plainTextListener = $"PLAINTEXT://{_container.Hostname}:{_container.Port}"; + var count = 0; + ExecResult result = default; + while (count < 10) + { + result = await _container.ExecAsync(new List + { + "kafka-configs", + "--alter", + "--bootstrap-server", + brokerAdvertisedListener, + "--entity-type", + "brokers", + "--entity-name", + "1", + "--add-config", + $"advertised.listeners=[{plainTextListener},{brokerAdvertisedListener}]" + } + ); + + + if (result.ExitCode == 0) + break; + + await Task.Delay(1000); + count++; + } + + if (result.ExitCode != 0) + { + throw new InvalidOperationException( + $"Updating kafka-configs returned exit code {result.ExitCode}.\nstdout: {result.Stdout}\nstderr:{result.Stderr}"); + } + + BootstrapServers = _container.BootstrapServers; + } + + public async Task DisposeAsync() => await _container.DisposeAsync(); + + public string BootstrapServers { get; private set; } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaTests.cs new file mode 100644 index 000000000..367e95d51 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/Kafka/KafkaTests.cs @@ -0,0 +1,82 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elastic.Apm.Tests.MockApmServer; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Docker; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Profiler.Managed.Tests.Kafka +{ + [Collection("Kafka")] + public class KafkaTests + { + private readonly KafkaFixture _fixture; + private readonly ITestOutputHelper _output; + + public KafkaTests(KafkaFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + [DockerTheory] + [InlineData("net5.0")] + public async Task CaptureAutoInstrumentedSpans(string targetFramework) + { + var apmLogger = new InMemoryBlockingLogger(Elastic.Apm.Logging.LogLevel.Error); + var apmServer = new MockApmServer(apmLogger, nameof(CaptureAutoInstrumentedSpans)); + var port = apmServer.FindAvailablePortToListen(); + apmServer.RunInBackground(port); + + using (var profiledApplication = new ProfiledApplication("KafkaSample")) + { + IDictionary environmentVariables = new Dictionary + { + ["KAFKA_HOST"] = _fixture.BootstrapServers, + ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", + ["ELASTIC_APM_DISABLE_METRICS"] = "*", + }; + + profiledApplication.Start( + targetFramework, + TimeSpan.FromMinutes(2), + environmentVariables, + line => _output.WriteLine(line.Line), + exception => _output.WriteLine($"{exception}")); + } + + // 6 * 10 consume transactions, 8 produce transactions + var transactions = apmServer.ReceivedData.Transactions; + transactions.Should().HaveCount(68); + + var consumeTransactions = transactions.Where(t => t.Name.StartsWith("Kafka RECEIVE")).ToList(); + consumeTransactions.Should().HaveCount(60); + + foreach (var consumeTransaction in consumeTransactions) + { + var spans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == consumeTransaction.Id); + spans.Should().HaveCount(1); + } + + var produceTransactions = transactions.Where(t => !t.Name.StartsWith("Kafka RECEIVE")).ToList(); + produceTransactions.Should().HaveCount(8); + + foreach (var produceTransaction in produceTransactions) + { + var spans = apmServer.ReceivedData.Spans.Where(s => s.TransactionId == produceTransaction.Id); + spans.Should().HaveCount(produceTransaction.Name.Contains("INVALID-TOPIC") ? 1 : 10, produceTransaction.Name); + } + + await apmServer.StopAsync(); + } + } +} diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs b/test/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs new file mode 100644 index 000000000..ce6adf377 --- /dev/null +++ b/test/Elastic.Apm.Profiler.Managed.Tests/ProfiledApplication.cs @@ -0,0 +1,141 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Elastic.Apm.Tests.Utilities; +using ProcNet; +using ProcNet.Std; + +namespace Elastic.Apm.Profiler.Managed.Tests +{ + /// + /// A sample application that can be instrumented with the (Core)CLR profiler + /// + public class ProfiledApplication : IDisposable + { + private const string ProfilerClassId = "{FA65FE15-F085-4681-9B20-95E04F6C03CC}"; + private readonly string _profilerPath; + private readonly string _projectDirectory; + private readonly string _projectName; + private readonly string _publishDirectory; + + public ProfiledApplication(string projectName) + { + _projectName = projectName; + _projectDirectory = Path.Combine(SolutionPaths.Root, "sample", projectName); + + if (!Directory.Exists(_projectDirectory)) + throw new DirectoryNotFoundException($"project could not be found at {_projectDirectory}"); + + string profilerFile; + if (TestEnvironment.IsWindows) + profilerFile = "elastic_apm_profiler.dll"; + else if (TestEnvironment.IsLinux) + profilerFile = "libelastic_apm_profiler.so"; + else + profilerFile = "libelastic_apm_profiler.dylib"; + + _profilerPath = Path.Combine(SolutionPaths.Root, "target", "release", profilerFile); + + if (!File.Exists(_profilerPath)) + throw new FileNotFoundException( + $"profiler could not be found at {_profilerPath}. Run `build.[bat|sh] in project root to build it", + _profilerPath); + + _publishDirectory = Path.Combine("bin", "Publish"); + } + + private ObservableProcess _process; + + private void Publish(string targetFramework) + { + var targetFrameworkPublishDirectory = Path.Combine(_publishDirectory, targetFramework); + + // if we're running on CI and the publish directory already exists for the + // target framework, skip publishing again. + if (TestEnvironment.IsCi && Directory.Exists(targetFrameworkPublishDirectory)) + return; + + var processInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"publish -c Release -f {targetFramework} -o {targetFrameworkPublishDirectory}", + WorkingDirectory = _projectDirectory + }; + + using var process = new Process { StartInfo = processInfo }; + process.Start(); + process.WaitForExit(); + } + + /// + /// Starts the sample application and returns the on which the application + /// can be reached. + /// + /// + /// The target framework under which to run the profiled app. Must be a version supported in + /// the TargetFrameworks of the profiled app + /// + /// A timeout to wait for the process to complete. + /// + /// The environment variables to start the sample app with. The profiler + /// environment variables will be added. + /// + /// delegate to call when line is received + /// delegate to call when exception occurs + /// + public void Start( + string targetFramework, + TimeSpan timeout, + IDictionary environmentVariables = null, + Action onNext = null, + Action onException = null + ) + { + Publish(targetFramework); + + environmentVariables ??= new Dictionary(); + environmentVariables["CORECLR_ENABLE_PROFILING"] = "1"; + environmentVariables["CORECLR_PROFILER"] = ProfilerClassId; + environmentVariables["CORECLR_PROFILER_PATH"] = _profilerPath; + + environmentVariables["COR_ENABLE_PROFILING"] = "1"; + environmentVariables["COR_PROFILER"] = ProfilerClassId; + environmentVariables["COR_PROFILER_PATH"] = _profilerPath; + + environmentVariables["ELASTIC_APM_PROFILER_HOME"] = + Path.Combine(SolutionPaths.Root, "src", "Elastic.Apm.Profiler.Managed", "bin", "Release"); + environmentVariables["ELASTIC_APM_PROFILER_INTEGRATIONS"] = + Path.Combine(SolutionPaths.Root, "src", "Elastic.Apm.Profiler.Managed", "integrations.yml"); + + environmentVariables["ELASTIC_APM_PROFILER_LOG"] = "trace"; + // log to relative logs directory for managed loader + environmentVariables["ELASTIC_APM_PROFILER_LOG_DIR"] = Path.Combine(SolutionPaths.Root, "logs"); + + //environmentVariables["ELASTIC_APM_PROFILER_LOG_TARGETS"] = "file;stdout"; + //environmentVariables["ELASTIC_APM_PROFILER_LOG_IL"] = "true"; + + var workingDirectory = Path.Combine(_projectDirectory, _publishDirectory, targetFramework); + + // use the .exe for net461 + var arguments = targetFramework == "net461" + ? new StartArguments(Path.Combine(workingDirectory, $"{_projectName}.exe")) + : new StartArguments("dotnet", $"{_projectName}.dll"); + + arguments.Environment = environmentVariables; + arguments.WorkingDirectory = workingDirectory; + + _process = new ObservableProcess(arguments); + _process.SubscribeLines(onNext ?? (_ => { }), onException ?? (_ => { })); + _process.WaitForCompletion(timeout); + } + + public void Dispose() => _process?.Dispose(); + } +} diff --git a/test/Elastic.Apm.StartupHook.Tests/SampleApplication.cs b/test/Elastic.Apm.StartupHook.Tests/SampleApplication.cs index 250279fd5..69eb6833e 100644 --- a/test/Elastic.Apm.StartupHook.Tests/SampleApplication.cs +++ b/test/Elastic.Apm.StartupHook.Tests/SampleApplication.cs @@ -11,6 +11,7 @@ using System.Runtime.ExceptionServices; using System.Text.RegularExpressions; using System.Threading; +using Elastic.Apm.Tests.Utilities; using ProcNet; namespace Elastic.Apm.StartupHook.Tests diff --git a/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs b/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs index 91478b48c..0d393ff2f 100644 --- a/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs +++ b/test/Elastic.Apm.Tests.MockApmServer/Controllers/IntakeV2EventsController.cs @@ -53,26 +53,22 @@ public IntakeV2EventsController(MockApmServer mockApmServer, Validator validator private async Task PostImpl() { - _logger.Debug() - ?.Log("Received request with content length: {ContentLength}." - + " Current thread: {ThreadDesc}." - , Request.ContentLength, DbgUtils.CurrentThreadDesc); - - int numberOfObjects; - try { - numberOfObjects = await ParsePayload(); + _logger.Debug() + ?.Log("Received request with content length: {ContentLength}." + + " Current thread: {ThreadDesc}." + , Request.ContentLength, DbgUtils.CurrentThreadDesc); + + var numberOfObjects = await ParsePayload(); + _logger.Debug()?.Log("Successfully parsed {numberOfObjects} objects", numberOfObjects); + return Ok($"Successfully processed request with Content-Length: {Request.ContentLength} and number of objects: {numberOfObjects}"); } - catch (ArgumentException ex) + catch (Exception ex) { _mockApmServer.AddInvalidPayload(ex.ToString()); return BadRequest(ex.ToString()); } - - _logger.Debug()?.Log("Successfully parsed {numberOfObjects} objects", numberOfObjects); - - return Ok($"Successfully processed request with Content-Length: {Request.ContentLength} and number of objects: {numberOfObjects}"); } private async Task ParsePayload() @@ -103,7 +99,7 @@ private async Task ParsePayload() private async Task ParsePayloadLineAndAddToReceivedData(string line) { var foundDto = false; - var deserializedPayload = JsonConvert.DeserializeObject( + var payload = JsonConvert.DeserializeObject( line, new JsonSerializerSettings { @@ -113,14 +109,10 @@ private async Task ParsePayloadLineAndAddToReceivedData(string line) _logger.Error() ?.Log("Failed to parse payload line as JSON. Error: {PayloadParsingErrorMessage}, line: `{PayloadLine}'", errorEventArgs.ErrorContext.Error.Message, line); - throw new ArgumentException(errorEventArgs.ErrorContext.Error.Message); } }); - PayloadLineDto payload = null; - if (deserializedPayload != null) - payload = (PayloadLineDto)deserializedPayload; - else + if (payload is null) throw new ArgumentException("Deserialization failed"); await HandleParsed(nameof(payload.Error), payload.Error, _mockApmServer.ReceivedData.Errors, _mockApmServer.AddError); diff --git a/test/Elastic.Apm.Tests.MockApmServer/Elastic.Apm.Tests.MockApmServer.csproj b/test/Elastic.Apm.Tests.MockApmServer/Elastic.Apm.Tests.MockApmServer.csproj index 94dc3cd62..f827fae09 100644 --- a/test/Elastic.Apm.Tests.MockApmServer/Elastic.Apm.Tests.MockApmServer.csproj +++ b/test/Elastic.Apm.Tests.MockApmServer/Elastic.Apm.Tests.MockApmServer.csproj @@ -10,6 +10,7 @@ + diff --git a/test/Elastic.Apm.Tests.MockApmServer/MetadataDto.cs b/test/Elastic.Apm.Tests.MockApmServer/MetadataDto.cs index ef02d1244..e72fd9963 100644 --- a/test/Elastic.Apm.Tests.MockApmServer/MetadataDto.cs +++ b/test/Elastic.Apm.Tests.MockApmServer/MetadataDto.cs @@ -18,6 +18,7 @@ internal class MetadataDto : IDto { public Service Service { get; set; } public Api.System System { get; set; } + public Api.Cloud Cloud { get; set; } public Dictionary Labels { get; set; } public override string ToString() => @@ -25,7 +26,8 @@ public override string ToString() => { { nameof(Service), Service }, { nameof(System), System }, - { nameof(Labels), AbstractConfigurationReader.ToLogString(Labels) } + { nameof(Labels), AbstractConfigurationReader.ToLogString(Labels) }, + { nameof(Cloud), Cloud }, }.ToString(); public void AssertValid() diff --git a/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs index ef524f393..5b1109d0b 100644 --- a/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs +++ b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs @@ -78,8 +78,7 @@ public abstract class AzureCredentials private static AzureCredentials LoadCredentials() { - var runningInCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_ID")); - if (runningInCi) + if (TestEnvironment.IsCi) { var credentialsFile = Path.Combine(SolutionPaths.Root, ".credentials.json"); if (!File.Exists(credentialsFile)) @@ -115,10 +114,10 @@ private static bool LoggedIntoAccountWithAzureCli() { // run azure CLI using cmd on Windows so that %~dp0 in az.cmd expands to // the path containing the cmd file. - var binary = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + var binary = TestEnvironment.IsWindows ? "cmd" : "az"; - var args = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + var args = TestEnvironment.IsWindows ? new[] { "/c", "az", "account", "show" } : new[] { "account", "show" }; diff --git a/test/Elastic.Apm.Tests.Utilities/Docker/DockerFactAttribute.cs b/test/Elastic.Apm.Tests.Utilities/Docker/DockerFactAttribute.cs index dc94fccf7..d06c810ed 100644 --- a/test/Elastic.Apm.Tests.Utilities/Docker/DockerFactAttribute.cs +++ b/test/Elastic.Apm.Tests.Utilities/Docker/DockerFactAttribute.cs @@ -20,6 +20,12 @@ static DockerFactAttribute() { try { + if (TestEnvironment.IsCi && TestEnvironment.IsWindows) + { + _skip = "not running tests that require docker in CI on Windows"; + return; + } + var result = Proc.Start(new StartArguments("docker", "--version")); if (result.ExitCode != 0) _skip = "docker not installed"; diff --git a/test/Elastic.Apm.Tests.Utilities/Docker/DockerTheoryAttribute.cs b/test/Elastic.Apm.Tests.Utilities/Docker/DockerTheoryAttribute.cs new file mode 100644 index 000000000..2038e786b --- /dev/null +++ b/test/Elastic.Apm.Tests.Utilities/Docker/DockerTheoryAttribute.cs @@ -0,0 +1,41 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using ProcNet; +using Xunit; + +namespace Elastic.Apm.Tests.Utilities.Docker +{ + /// + /// Test method that should be run only if docker exists on the host + /// + public class DockerTheoryAttribute : TheoryAttribute + { + private static readonly string _skip; + + static DockerTheoryAttribute() + { + try + { + if (TestEnvironment.IsCi && TestEnvironment.IsWindows) + { + _skip = "not running tests that require docker in CI on Windows"; + return; + } + + var result = Proc.Start(new StartArguments("docker", "--version")); + if (result.ExitCode != 0) + _skip = "docker not installed"; + } + catch (Exception) + { + _skip = "could not get version of docker"; + } + } + + public DockerTheoryAttribute() => Skip = _skip; + } +} diff --git a/test/Elastic.Apm.Tests.Utilities/LocalPort.cs b/test/Elastic.Apm.Tests.Utilities/LocalPort.cs new file mode 100644 index 000000000..2885e6005 --- /dev/null +++ b/test/Elastic.Apm.Tests.Utilities/LocalPort.cs @@ -0,0 +1,24 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Net; +using System.Net.Sockets; + +namespace Elastic.Apm.Tests.Utilities +{ + public class LocalPort + { + private static readonly IPEndPoint DefaultLoopbackEndpoint = new IPEndPoint(IPAddress.Loopback, 0); + + public static int GetAvailablePort() + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Bind(DefaultLoopbackEndpoint); + return ((IPEndPoint) socket.LocalEndPoint).Port; + } + } + } +} diff --git a/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs b/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs index 9bf7a7a94..0a9fd3f30 100644 --- a/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs +++ b/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs @@ -5,12 +5,15 @@ using System; using System.IO; +using System.Linq; namespace Elastic.Apm.Tests.Utilities { public static class SolutionPaths { private static readonly Lazy _root = new Lazy(FindSolutionRoot); + + private static readonly Lazy _agentZip = new Lazy(FindVersionedAgentZip); private static string FindSolutionRoot() { var solutionFileName = "ElasticApmAgent.sln"; @@ -27,9 +30,29 @@ private static string FindSolutionRoot() throw new InvalidOperationException($"Could not find solution root directory from the current directory `{currentDirectory}'"); } - /// - /// The full path to the solution root - /// + private static string FindVersionedAgentZip() + { + var buildOutputDir = Path.Combine(Root, "build/output"); + if (!Directory.Exists(buildOutputDir)) + { + throw new DirectoryNotFoundException( + $"build output directory does not exist at {buildOutputDir}. " + + $"Run the build script in the solution root with agent-zip target to build"); + } + + var agentZip = Directory.EnumerateFiles(buildOutputDir, "ElasticApmAgent_*.zip", SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + + if (agentZip is null) + { + throw new FileNotFoundException($"ElasticApmAgent_*.zip file not found in {buildOutputDir}. " + + $"Run the build script in the solution root with agent-zip target to build"); + } + + return agentZip; + } + public static string Root => _root.Value; + public static string AgentZip => _agentZip.Value; } } diff --git a/test/Elastic.Apm.Tests.Utilities/TestEnvironment.cs b/test/Elastic.Apm.Tests.Utilities/TestEnvironment.cs new file mode 100644 index 000000000..d2ac767a0 --- /dev/null +++ b/test/Elastic.Apm.Tests.Utilities/TestEnvironment.cs @@ -0,0 +1,26 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Runtime.InteropServices; + +namespace Elastic.Apm.Tests.Utilities +{ + public static class TestEnvironment + { + public static bool IsCi { get; } + + public static bool IsLinux { get; } + + public static bool IsWindows { get; } + + static TestEnvironment() + { + IsCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_ID")); + IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } + } +} diff --git a/test/Elastic.Apm.Tests/MetricsTests.cs b/test/Elastic.Apm.Tests/MetricsTests.cs index 1545372a1..e2f92a8f2 100644 --- a/test/Elastic.Apm.Tests/MetricsTests.cs +++ b/test/Elastic.Apm.Tests/MetricsTests.cs @@ -70,7 +70,7 @@ public void CollectAllMetrics() [Fact] public void SystemCpu() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!TestEnvironment.IsLinux && !TestEnvironment.IsWindows) return; using var systemTotalCpuProvider = new SystemTotalCpuProvider(new NoopLogger());