From c7d09b1f9878da92a50f5fbe317bad7e9afb69e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Mon, 15 Sep 2025 13:44:23 +0200 Subject: [PATCH 01/45] Make Logger and nativecsharpextension compile on Linux. --- .../dotnet-core-CSharp/include/DotnetEnvironment.h | 10 ++++++++++ .../dotnet-core-CSharp/include/Logger.h | 1 - .../dotnet-core-CSharp/include/nativecsharpextension.h | 3 +++ .../dotnet-core-CSharp/src/native/Logger.cpp | 2 -- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index c84b6cd2..755141ae 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -10,13 +10,23 @@ //********************************************************************* #pragma once +#if defined(__WIN32) || defined(WINDOWS) #include "Windows.h" +#else +#define E_FAIL 0x80004005 +#endif + #include #include #include +#if defined(__WIN32) || defined(WINDOWS) #define STR(s) L ## s #define CH(c) L ## c +#else +#define STR(s) s +#define CH(c) c +#endif using namespace std; using string_t = std::basic_string; diff --git a/language-extensions/dotnet-core-CSharp/include/Logger.h b/language-extensions/dotnet-core-CSharp/include/Logger.h index 2a96ec0a..fa43d0b8 100644 --- a/language-extensions/dotnet-core-CSharp/include/Logger.h +++ b/language-extensions/dotnet-core-CSharp/include/Logger.h @@ -10,7 +10,6 @@ //********************************************************************* #include #include -#include using namespace std; #define LOG(msg) Logger::Log(msg) diff --git a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h index 4fe35739..7907b6fe 100644 --- a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h +++ b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h @@ -20,7 +20,10 @@ #include #include +#if defined(__WIN32) || defined(WINDOWS) #include +#endif + #include #include "sqlexternallanguage.h" #include "sqlexternallibrary.h" diff --git a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp index 89d85852..c44b011b 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp @@ -10,8 +10,6 @@ //********************************************************************* #include #include -#include -#include #include "Logger.h" using namespace std; From d3de31b574ee9021242c3e318fe8cf5be7079f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Mon, 15 Sep 2025 14:28:40 +0200 Subject: [PATCH 02/45] Make DotnetEnvironment compile on Linux. --- .../include/DotnetEnvironment.h | 3 +- .../src/native/DotnetEnvironment.cpp | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index 755141ae..93edcab7 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -13,7 +13,8 @@ #if defined(__WIN32) || defined(WINDOWS) #include "Windows.h" #else -#define E_FAIL 0x80004005 +#define E_FAIL -1 +#define S_OK 0 #endif #include diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index bc2f8471..6d9c25a2 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -10,8 +10,15 @@ //********************************************************************* #include "DotnetEnvironment.h" #include "Logger.h" + +#if defined(_WIN32) || defined(WINDOWS) #include "Windows.h" +#else +#include +#endif + #include +#include #include #include #include @@ -19,8 +26,13 @@ #include #include +#if defined(__WIN32) || defined(WINDOWS) #define STR(s) L ## s #define CH(c) L ## c +#else +#define STR(s) s +#define CH(c) c +#endif using namespace std; using string_t = std::basic_string; @@ -35,7 +47,12 @@ DotnetEnvironment::DotnetEnvironment( std::string language_params, std::string language_path, std::string public_library_path, - std::string private_library_path) : m_root_path(to_utf16_str(language_path)) + std::string private_library_path) : +#if defined(_WIN32) || defined(WINDOWS) + m_root_path(to_utf16_str(language_path)) +#else + m_root_path(language_path) +#endif { } @@ -74,6 +91,7 @@ short DotnetEnvironment::Init() return S_OK; } +#if defined(_WIN32) || defined(WINDOWS) //-------------------------------------------------------------------------------------------------- // Name: DotnetEnvironment::to_utf16_str // @@ -88,6 +106,7 @@ string_t DotnetEnvironment::to_utf16_str(const std::string& utf8str) MultiByteToWideChar(CP_UTF8, 0, utf8str.c_str(), -1, wstr.get(), wchars_num); return string_t(wstr.get()); } +#endif //-------------------------------------------------------------------------------------------------- // Name: DotnetEnvironment::to_hex_string @@ -112,7 +131,11 @@ string DotnetEnvironment::to_hex_string(int value) void* DotnetEnvironment::load_library(const char_t *path) { LOG("DotnetEnvironment::load_library"); +#if defined(_WIN32) || defined(WINDOWS) HMODULE h = ::LoadLibraryW(path); +#else + void *h = dlopen(path, RTLD_LAZY); +#endif assert(h != nullptr); return (void*)h; } @@ -126,13 +149,17 @@ void* DotnetEnvironment::load_library(const char_t *path) void* DotnetEnvironment::get_export(void *h, const char *name) { LOG("DotnetEnvironment::get_export"); +#if defined(_WIN32) || defined(WINDOWS) void *f = ::GetProcAddress((HMODULE)h, name); +#else + void *f = dlsym(h, name); +#endif assert(f != nullptr); return f; } //-------------------------------------------------------------------------------------------------- -// Name: DotnetEnvironment::get_export +// Name: DotnetEnvironment::load_hostfxr // // Description: // Load hostfxr and get desired exports @@ -140,7 +167,11 @@ void* DotnetEnvironment::get_export(void *h, const char *name) bool DotnetEnvironment::load_hostfxr() { LOG("DotnetEnvironment::load_hostfxr"); +#if defined(_WIN32) || defined(WINDOWS) string_t hostfxr_location = m_root_path + STR("\\hostfxr.dll"); +#else + string_t hostfxr_location = STR("libhostfxr.so"); +#endif void *lib = load_library(hostfxr_location.c_str()); m_init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config"); m_get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate"); From c711d0589fc28bfc7afe3ff1ce36a5ac784b130d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Mon, 15 Sep 2025 15:07:15 +0200 Subject: [PATCH 03/45] Add build script for Linux. --- .../build-dotnet-core-CSharp-extension.ps1 | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100755 language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 new file mode 100755 index 00000000..4f2c09e3 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 @@ -0,0 +1,106 @@ +#!/usr/bin/env pwsh + +# Set root and working directories +$ENL_ROOT = Join-Path $PSScriptRoot "../../../.." +$DOTNET_EXTENSION_HOME = Join-Path $ENL_ROOT "language-extensions/dotnet-core-CSharp" +$DOTNET_EXTENSION_WORKING_DIR = Join-Path $ENL_ROOT "build-output/dotnet-core-CSharp-extension/linux" + +# Clean and create working directory +if (Test-Path $DOTNET_EXTENSION_WORKING_DIR) { + Remove-Item $DOTNET_EXTENSION_WORKING_DIR -Recurse -Force +} +New-Item -ItemType Directory -Path $DOTNET_EXTENSION_WORKING_DIR | Out-Null + +$i = 0 + +# Process each build configuration +while($true) { + $BUILD_CONFIGURATION = "" + if ($i -lt $args.Count) { + $BUILD_CONFIGURATION = $args[$i] + } + # Default to release if not debug + if ($BUILD_CONFIGURATION -ne "debug") { + $BUILD_CONFIGURATION = "release" + } + + # Set target directory + $TARGET = Join-Path $ENL_ROOT "build-output/dotnet-core-CSharp-extension/target/$BUILD_CONFIGURATION" + + # Clean and create target directory + if (Test-Path $TARGET) { + Remove-Item $TARGET -Recurse -Force + } + New-Item -ItemType Directory -Path $TARGET | Out-Null + + Write-Host "[Info] Building dotnet-core-CSharp-extension nativecsharpextension dll..." + + # Set build output directory + $BUILD_OUTPUT = Join-Path $DOTNET_EXTENSION_WORKING_DIR $BUILD_CONFIGURATION + if (Test-Path $BUILD_OUTPUT) { + Remove-Item $BUILD_OUTPUT -Recurse -Force + } + New-Item -ItemType Directory -Path $BUILD_OUTPUT | Out-Null + Push-Location $BUILD_OUTPUT + + # Set source and include paths + $DOTNET_NATIVE_SRC = Join-Path $DOTNET_EXTENSION_HOME "src/native" + $DOTNET_NATIVE_INCLUDE = Join-Path $DOTNET_EXTENSION_HOME "include" + $EXTENSION_HOST_INCLUDE = Join-Path $ENL_ROOT "extension-host/include" + $DOTNET_NATIVE_LIB = Join-Path $DOTNET_EXTENSION_HOME "lib" + + # Build native code + $ccArgs = @( + "-shared", + "-fPIC", + "-o", "libnativecsharpextension.so", + "-I", $DOTNET_NATIVE_INCLUDE, + "-I", $EXTENSION_HOST_INCLUDE, + "-D", "LINUX" + ) + $ccArgs += Get-ChildItem -Path $DOTNET_NATIVE_SRC -Filter "*.cpp" | ForEach-Object { $_.FullName } + if ($BUILD_CONFIGURATION -eq "debug") { + $ccArgs += "-D", "DEBUG" + } + + if ($null -ne $env:CXX) { + $cxx = $env:CXX + } else { + $cxx = "c++" + } + + $proc = Start-Process -FilePath $cxx -ArgumentList $ccArgs -NoNewWindow -Wait -PassThru + if ($proc.ExitCode -ne 0) { + Write-Host "Error: Failed to build nativecsharpextension for configuration=$BUILD_CONFIGURATION" + exit $proc.ExitCode + } + + Write-Host "[Info] Copying dependent libraries..." + Copy-Item (Join-Path $DOTNET_NATIVE_LIB "libhostfxr.so") $BUILD_OUTPUT -Force + + # Build managed code + Write-Host "[Info] Building Microsoft.SqlServer.CSharpExtension dll..." + $DOTNET_MANAGED_SRC = Join-Path $DOTNET_EXTENSION_HOME "src/managed" + $dotnetProc = Start-Process -FilePath "dotnet" -ArgumentList @( + "build", + (Join-Path $DOTNET_MANAGED_SRC "Microsoft.SqlServer.CSharpExtension.csproj"), + "-m", + "-c", $BUILD_CONFIGURATION, + "-o", $BUILD_OUTPUT, + "--no-dependencies" + ) -NoNewWindow -Wait -PassThru + + if ($dotnetProc.ExitCode -ne 0) { + Write-Host "Error: Failed to build for Microsoft.SqlServer.CSharpExtension.dll for configuration=$BUILD_CONFIGURATION" + exit $dotnetProc.ExitCode + } + + Write-Host "Success: Built dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." + + $i++ + if ($i -ge $args.Count) { + break + } +} + +exit 0 From 903152643d7499dded1ba627c467e79a48ba88cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Mon, 15 Sep 2025 15:32:44 +0200 Subject: [PATCH 04/45] Add archive script for Linux. SQL Server on Linux expects a .tar.gz file instead of a .zip when using CREATE EXTERNAL LANGUAGE. --- .../build-dotnet-core-CSharp-extension.ps1 | 6 +- ...reate-dotnet-core-CSharp-extension-zip.ps1 | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100755 language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 index 4f2c09e3..092ecd12 100755 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 @@ -14,11 +14,12 @@ New-Item -ItemType Directory -Path $DOTNET_EXTENSION_WORKING_DIR | Out-Null $i = 0 # Process each build configuration -while($true) { +while ($true) { $BUILD_CONFIGURATION = "" if ($i -lt $args.Count) { $BUILD_CONFIGURATION = $args[$i] } + $BUILD_CONFIGURATION = $BUILD_CONFIGURATION.ToLower() # Default to release if not debug if ($BUILD_CONFIGURATION -ne "debug") { $BUILD_CONFIGURATION = "release" @@ -65,7 +66,8 @@ while($true) { if ($null -ne $env:CXX) { $cxx = $env:CXX - } else { + } + else { $cxx = "c++" } diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 new file mode 100755 index 00000000..47c33386 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 @@ -0,0 +1,65 @@ +#!/usr/bin/env pwsh + +$ENL_ROOT = Join-Path $PSScriptRoot "../../../.." +$DOTNET_EXTENSION_WORKING_DIR = Join-Path -Path $ENL_ROOT -ChildPath "build-output/dotnet-core-CSharp-extension/linux" + +function CheckError { + param( + [int]$errorLevel, + [string]$errorMessage + ) + if ($errorLevel -ne 0) { + Write-Error $errorMessage + exit $errorLevel + } +} + + +$i = 0 + +# Process each build configuration +while ($true) { + $BUILD_CONFIGURATION = "" + if ($i -lt $args.Count) { + $BUILD_CONFIGURATION = $args[$i] + } + $BUILD_CONFIGURATION = $BUILD_CONFIGURATION.ToLower() + # Default to release if not debug + if ($BUILD_CONFIGURATION -ne "debug") { + $BUILD_CONFIGURATION = "release" + } + + $BUILD_OUTPUT = Join-Path -Path $DOTNET_EXTENSION_WORKING_DIR -ChildPath $BUILD_CONFIGURATION + New-Item -ItemType Directory -Force -Path "$BUILD_OUTPUT/packages" | Out-Null + + # Delete the ref folder so that the zip can be loaded by the SPEES + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "$BUILD_OUTPUT/ref" + + # Define files to compress, conditionally including .pdb files if BUILD_CONFIGURATION is "debug" + $FILES_TO_COMPRESS = @( + "Microsoft.SqlServer.CSharpExtension.runtimeconfig.json", + "Microsoft.SqlServer.CSharpExtension.deps.json" + ) + $FILES_TO_COMPRESS += Get-ChildItem -Path $BUILD_OUTPUT -Filter "*.dll" | ForEach-Object { $_.Name } + $FILES_TO_COMPRESS += Get-ChildItem -Path $BUILD_OUTPUT -Filter "*.so" | ForEach-Object { $_.Name } + if ($BUILD_CONFIGURATION -ieq "debug") { + $FILES_TO_COMPRESS += Get-ChildItem -Path $BUILD_OUTPUT -Filter "*.pdb" | ForEach-Object { $_.Name } + } + + # Package the signed binaries. + try { + Push-Location $BUILD_OUTPUT + & tar -czvf "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension.tar.gz" $FILES_TO_COMPRESS + CheckError $LASTEXITCODE "Error: Failed to create zip for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" + Pop-Location + Write-Host "Success: Compressed dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." + } + catch { + CheckError 1 "Error: Failed to create zip for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" + } + + $i++ + if ($i -ge $args.Count) { + break + } +} From 069def815fbdaa6bde2f7794e71e872fa8639fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Wed, 17 Sep 2025 08:40:12 +0200 Subject: [PATCH 05/45] Add static nethost library from .NET 8.0 Linux runtime. We will use the nethost to locate the system's installed hostfxr, as using a local libhostfxr.so breaks the .NET runtime lookup. nethost contains the logic needed to find the .NET root based on the DOTNET_ROOT environment variable or the install_location configuration file, which we will require for .NET runtime lookup on diverging Linux distributions (e.g. RHEL and Debian packages not installing .NET in the same base directories). --- .../dotnet-core-CSharp/include/nethost.h | 99 ++++++++++++++++++ .../dotnet-core-CSharp/lib/libnethost.a | Bin 0 -> 178378 bytes 2 files changed, 99 insertions(+) create mode 100644 language-extensions/dotnet-core-CSharp/include/nethost.h create mode 100644 language-extensions/dotnet-core-CSharp/lib/libnethost.a diff --git a/language-extensions/dotnet-core-CSharp/include/nethost.h b/language-extensions/dotnet-core-CSharp/include/nethost.h new file mode 100644 index 00000000..eaca1753 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/include/nethost.h @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __NETHOST_H__ +#define __NETHOST_H__ + +#include + +#ifdef _WIN32 + #ifdef NETHOST_EXPORT + #define NETHOST_API __declspec(dllexport) + #else + // Consuming the nethost as a static library + // Shouldn't export attempt to dllimport. + #ifdef NETHOST_USE_AS_STATIC + #define NETHOST_API + #else + #define NETHOST_API __declspec(dllimport) + #endif + #endif + + #define NETHOST_CALLTYPE __stdcall + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #ifdef NETHOST_EXPORT + #define NETHOST_API __attribute__((__visibility__("default"))) + #else + #define NETHOST_API + #endif + + #define NETHOST_CALLTYPE + typedef char char_t; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// Parameters for get_hostfxr_path +// +// Fields: +// size +// Size of the struct. This is used for versioning. +// +// assembly_path +// Path to the component's assembly. +// If specified, hostfxr is located as if the assembly_path is the apphost +// +// dotnet_root +// Path to directory containing the dotnet executable. +// If specified, hostfxr is located as if an application is started using +// 'dotnet app.dll', which means it will be searched for under the dotnet_root +// path and the assembly_path is ignored. +// +struct get_hostfxr_parameters { + size_t size; + const char_t *assembly_path; + const char_t *dotnet_root; +}; + +// +// Get the path to the hostfxr library +// +// Parameters: +// buffer +// Buffer that will be populated with the hostfxr path, including a null terminator. +// +// buffer_size +// [in] Size of buffer in char_t units. +// [out] Size of buffer used in char_t units. If the input value is too small +// or buffer is nullptr, this is populated with the minimum required size +// in char_t units for a buffer to hold the hostfxr path +// +// get_hostfxr_parameters +// Optional. Parameters that modify the behaviour for locating the hostfxr library. +// If nullptr, hostfxr is located using the environment variable or global registration +// +// Return value: +// 0 on success, otherwise failure +// 0x80008098 - buffer is too small (HostApiBufferTooSmall) +// +// Remarks: +// The full search for the hostfxr library is done on every call. To minimize the need +// to call this function multiple times, pass a large buffer (e.g. PATH_MAX). +// +NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path( + char_t * buffer, + size_t * buffer_size, + const struct get_hostfxr_parameters *parameters); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // __NETHOST_H__ diff --git a/language-extensions/dotnet-core-CSharp/lib/libnethost.a b/language-extensions/dotnet-core-CSharp/lib/libnethost.a new file mode 100644 index 0000000000000000000000000000000000000000..9b333a48619ed1002570e148f9a916d5ab2c9dd6 GIT binary patch literal 178378 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 literal 0 HcmV?d00001 From fade547fc8a53ed2ab9f0593b22fa9701f750601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Wed, 17 Sep 2025 09:00:17 +0200 Subject: [PATCH 06/45] Use nethost to lookup hostfxr on Linux. --- .../build/linux/build-dotnet-core-CSharp-extension.ps1 | 10 ++++------ .../src/native/DotnetEnvironment.cpp | 8 +++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 index 092ecd12..2bafbee1 100755 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 @@ -55,11 +55,12 @@ while ($true) { "-shared", "-fPIC", "-o", "libnativecsharpextension.so", - "-I", $DOTNET_NATIVE_INCLUDE, - "-I", $EXTENSION_HOST_INCLUDE, - "-D", "LINUX" + "-I$DOTNET_NATIVE_INCLUDE", + "-I$EXTENSION_HOST_INCLUDE", + "-DLINUX" ) $ccArgs += Get-ChildItem -Path $DOTNET_NATIVE_SRC -Filter "*.cpp" | ForEach-Object { $_.FullName } + $ccArgs += Join-Path -Path $DOTNET_NATIVE_LIB -ChildPath "libnethost.a" if ($BUILD_CONFIGURATION -eq "debug") { $ccArgs += "-D", "DEBUG" } @@ -77,9 +78,6 @@ while ($true) { exit $proc.ExitCode } - Write-Host "[Info] Copying dependent libraries..." - Copy-Item (Join-Path $DOTNET_NATIVE_LIB "libhostfxr.so") $BUILD_OUTPUT -Force - # Build managed code Write-Host "[Info] Building Microsoft.SqlServer.CSharpExtension dll..." $DOTNET_MANAGED_SRC = Join-Path $DOTNET_EXTENSION_HOME "src/managed" diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 6d9c25a2..3d6b2a83 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #if defined(__WIN32) || defined(WINDOWS) #define STR(s) L ## s @@ -170,7 +171,12 @@ bool DotnetEnvironment::load_hostfxr() #if defined(_WIN32) || defined(WINDOWS) string_t hostfxr_location = m_root_path + STR("\\hostfxr.dll"); #else - string_t hostfxr_location = STR("libhostfxr.so"); + char buffer[4096]; + size_t buffer_size = sizeof(buffer); + if (get_hostfxr_path(buffer, &buffer_size, nullptr) != 0) { + return false; + } + string_t hostfxr_location(buffer); #endif void *lib = load_library(hostfxr_location.c_str()); m_init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config"); From 324efe6181681c5df893c2e39a067dd089e65fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Wed, 17 Sep 2025 09:07:16 +0200 Subject: [PATCH 07/45] Add debug compiler flag when building in debug. --- .../build/linux/build-dotnet-core-CSharp-extension.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 index 2bafbee1..228df9b5 100755 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 @@ -62,7 +62,7 @@ while ($true) { $ccArgs += Get-ChildItem -Path $DOTNET_NATIVE_SRC -Filter "*.cpp" | ForEach-Object { $_.FullName } $ccArgs += Join-Path -Path $DOTNET_NATIVE_LIB -ChildPath "libnethost.a" if ($BUILD_CONFIGURATION -eq "debug") { - $ccArgs += "-D", "DEBUG" + $ccArgs += "-D", "DEBUG", "-g" } if ($null -ne $env:CXX) { From 619f48adcff2e4e9dc4adffff0db80ca3f312ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Wed, 17 Sep 2025 09:50:31 +0200 Subject: [PATCH 08/45] Fix Linux/Windows portability issues. --- .../dotnet-core-CSharp/include/DotnetEnvironment.h | 10 ++++++++-- .../src/native/DotnetEnvironment.cpp | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index 93edcab7..f58b30e1 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -24,9 +24,11 @@ #if defined(__WIN32) || defined(WINDOWS) #define STR(s) L ## s #define CH(c) L ## c +#define PATH_SEPARATOR CH('\\') #else #define STR(s) s #define CH(c) c +#define PATH_SEPARATOR CH('/') #endif using namespace std; @@ -57,10 +59,14 @@ class DotnetEnvironment // Load managed assembly and get function pointer to a managed method // const string_t ManagedExtensionName = STR("Microsoft.SqlServer.CSharpExtension"); - const string_t ManagedExtensionPath = m_root_path + STR("\\") + ManagedExtensionName + STR(".dll"); + const string_t ManagedExtensionPath = m_root_path + PATH_SEPARATOR + ManagedExtensionName + STR(".dll"); const string_t ManagedExtensionType = ManagedExtensionName + STR(".CSharpExtension, ") + ManagedExtensionName; +#if defined(_WIN32) || defined(WINDOWS) const string_t ManagedExtensionMethod = to_utf16_str(method_name); - const string_t DelegateTypeName = ManagedExtensionName + STR(".CSharpExtension+") + to_utf16_str(method_name) + STR("Delegate, ") + ManagedExtensionName; +#else + const string_t ManagedExtensionMethod = method_name; +#endif + const string_t DelegateTypeName = ManagedExtensionName + STR(".CSharpExtension+") + ManagedExtensionMethod + STR("Delegate, ") + ManagedExtensionName; int rc = m_load_assembly_and_get_function_pointer( ManagedExtensionPath.c_str(), ManagedExtensionType.c_str(), diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 3d6b2a83..031a9725 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -75,7 +75,7 @@ short DotnetEnvironment::Init() // STEP 2: Initialize and start the .NET Core runtime // - const string_t config_path = m_root_path + STR("\\Microsoft.SqlServer.CSharpExtension.runtimeconfig.json"); + const string_t config_path = m_root_path + PATH_SEPARATOR + STR("Microsoft.SqlServer.CSharpExtension.runtimeconfig.json"); hostfxr_handle cxt = get_dotnet(config_path.c_str()); if (cxt == nullptr) { From 7e692937569a307856e8991e289c06b253665662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Wed, 17 Sep 2025 18:11:29 +0200 Subject: [PATCH 09/45] Add documentation about C# on Linux. --- .../dotnet-core-CSharp/README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/README.md b/language-extensions/dotnet-core-CSharp/README.md index 4fb63dfd..e089f490 100644 --- a/language-extensions/dotnet-core-CSharp/README.md +++ b/language-extensions/dotnet-core-CSharp/README.md @@ -5,11 +5,11 @@ Language Extensions is a feature of SQL Server used for executing external code. For more information about SQL Server Language Extensions, refer to this [documentation](https://docs.microsoft.com/en-us/sql/language-extensions/language-extensions-overview?view=sql-server-ver15). -The dotnet-core-CSharp-extension version in this repository is compatible with SQL Server 2019 CU3 onwards. It integrates .NET core in SQL Server and works with .NET 8.0 in **Windows only**. +The dotnet-core-CSharp-extension version in this repository is compatible with SQL Server 2019 CU3 onwards. It integrates .NET Core in SQL Server and works with .NET 8.0 and up on Windows and Linux. Currently, the extension supports the following data types: SQL_C_SLONG, SQL_C_ULONG, SQL_C_SSHORT, SQL_C_USHORT, SQL_C_SBIGINT, SQL_C_UBIGINT, SQL_C_STINYINT, SQL_C_UTINYINT, SQL_C_BIT, SQL_C_FLOAT, SQL_C_DOUBLE, SQL_C_CHAR, SQL_C_WCHAR, and SQL_C_NUMERIC. It supports the following SQL data types: int, bigint, smallint, tinyint, real, float, bit, char(n), varchar(n), nchar(n), nvarchar(n), decimal(p,s), and numeric(p,s). -To use this dotnet-core-CSharp-lang-extension.zip package, follow [this tutorial](./sample/regex/README.md). For any fixes or enhancements, you are welcome to modify, rebuild and use the binaries using the following instructions. +To use this `dotnet-core-CSharp-lang-extension.zip` (Windows) or `dotnet-core-CSharp-lang-extension.tar.gz` (Linux) package, follow [this tutorial](./sample/regex/README.md). For any fixes or enhancements, you are welcome to modify, rebuild and use the binaries using the following instructions. ## Building @@ -24,15 +24,24 @@ To use this dotnet-core-CSharp-lang-extension.zip package, follow [this tutorial - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\windows\release\nativecsharpextension.dll \ - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\windows\release\hostfxr.dll \ - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\windows\release\Microsoft.SqlServer.CSharpExtension.dll \ - - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\windows\release\Microsoft.SqlServer.CSharpExtension.runtimeconfig.json\ + - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\windows\release\Microsoft.SqlServer.CSharpExtension.runtimeconfig.json \ - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\windows\release\Microsoft.SqlServer.CSharpExtension.deps.json 5. Run [create-dotnet-core-CSharp-extension-zip.cmd](./build/windows/create-dotnet-core-CSharp-extension-zip.cmd) which will generate: \ - - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\target\debug\dotnet-core-CSharp-lang-extension.zip + - PATH\TO\ENLISTMENT\build-output\dotnet-core-CSharp-extension\windows\release\packages\dotnet-core-CSharp-lang-extension.zip This zip can be used in CREATE EXTERNAL LANGUAGE, as detailed in the tutorial in the Usage section below. ### Linux -Not Supported. + +1. Run [build-dotnet-core-CSharp-extension.ps1](./build/windows/build-dotnet-core-CSharp-extension.cmd) which will generate: \ + - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/libnativecsharpextension.so \ + - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/Microsoft.SqlServer.CSharpExtension.dll \ + - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/Microsoft.SqlServer.CSharpExtension.runtimeconfig.json \ + - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/Microsoft.SqlServer.CSharpExtension.deps.json + +2. Run [create-dotnet-core-CSharp-extension-zip.ps1](./build/windows/create-dotnet-core-CSharp-extension-zip.cmd) which will generate: \ + - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/packages/dotnet-core-CSharp-lang-extension.tar.gz + This tarball can be used in CREATE EXTERNAL LANGUAGE, as detailed in the tutorial in the Usage section below. ## Testing (Optional) From 849562052a39538c3382b14d0d99799d9d6a7bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Fri, 10 Oct 2025 10:31:58 +0200 Subject: [PATCH 10/45] Fix _WIN32 preprocessor symbol typo. --- .../dotnet-core-CSharp/include/DotnetEnvironment.h | 4 ++-- .../dotnet-core-CSharp/include/nativecsharpextension.h | 2 +- .../dotnet-core-CSharp/src/native/DotnetEnvironment.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index f58b30e1..e12856cc 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -10,7 +10,7 @@ //********************************************************************* #pragma once -#if defined(__WIN32) || defined(WINDOWS) +#if defined(_WIN32) || defined(WINDOWS) #include "Windows.h" #else #define E_FAIL -1 @@ -21,7 +21,7 @@ #include #include -#if defined(__WIN32) || defined(WINDOWS) +#if defined(_WIN32) || defined(WINDOWS) #define STR(s) L ## s #define CH(c) L ## c #define PATH_SEPARATOR CH('\\') diff --git a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h index 7907b6fe..3320dcb3 100644 --- a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h +++ b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h @@ -20,7 +20,7 @@ #include #include -#if defined(__WIN32) || defined(WINDOWS) +#if defined(_WIN32) || defined(WINDOWS) #include #endif diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 031a9725..31b44640 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -27,7 +27,7 @@ #include #include -#if defined(__WIN32) || defined(WINDOWS) +#if defined(_WIN32) || defined(WINDOWS) #define STR(s) L ## s #define CH(c) L ## c #else From a3593161a2aac4bfcaf15f084a524c8f1ef685d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Fri, 10 Oct 2025 10:48:33 +0200 Subject: [PATCH 11/45] Cleanup argument loop in Linux scripts. --- .../build-dotnet-core-CSharp-extension.ps1 | 19 ++++++++---------- ...reate-dotnet-core-CSharp-extension-zip.ps1 | 20 ++++++++----------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 index 228df9b5..7ec3394b 100755 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 @@ -11,14 +11,16 @@ if (Test-Path $DOTNET_EXTENSION_WORKING_DIR) { } New-Item -ItemType Directory -Path $DOTNET_EXTENSION_WORKING_DIR | Out-Null -$i = 0 +if ($args.Count -eq 0) { + # Default to release + $actualArgs = @("release") +} +else { + $actualArgs = $args +} # Process each build configuration -while ($true) { - $BUILD_CONFIGURATION = "" - if ($i -lt $args.Count) { - $BUILD_CONFIGURATION = $args[$i] - } +foreach ($BUILD_CONFIGURATION in $actualArgs) { $BUILD_CONFIGURATION = $BUILD_CONFIGURATION.ToLower() # Default to release if not debug if ($BUILD_CONFIGURATION -ne "debug") { @@ -96,11 +98,6 @@ while ($true) { } Write-Host "Success: Built dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." - - $i++ - if ($i -ge $args.Count) { - break - } } exit 0 diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 index 47c33386..06b20dd3 100755 --- a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 @@ -14,15 +14,16 @@ function CheckError { } } - -$i = 0 +if ($args.Count -eq 0) { + # Default to release + $actualArgs = @("release") +} +else { + $actualArgs = $args +} # Process each build configuration -while ($true) { - $BUILD_CONFIGURATION = "" - if ($i -lt $args.Count) { - $BUILD_CONFIGURATION = $args[$i] - } +foreach ($BUILD_CONFIGURATION in $actualArgs) { $BUILD_CONFIGURATION = $BUILD_CONFIGURATION.ToLower() # Default to release if not debug if ($BUILD_CONFIGURATION -ne "debug") { @@ -57,9 +58,4 @@ while ($true) { catch { CheckError 1 "Error: Failed to create zip for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" } - - $i++ - if ($i -ge $args.Count) { - break - } } From 7bda33d3cc7d325982af4508af35829f1ae359e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Fri, 27 Mar 2026 08:44:07 +0100 Subject: [PATCH 12/45] Fix doc links. --- language-extensions/dotnet-core-CSharp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/README.md b/language-extensions/dotnet-core-CSharp/README.md index e089f490..dc732e57 100644 --- a/language-extensions/dotnet-core-CSharp/README.md +++ b/language-extensions/dotnet-core-CSharp/README.md @@ -33,13 +33,13 @@ To use this `dotnet-core-CSharp-lang-extension.zip` (Windows) or `dotnet-core-CS ### Linux -1. Run [build-dotnet-core-CSharp-extension.ps1](./build/windows/build-dotnet-core-CSharp-extension.cmd) which will generate: \ +1. Run [build-dotnet-core-CSharp-extension.ps1](./build/linux/build-dotnet-core-CSharp-extension.ps1) which will generate: \ - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/libnativecsharpextension.so \ - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/Microsoft.SqlServer.CSharpExtension.dll \ - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/Microsoft.SqlServer.CSharpExtension.runtimeconfig.json \ - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/Microsoft.SqlServer.CSharpExtension.deps.json -2. Run [create-dotnet-core-CSharp-extension-zip.ps1](./build/windows/create-dotnet-core-CSharp-extension-zip.cmd) which will generate: \ +2. Run [create-dotnet-core-CSharp-extension-zip.ps1](./build/linux/create-dotnet-core-CSharp-extension-zip.ps1) which will generate: \ - PATH/TO/ENLISTMENT/build-output/dotnet-core-CSharp-extension/linux/release/packages/dotnet-core-CSharp-lang-extension.tar.gz This tarball can be used in CREATE EXTERNAL LANGUAGE, as detailed in the tutorial in the Usage section below. From a3b94238e9e8e6db4d6bae2fe01dc9fa33f7152d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Fri, 27 Mar 2026 08:47:09 +0100 Subject: [PATCH 13/45] Rationalize defines. --- .../include/DotnetEnvironment.h | 16 ++++++---------- .../src/native/DotnetEnvironment.cpp | 8 -------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index e12856cc..bc343740 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -12,25 +12,21 @@ #if defined(_WIN32) || defined(WINDOWS) #include "Windows.h" -#else -#define E_FAIL -1 -#define S_OK 0 -#endif - -#include -#include -#include - -#if defined(_WIN32) || defined(WINDOWS) #define STR(s) L ## s #define CH(c) L ## c #define PATH_SEPARATOR CH('\\') #else +#define E_FAIL -1 +#define S_OK 0 #define STR(s) s #define CH(c) c #define PATH_SEPARATOR CH('/') #endif +#include +#include +#include + using namespace std; using string_t = std::basic_string; diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 31b44640..7ab6e6fe 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -27,14 +27,6 @@ #include #include -#if defined(_WIN32) || defined(WINDOWS) -#define STR(s) L ## s -#define CH(c) L ## c -#else -#define STR(s) s -#define CH(c) c -#endif - using namespace std; using string_t = std::basic_string; From fd3596304487e04a8d282487b7f408fbbba445bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindstr=C3=B6m?= Date: Fri, 27 Mar 2026 09:00:05 +0100 Subject: [PATCH 14/45] Handle DOTNET_ROOT DotnetEnvironment on Linux. --- .../dotnet-core-CSharp/src/native/DotnetEnvironment.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 7ab6e6fe..20a06571 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -220,6 +220,7 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ params.dotnet_root = nullptr; // Get the required size for the environment variable +#if defined(_WIN32) || defined(WINDOWS) DWORD requiredSize = GetEnvironmentVariableW(L"DOTNET_ROOT", nullptr, 0); std::vector dotnet_root_buffer; if (requiredSize > 0) @@ -230,6 +231,9 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ params.dotnet_root = dotnet_root_buffer.data(); } } +#else + params.dotnet_root = getenv("DOTNET_ROOT"); +#endif int rc = m_init_fptr(config_path, ¶ms, &cxt); if (rc != 0 || cxt == nullptr) From 9a7a2d74f1f04ff2224b6137ac625a9ed476045d Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 31 Mar 2026 16:39:17 -0700 Subject: [PATCH 15/45] Linux support improvements on top of PR #64 - Replace PowerShell build scripts with bash (build, archive, restore) - Remove checked-in libnethost.a; fetch at build time via restore-packages.sh - Add libnethost.a existence guard in build script - Add convert_string() abstraction in DotnetEnvironment - Conditional nethost.h include (Linux only), HOSTFXR_PATH_BUFFER_SIZE constant - Add LOG_ERROR on hostfxr/dlopen/dlsym failures (release-safe, not just assert) - Guard S_OK/E_FAIL macros (#ifndef) on non-Windows - Add Linux test build/run scripts and CMake platform support - Add -fshort-wchar and replace std::wstring with vector/raw wchar_t* in test code to avoid ABI mismatch under -fshort-wchar on Linux - Add platform-conditional HintPath in test csproj - Add Linux dlopen/dlsym equivalents in test API code; throw on readlink failure (cherry picked from commit 2497aa275ee6861eabdf8635eb94d3e6313b27b1) --- .../build-dotnet-core-CSharp-extension.ps1 | 103 ------------------ .../build-dotnet-core-CSharp-extension.sh | 94 ++++++++++++++++ ...create-dotnet-core-CSharp-extension-tar.sh | 61 +++++++++++ ...reate-dotnet-core-CSharp-extension-zip.ps1 | 61 ----------- .../build/linux/restore-packages.sh | 84 ++++++++++++++ .../include/DotnetEnvironment.h | 40 ++++--- .../dotnet-core-CSharp/lib/libnethost.a | Bin 178378 -> 0 bytes .../src/native/DotnetEnvironment.cpp | 82 +++++++++++--- ...build-dotnet-core-CSharp-extension-test.sh | 62 +++++++++++ .../run-dotnet-core-CSharp-extension-test.sh | 31 ++++++ .../dotnet-core-CSharp/test/include/Common.h | 12 +- ...osoft.SqlServer.CSharpExtensionTest.csproj | 7 +- .../test/src/native/CMakeLists.txt | 63 +++++++---- .../src/native/CSharpExtensionApiTests.cpp | 64 +++++++++++ .../src/native/CSharpGetOutputParamTests.cpp | 103 +++++++++--------- 15 files changed, 595 insertions(+), 272 deletions(-) delete mode 100755 language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 create mode 100644 language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh create mode 100644 language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh delete mode 100755 language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 create mode 100644 language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh delete mode 100644 language-extensions/dotnet-core-CSharp/lib/libnethost.a create mode 100644 language-extensions/dotnet-core-CSharp/test/build/linux/build-dotnet-core-CSharp-extension-test.sh create mode 100644 language-extensions/dotnet-core-CSharp/test/build/linux/run-dotnet-core-CSharp-extension-test.sh diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 deleted file mode 100755 index 7ec3394b..00000000 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.ps1 +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env pwsh - -# Set root and working directories -$ENL_ROOT = Join-Path $PSScriptRoot "../../../.." -$DOTNET_EXTENSION_HOME = Join-Path $ENL_ROOT "language-extensions/dotnet-core-CSharp" -$DOTNET_EXTENSION_WORKING_DIR = Join-Path $ENL_ROOT "build-output/dotnet-core-CSharp-extension/linux" - -# Clean and create working directory -if (Test-Path $DOTNET_EXTENSION_WORKING_DIR) { - Remove-Item $DOTNET_EXTENSION_WORKING_DIR -Recurse -Force -} -New-Item -ItemType Directory -Path $DOTNET_EXTENSION_WORKING_DIR | Out-Null - -if ($args.Count -eq 0) { - # Default to release - $actualArgs = @("release") -} -else { - $actualArgs = $args -} - -# Process each build configuration -foreach ($BUILD_CONFIGURATION in $actualArgs) { - $BUILD_CONFIGURATION = $BUILD_CONFIGURATION.ToLower() - # Default to release if not debug - if ($BUILD_CONFIGURATION -ne "debug") { - $BUILD_CONFIGURATION = "release" - } - - # Set target directory - $TARGET = Join-Path $ENL_ROOT "build-output/dotnet-core-CSharp-extension/target/$BUILD_CONFIGURATION" - - # Clean and create target directory - if (Test-Path $TARGET) { - Remove-Item $TARGET -Recurse -Force - } - New-Item -ItemType Directory -Path $TARGET | Out-Null - - Write-Host "[Info] Building dotnet-core-CSharp-extension nativecsharpextension dll..." - - # Set build output directory - $BUILD_OUTPUT = Join-Path $DOTNET_EXTENSION_WORKING_DIR $BUILD_CONFIGURATION - if (Test-Path $BUILD_OUTPUT) { - Remove-Item $BUILD_OUTPUT -Recurse -Force - } - New-Item -ItemType Directory -Path $BUILD_OUTPUT | Out-Null - Push-Location $BUILD_OUTPUT - - # Set source and include paths - $DOTNET_NATIVE_SRC = Join-Path $DOTNET_EXTENSION_HOME "src/native" - $DOTNET_NATIVE_INCLUDE = Join-Path $DOTNET_EXTENSION_HOME "include" - $EXTENSION_HOST_INCLUDE = Join-Path $ENL_ROOT "extension-host/include" - $DOTNET_NATIVE_LIB = Join-Path $DOTNET_EXTENSION_HOME "lib" - - # Build native code - $ccArgs = @( - "-shared", - "-fPIC", - "-o", "libnativecsharpextension.so", - "-I$DOTNET_NATIVE_INCLUDE", - "-I$EXTENSION_HOST_INCLUDE", - "-DLINUX" - ) - $ccArgs += Get-ChildItem -Path $DOTNET_NATIVE_SRC -Filter "*.cpp" | ForEach-Object { $_.FullName } - $ccArgs += Join-Path -Path $DOTNET_NATIVE_LIB -ChildPath "libnethost.a" - if ($BUILD_CONFIGURATION -eq "debug") { - $ccArgs += "-D", "DEBUG", "-g" - } - - if ($null -ne $env:CXX) { - $cxx = $env:CXX - } - else { - $cxx = "c++" - } - - $proc = Start-Process -FilePath $cxx -ArgumentList $ccArgs -NoNewWindow -Wait -PassThru - if ($proc.ExitCode -ne 0) { - Write-Host "Error: Failed to build nativecsharpextension for configuration=$BUILD_CONFIGURATION" - exit $proc.ExitCode - } - - # Build managed code - Write-Host "[Info] Building Microsoft.SqlServer.CSharpExtension dll..." - $DOTNET_MANAGED_SRC = Join-Path $DOTNET_EXTENSION_HOME "src/managed" - $dotnetProc = Start-Process -FilePath "dotnet" -ArgumentList @( - "build", - (Join-Path $DOTNET_MANAGED_SRC "Microsoft.SqlServer.CSharpExtension.csproj"), - "-m", - "-c", $BUILD_CONFIGURATION, - "-o", $BUILD_OUTPUT, - "--no-dependencies" - ) -NoNewWindow -Wait -PassThru - - if ($dotnetProc.ExitCode -ne 0) { - Write-Host "Error: Failed to build for Microsoft.SqlServer.CSharpExtension.dll for configuration=$BUILD_CONFIGURATION" - exit $dotnetProc.ExitCode - } - - Write-Host "Success: Built dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." -} - -exit 0 diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh new file mode 100644 index 00000000..f328c724 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Set root and working directories +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +DOTNET_EXTENSION_HOME="$ENL_ROOT/language-extensions/dotnet-core-CSharp" +DOTNET_EXTENSION_WORKING_DIR="$ENL_ROOT/build-output/dotnet-core-CSharp-extension/linux" + +# Clean and create working directory +rm -rf "$DOTNET_EXTENSION_WORKING_DIR" +mkdir -p "$DOTNET_EXTENSION_WORKING_DIR" + +# Default to release if no arguments provided +if [ $# -eq 0 ]; then + set -- "release" +fi + +# Process each build configuration +for BUILD_CONFIGURATION in "$@"; do + BUILD_CONFIGURATION="$(echo "$BUILD_CONFIGURATION" | tr '[:upper:]' '[:lower:]')" + + # Default to release if not debug + if [ "$BUILD_CONFIGURATION" != "debug" ]; then + BUILD_CONFIGURATION="release" + fi + + echo "[Info] Building dotnet-core-CSharp-extension libnativecsharpextension.so..." + + # Set build output directory + BUILD_OUTPUT="$DOTNET_EXTENSION_WORKING_DIR/$BUILD_CONFIGURATION" + rm -rf "$BUILD_OUTPUT" + mkdir -p "$BUILD_OUTPUT" + pushd "$BUILD_OUTPUT" > /dev/null + + # Set source and include paths + DOTNET_NATIVE_SRC="$DOTNET_EXTENSION_HOME/src/native" + DOTNET_NATIVE_INCLUDE="$DOTNET_EXTENSION_HOME/include" + EXTENSION_HOST_INCLUDE="$ENL_ROOT/extension-host/include" + DOTNET_NATIVE_LIB="$DOTNET_EXTENSION_HOME/lib" + + # Determine C++ compiler + CXX="${CXX:-c++}" + + # Build compiler arguments + CC_ARGS=( + -shared + -fPIC + -fshort-wchar + -std=c++17 + -o libnativecsharpextension.so + "-I$DOTNET_NATIVE_INCLUDE" + "-I$EXTENSION_HOST_INCLUDE" + ) + + if [ "$BUILD_CONFIGURATION" = "debug" ]; then + CC_ARGS+=(-DDEBUG -g) + fi + + # Add all .cpp source files + for src in "$DOTNET_NATIVE_SRC"/*.cpp; do + CC_ARGS+=("$src") + done + + # Link with static nethost library (must run restore-packages.sh first) + if [ ! -f "$DOTNET_NATIVE_LIB/libnethost.a" ]; then + echo "Error: libnethost.a not found at $DOTNET_NATIVE_LIB/libnethost.a" >&2 + echo " Run restore-packages.sh first to install the .NET SDK and copy libnethost.a." >&2 + exit 1 + fi + CC_ARGS+=("$DOTNET_NATIVE_LIB/libnethost.a") + # Link with libdl for dlopen/dlsym + CC_ARGS+=(-ldl) + + echo "[Info] Compiling with: $CXX ${CC_ARGS[*]}" + "$CXX" "${CC_ARGS[@]}" + + popd > /dev/null + + # Build managed code + echo "[Info] Building Microsoft.SqlServer.CSharpExtension dll..." + DOTNET_MANAGED_SRC="$DOTNET_EXTENSION_HOME/src/managed" + dotnet build \ + "$DOTNET_MANAGED_SRC/Microsoft.SqlServer.CSharpExtension.csproj" \ + -m \ + -c "$BUILD_CONFIGURATION" \ + -o "$BUILD_OUTPUT" \ + --no-restore \ + --no-dependencies + + echo "Success: Built dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." +done + +exit 0 diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh new file mode 100644 index 00000000..4c021ab3 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +DOTNET_EXTENSION_WORKING_DIR="$ENL_ROOT/build-output/dotnet-core-CSharp-extension/linux" + +check_error() { + local error_level=$1 + local error_message=$2 + if [ "$error_level" -ne 0 ]; then + echo "Error: $error_message" >&2 + exit "$error_level" + fi +} + +# Default to release if no arguments provided +if [ $# -eq 0 ]; then + set -- "release" +fi + +# Process each build configuration +for BUILD_CONFIGURATION in "$@"; do + BUILD_CONFIGURATION="$(echo "$BUILD_CONFIGURATION" | tr '[:upper:]' '[:lower:]')" + + # Default to release if not debug + if [ "$BUILD_CONFIGURATION" != "debug" ]; then + BUILD_CONFIGURATION="release" + fi + + BUILD_OUTPUT="$DOTNET_EXTENSION_WORKING_DIR/$BUILD_CONFIGURATION" + mkdir -p "$BUILD_OUTPUT/packages" + + # Delete the ref folder so that the tarball can be loaded by the SPEES + rm -rf "$BUILD_OUTPUT/ref" + + # Collect files to compress + FILES_TO_COMPRESS=() + FILES_TO_COMPRESS+=("Microsoft.SqlServer.CSharpExtension.runtimeconfig.json") + FILES_TO_COMPRESS+=("Microsoft.SqlServer.CSharpExtension.deps.json") + + # Add all .dll and .so files + for f in "$BUILD_OUTPUT"/*.dll "$BUILD_OUTPUT"/*.so; do + [ -e "$f" ] && FILES_TO_COMPRESS+=("$(basename "$f")") + done + + # Include .pdb files for debug builds + if [ "$BUILD_CONFIGURATION" = "debug" ]; then + for f in "$BUILD_OUTPUT"/*.pdb; do + [ -e "$f" ] && FILES_TO_COMPRESS+=("$(basename "$f")") + done + fi + + # Package the binaries + pushd "$BUILD_OUTPUT" > /dev/null + tar -czvf "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension.tar.gz" "${FILES_TO_COMPRESS[@]}" + check_error $? "Failed to create tarball for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" + popd > /dev/null + + echo "Success: Compressed dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." +done diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 deleted file mode 100755 index 06b20dd3..00000000 --- a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-zip.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env pwsh - -$ENL_ROOT = Join-Path $PSScriptRoot "../../../.." -$DOTNET_EXTENSION_WORKING_DIR = Join-Path -Path $ENL_ROOT -ChildPath "build-output/dotnet-core-CSharp-extension/linux" - -function CheckError { - param( - [int]$errorLevel, - [string]$errorMessage - ) - if ($errorLevel -ne 0) { - Write-Error $errorMessage - exit $errorLevel - } -} - -if ($args.Count -eq 0) { - # Default to release - $actualArgs = @("release") -} -else { - $actualArgs = $args -} - -# Process each build configuration -foreach ($BUILD_CONFIGURATION in $actualArgs) { - $BUILD_CONFIGURATION = $BUILD_CONFIGURATION.ToLower() - # Default to release if not debug - if ($BUILD_CONFIGURATION -ne "debug") { - $BUILD_CONFIGURATION = "release" - } - - $BUILD_OUTPUT = Join-Path -Path $DOTNET_EXTENSION_WORKING_DIR -ChildPath $BUILD_CONFIGURATION - New-Item -ItemType Directory -Force -Path "$BUILD_OUTPUT/packages" | Out-Null - - # Delete the ref folder so that the zip can be loaded by the SPEES - Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "$BUILD_OUTPUT/ref" - - # Define files to compress, conditionally including .pdb files if BUILD_CONFIGURATION is "debug" - $FILES_TO_COMPRESS = @( - "Microsoft.SqlServer.CSharpExtension.runtimeconfig.json", - "Microsoft.SqlServer.CSharpExtension.deps.json" - ) - $FILES_TO_COMPRESS += Get-ChildItem -Path $BUILD_OUTPUT -Filter "*.dll" | ForEach-Object { $_.Name } - $FILES_TO_COMPRESS += Get-ChildItem -Path $BUILD_OUTPUT -Filter "*.so" | ForEach-Object { $_.Name } - if ($BUILD_CONFIGURATION -ieq "debug") { - $FILES_TO_COMPRESS += Get-ChildItem -Path $BUILD_OUTPUT -Filter "*.pdb" | ForEach-Object { $_.Name } - } - - # Package the signed binaries. - try { - Push-Location $BUILD_OUTPUT - & tar -czvf "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension.tar.gz" $FILES_TO_COMPRESS - CheckError $LASTEXITCODE "Error: Failed to create zip for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" - Pop-Location - Write-Host "Success: Compressed dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." - } - catch { - CheckError 1 "Error: Failed to create zip for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" - } -} diff --git a/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh b/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh new file mode 100644 index 00000000..4d104afc --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Note: errexit (-e) is intentionally NOT set. Each significant command is +# followed by check_exit_code, which prints a descriptive message and exits +# with the failing command's status. Enabling -e would abort before those +# messages could be emitted, making failures harder to diagnose. +set -uo pipefail + +check_exit_code() { + local exit_code=$? + if [ ${exit_code} -eq 0 ]; then + echo "$1" + else + echo "$2" + exit ${exit_code} + fi +} + +# Install .NET SDK 8.0 for building the C# extension managed code +# and for obtaining libnethost.a (used by the native host to discover hostfxr). +# + +# Check if dotnet is already installed and is version 8.x +if command -v dotnet &>/dev/null && dotnet --version 2>/dev/null | grep -q "^8\."; then + echo "Info: .NET SDK 8.x already installed ($(dotnet --version))" +else + echo "Info: Installing .NET SDK 8.0..." + apt-get update + apt-get install -y --no-install-recommends wget apt-transport-https + wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh + chmod +x /tmp/dotnet-install.sh + /tmp/dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet + ln -sf /usr/share/dotnet/dotnet /usr/bin/dotnet + check_exit_code "Success: Installed .NET SDK 8.0" "Error: Failed to install .NET SDK 8.0" +fi + +# Copy libnethost.a from the .NET SDK runtime packs into the extension's lib directory. +# The SDK ships this as part of the AppHost pack. +# +SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +ENL_ROOT=${SCRIPTDIR}/../../../.. +DOTNET_EXTENSION_HOME=${ENL_ROOT}/language-extensions/dotnet-core-CSharp + +NETHOST_DIR=$(find /usr/share/dotnet -path "*/runtimes/linux-x64/native/libnethost.a" -print -quit 2>/dev/null) +if [ -z "$NETHOST_DIR" ]; then + echo "Error: libnethost.a not found in .NET SDK. Ensure .NET SDK 8.0 is installed." + exit 1 +fi + +echo "Info: Found libnethost.a at: $NETHOST_DIR" +mkdir -p "${DOTNET_EXTENSION_HOME}/lib" +cp "$NETHOST_DIR" "${DOTNET_EXTENSION_HOME}/lib/libnethost.a" +check_exit_code "Success: Copied libnethost.a to extension lib directory" "Error: Failed to copy libnethost.a" + +# Pre-restore NuGet packages while network is still available. +# OneBranch enables network isolation after package-restore phases, +# so dotnet restore must happen here rather than during the build step. +# +# Use the parent repo's NuGet.Config which points to the ADO artifact feed +# (with nuget.org as upstream) rather than the submodule's NuGet.Config +# which points directly to nuget.org (unreachable from OneBranch containers). +# +MANAGED_PROJ="${DOTNET_EXTENSION_HOME}/src/managed/Microsoft.SqlServer.CSharpExtension.csproj" +MANAGED_TEST_PROJ="${DOTNET_EXTENSION_HOME}/test/src/managed/Microsoft.SqlServer.CSharpExtensionTest.csproj" +PARENT_NUGET_CONFIG="${ENL_ROOT}/../NuGet.Config" + +echo "Info: Restoring NuGet packages for Microsoft.SqlServer.CSharpExtension..." +if [ -f "$PARENT_NUGET_CONFIG" ]; then + echo "Info: Using NuGet.Config from parent repo: $PARENT_NUGET_CONFIG" + dotnet restore "$MANAGED_PROJ" --configfile "$PARENT_NUGET_CONFIG" + check_exit_code "Success: NuGet packages restored (extension)" "Error: Failed to restore NuGet packages (extension)" + + echo "Info: Restoring NuGet packages for Microsoft.SqlServer.CSharpExtensionTest..." + dotnet restore "$MANAGED_TEST_PROJ" --configfile "$PARENT_NUGET_CONFIG" + check_exit_code "Success: NuGet packages restored (test)" "Error: Failed to restore NuGet packages (test)" +else + echo "Info: Parent NuGet.Config not found, using default" + dotnet restore "$MANAGED_PROJ" + check_exit_code "Success: NuGet packages restored (extension)" "Error: Failed to restore NuGet packages (extension)" + + dotnet restore "$MANAGED_TEST_PROJ" + check_exit_code "Success: NuGet packages restored (test)" "Error: Failed to restore NuGet packages (test)" +fi + +exit 0 diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index bc343740..a32c41e8 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -10,23 +10,34 @@ //********************************************************************* #pragma once -#if defined(_WIN32) || defined(WINDOWS) +#ifdef _WIN32 #include "Windows.h" +#else +// Provide the minimal HRESULT-style codes used by this extension on non-Windows +// platforms. Guard each definition so we never clash with a value supplied by +// another header, and parenthesize the values for safe use in expressions. +#ifndef S_OK +#define S_OK (0) +#endif +#ifndef E_FAIL +#define E_FAIL (-1) +#endif +#endif + +#include +#include +#include + +#ifdef _WIN32 #define STR(s) L ## s #define CH(c) L ## c #define PATH_SEPARATOR CH('\\') #else -#define E_FAIL -1 -#define S_OK 0 #define STR(s) s #define CH(c) c #define PATH_SEPARATOR CH('/') #endif -#include -#include -#include - using namespace std; using string_t = std::basic_string; @@ -57,11 +68,7 @@ class DotnetEnvironment const string_t ManagedExtensionName = STR("Microsoft.SqlServer.CSharpExtension"); const string_t ManagedExtensionPath = m_root_path + PATH_SEPARATOR + ManagedExtensionName + STR(".dll"); const string_t ManagedExtensionType = ManagedExtensionName + STR(".CSharpExtension, ") + ManagedExtensionName; -#if defined(_WIN32) || defined(WINDOWS) - const string_t ManagedExtensionMethod = to_utf16_str(method_name); -#else - const string_t ManagedExtensionMethod = method_name; -#endif + const string_t ManagedExtensionMethod = convert_string(method_name); const string_t DelegateTypeName = ManagedExtensionName + STR(".CSharpExtension+") + ManagedExtensionMethod + STR("Delegate, ") + ManagedExtensionName; int rc = m_load_assembly_and_get_function_pointer( ManagedExtensionPath.c_str(), @@ -85,9 +92,16 @@ class DotnetEnvironment load_assembly_and_get_function_pointer_fn m_load_assembly_and_get_function_pointer; string_t m_root_path; - // Convert utf8_str to utf16_str + // Convert a std::string to the platform string_t type. + // On Windows this is UTF-8 -> UTF-16; on Linux it is a no-op. + // + static string_t convert_string(const std::string& str); + +#ifdef _WIN32 + // Convert utf8_str to utf16_str (Windows only) // static string_t to_utf16_str(const std::string& utf8str); +#endif // Convert an int to string in hex. // diff --git a/language-extensions/dotnet-core-CSharp/lib/libnethost.a b/language-extensions/dotnet-core-CSharp/lib/libnethost.a deleted file mode 100644 index 9b333a48619ed1002570e148f9a916d5ab2c9dd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178378 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 diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 20a06571..e7bf1fa7 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -11,25 +11,29 @@ #include "DotnetEnvironment.h" #include "Logger.h" -#if defined(_WIN32) || defined(WINDOWS) +#ifdef _WIN32 #include "Windows.h" #else #include +#include #endif #include -#include #include #include #include #include #include #include -#include using namespace std; using string_t = std::basic_string; +// Named constant for hostfxr path buffer size on Linux +#ifndef _WIN32 +constexpr size_t HOSTFXR_PATH_BUFFER_SIZE = 4096; +#endif + //-------------------------------------------------------------------------------------------------- // Name: DotnetEnvironment::DotnetEnvironment // @@ -40,12 +44,7 @@ DotnetEnvironment::DotnetEnvironment( std::string language_params, std::string language_path, std::string public_library_path, - std::string private_library_path) : -#if defined(_WIN32) || defined(WINDOWS) - m_root_path(to_utf16_str(language_path)) -#else - m_root_path(language_path) -#endif + std::string private_library_path) : m_root_path(convert_string(language_path)) { } @@ -84,7 +83,23 @@ short DotnetEnvironment::Init() return S_OK; } -#if defined(_WIN32) || defined(WINDOWS) +//-------------------------------------------------------------------------------------------------- +// Name: DotnetEnvironment::convert_string +// +// Description: +// Convert a std::string to the platform string_t type. +// On Windows this performs UTF-8 to UTF-16 conversion; on Linux it is a no-op. +// +string_t DotnetEnvironment::convert_string(const std::string& str) +{ +#ifdef _WIN32 + return to_utf16_str(str); +#else + return str; +#endif +} + +#ifdef _WIN32 //-------------------------------------------------------------------------------------------------- // Name: DotnetEnvironment::to_utf16_str // @@ -124,11 +139,20 @@ string DotnetEnvironment::to_hex_string(int value) void* DotnetEnvironment::load_library(const char_t *path) { LOG("DotnetEnvironment::load_library"); -#if defined(_WIN32) || defined(WINDOWS) +#ifdef _WIN32 HMODULE h = ::LoadLibraryW(path); #else void *h = dlopen(path, RTLD_LAZY); #endif + if (h == nullptr) + { +#ifdef _WIN32 + LOG_ERROR("Failed to load library"); +#else + const char *dl_error = dlerror(); + LOG_ERROR(std::string("Failed to load library: ") + (dl_error != nullptr ? dl_error : "unknown error")); +#endif + } assert(h != nullptr); return (void*)h; } @@ -142,11 +166,20 @@ void* DotnetEnvironment::load_library(const char_t *path) void* DotnetEnvironment::get_export(void *h, const char *name) { LOG("DotnetEnvironment::get_export"); -#if defined(_WIN32) || defined(WINDOWS) +#ifdef _WIN32 void *f = ::GetProcAddress((HMODULE)h, name); #else void *f = dlsym(h, name); #endif + if (f == nullptr) + { +#ifdef _WIN32 + LOG_ERROR(std::string("Failed to get export: ") + name); +#else + const char *dl_error = dlerror(); + LOG_ERROR(std::string("Failed to get export '") + name + "': " + (dl_error != nullptr ? dl_error : "unknown error")); +#endif + } assert(f != nullptr); return f; } @@ -160,17 +193,27 @@ void* DotnetEnvironment::get_export(void *h, const char *name) bool DotnetEnvironment::load_hostfxr() { LOG("DotnetEnvironment::load_hostfxr"); -#if defined(_WIN32) || defined(WINDOWS) +#ifdef _WIN32 string_t hostfxr_location = m_root_path + STR("\\hostfxr.dll"); #else - char buffer[4096]; + // On Linux, use nethost to dynamically locate hostfxr. + // This honors DOTNET_ROOT and install_location files, which is more + // portable across distributions (e.g. RHEL vs Debian). + char buffer[HOSTFXR_PATH_BUFFER_SIZE]; size_t buffer_size = sizeof(buffer); - if (get_hostfxr_path(buffer, &buffer_size, nullptr) != 0) { + if (get_hostfxr_path(buffer, &buffer_size, nullptr) != 0) + { + LOG_ERROR("Failed to locate hostfxr via nethost"); return false; } string_t hostfxr_location(buffer); #endif void *lib = load_library(hostfxr_location.c_str()); + if (lib == nullptr) + { + LOG_ERROR("Failed to load hostfxr library"); + return false; + } m_init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config"); m_get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate"); m_close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close"); @@ -219,8 +262,8 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ params.host_path = nullptr; params.dotnet_root = nullptr; +#ifdef _WIN32 // Get the required size for the environment variable -#if defined(_WIN32) || defined(WINDOWS) DWORD requiredSize = GetEnvironmentVariableW(L"DOTNET_ROOT", nullptr, 0); std::vector dotnet_root_buffer; if (requiredSize > 0) @@ -232,7 +275,12 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ } } #else - params.dotnet_root = getenv("DOTNET_ROOT"); + // On Linux, read DOTNET_ROOT from the environment (char-based) + const char *dotnet_root_env = getenv("DOTNET_ROOT"); + if (dotnet_root_env != nullptr) + { + params.dotnet_root = dotnet_root_env; + } #endif int rc = m_init_fptr(config_path, ¶ms, &cxt); diff --git a/language-extensions/dotnet-core-CSharp/test/build/linux/build-dotnet-core-CSharp-extension-test.sh b/language-extensions/dotnet-core-CSharp/test/build/linux/build-dotnet-core-CSharp-extension-test.sh new file mode 100644 index 00000000..b93e4453 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/test/build/linux/build-dotnet-core-CSharp-extension-test.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Set environment variables +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENL_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +DOTNETCORE_CSHARP_EXTENSION_TEST_HOME="$ENL_ROOT/language-extensions/dotnet-core-CSharp/test" +DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR="$ENL_ROOT/build-output/dotnet-core-CSharp-extension-test/linux" + +# Clean and create build working directory +rm -rf "$DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR" +mkdir -p "$DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR" + +# Default to release if no arguments +if [ $# -eq 0 ]; then + set -- "release" +fi + +for CMAKE_CONFIGURATION in "$@"; do + CMAKE_CONFIGURATION="$(echo "$CMAKE_CONFIGURATION" | tr '[:upper:]' '[:lower:]')" + if [ "$CMAKE_CONFIGURATION" != "debug" ]; then + CMAKE_CONFIGURATION="release" + fi + + BUILD_OUTPUT="$DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR/$CMAKE_CONFIGURATION" + mkdir -p "$BUILD_OUTPUT" + pushd "$BUILD_OUTPUT" > /dev/null + + echo "[INFO] Generating dotnet-core-CSharp-extension test project build files using CMAKE_CONFIGURATION=$CMAKE_CONFIGURATION" + + # Call cmake to generate makefiles + cmake \ + -DCMAKE_INSTALL_PREFIX:PATH="$DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR/$CMAKE_CONFIGURATION" \ + -DENL_ROOT="$ENL_ROOT" \ + -DCMAKE_CONFIGURATION="$CMAKE_CONFIGURATION" \ + -DPLATFORM=linux \ + "$DOTNETCORE_CSHARP_EXTENSION_TEST_HOME/src/native" + + echo "[INFO] Building dotnet-core-CSharp-extension test project using CMAKE_CONFIGURATION=$CMAKE_CONFIGURATION" + + # Build managed test executor + dotnet build \ + "$DOTNETCORE_CSHARP_EXTENSION_TEST_HOME/src/managed/Microsoft.SqlServer.CSharpExtensionTest.csproj" \ + -m \ + -c "$CMAKE_CONFIGURATION" \ + -o "$BUILD_OUTPUT" \ + --no-restore \ + --no-dependencies + + # Delete Microsoft.SqlServer.CSharpExtension.dll to avoid test executor referencing it + # instead of the extension itself + rm -f "$BUILD_OUTPUT/Microsoft.SqlServer.CSharpExtension.dll" + + # Build native test executable + cmake --build . --config "$CMAKE_CONFIGURATION" --target install + + popd > /dev/null + + echo "Success: Built dotnet-core-CSharp-extension-test for $CMAKE_CONFIGURATION configuration." +done + +exit 0 diff --git a/language-extensions/dotnet-core-CSharp/test/build/linux/run-dotnet-core-CSharp-extension-test.sh b/language-extensions/dotnet-core-CSharp/test/build/linux/run-dotnet-core-CSharp-extension-test.sh new file mode 100644 index 00000000..978763d2 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/test/build/linux/run-dotnet-core-CSharp-extension-test.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Set environment variables +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENL_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR="$ENL_ROOT/build-output/dotnet-core-CSharp-extension-test/linux" + +# Default to release if no arguments +if [ $# -eq 0 ]; then + set -- "release" +fi + +for CMAKE_CONFIGURATION in "$@"; do + CMAKE_CONFIGURATION="$(echo "$CMAKE_CONFIGURATION" | tr '[:upper:]' '[:lower:]')" + if [ "$CMAKE_CONFIGURATION" != "debug" ]; then + CMAKE_CONFIGURATION="release" + fi + + TEST_DIR="$DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR/$CMAKE_CONFIGURATION" + pushd "$TEST_DIR" > /dev/null + + echo "[INFO] Running dotnet-core-CSharp-extension tests for $CMAKE_CONFIGURATION configuration" + + ./dotnet-core-CSharp-extension-test \ + --gtest_output=xml:"$ENL_ROOT/out/TestReport_dotnet-core-csharp-extension-test.xml" + + popd > /dev/null +done + +exit 0 diff --git a/language-extensions/dotnet-core-CSharp/test/include/Common.h b/language-extensions/dotnet-core-CSharp/test/include/Common.h index 01c09f56..1e6eae60 100644 --- a/language-extensions/dotnet-core-CSharp/test/include/Common.h +++ b/language-extensions/dotnet-core-CSharp/test/include/Common.h @@ -12,11 +12,21 @@ #ifdef _WIN64 #include +#else +#include +#include +#include +typedef void* HINSTANCE; #endif -#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING #include +#include +#ifdef _WIN32 +#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING #include +#else +#include +#endif #include #include #include diff --git a/language-extensions/dotnet-core-CSharp/test/src/managed/Microsoft.SqlServer.CSharpExtensionTest.csproj b/language-extensions/dotnet-core-CSharp/test/src/managed/Microsoft.SqlServer.CSharpExtensionTest.csproj index d0f49b7d..1a1515fc 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/managed/Microsoft.SqlServer.CSharpExtensionTest.csproj +++ b/language-extensions/dotnet-core-CSharp/test/src/managed/Microsoft.SqlServer.CSharpExtensionTest.csproj @@ -14,9 +14,14 @@ - + ..\..\..\..\..\build-output\dotnet-core-CSharp-extension\windows\$(Configuration)\Microsoft.SqlServer.CSharpExtension.dll + + + ..\..\..\..\..\build-output\dotnet-core-CSharp-extension\linux\release\Microsoft.SqlServer.CSharpExtension.dll + + diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CMakeLists.txt b/language-extensions/dotnet-core-CSharp/test/src/native/CMakeLists.txt index 952df261..63680887 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CMakeLists.txt +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CMakeLists.txt @@ -4,6 +4,9 @@ cmake_minimum_required (VERSION 3.5) # project(dotnet-core-CSharp-extension-test VERSION 1.0 LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + # All string comparisons are CASE SENSITIVE in CMAKE. Make all strings lower before comparisons! # string(TOLOWER ${PLATFORM} PLATFORM) @@ -23,34 +26,50 @@ add_executable(dotnet-core-CSharp-extension-test ${DOTNETCORE_CSHARP_EXTENSION_TEST_SOURCE_FILES} ) -target_compile_options(dotnet-core-CSharp-extension-test PRIVATE - "$<$:/std:c++17>" - "$<$>:-std=c++17>" -) - -# Set the DLLEXPORT variable to export symbols -# -add_definitions(-DWIN_EXPORT -D_WIN64 -D_WINDOWS) -target_compile_definitions(dotnet-core-CSharp-extension-test PRIVATE WIN_EXPORT) - - file(TO_CMAKE_PATH ${ENL_ROOT}/language-extensions/dotnet-core-CSharp/test/include/ DOTNETCORE_CSHARP_EXTENSION_TEST_INCLUDE_DIR) file(TO_CMAKE_PATH ${ENL_ROOT}/build-output/dotnet-core-CSharp-extension-test/${PLATFORM}/ DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR) file(TO_CMAKE_PATH ${DOTNETCORE_CSHARP_EXTENSION_TEST_WORKING_DIR}/${CMAKE_CONFIGURATION} DOTNETCORE_CSHARP_EXTENSION_TEST_INSTALL_DIR) -file(TO_CMAKE_PATH ${ENL_ROOT}/packages/Microsoft.googletest.v140.windesktop.msvcstl.dyn.rt-dyn.1.8.1.3 GTEST_HOME) -file(TO_CMAKE_PATH ${GTEST_HOME}/build/native/include GTEST_INCLUDE_DIR) -file(TO_CMAKE_PATH ${GTEST_HOME}/lib/native/v140/windesktop/msvcstl/dyn/rt-dyn/x64/${CMAKE_CONFIGURATION} GTEST_LIB_PATH) +if(WIN32) + # Set the DLLEXPORT variable to export symbols + # + add_definitions(-DWIN_EXPORT -D_WIN64 -D_WINDOWS) + target_compile_definitions(dotnet-core-CSharp-extension-test PRIVATE WIN_EXPORT) -# MDd is for debug DLL and MD is for release DLL -# -if (${CMAKE_CONFIGURATION} STREQUAL debug) - set(COMPILE_OPTIONS ${COMPILE_OPTIONS} /MDd) - find_library(GTEST_LIB gtestd ${GTEST_LIB_PATH}) - set(CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG} /Od) + file(TO_CMAKE_PATH ${ENL_ROOT}/packages/Microsoft.googletest.v140.windesktop.msvcstl.dyn.rt-dyn.1.8.1.3 GTEST_HOME) + file(TO_CMAKE_PATH ${GTEST_HOME}/build/native/include GTEST_INCLUDE_DIR) + file(TO_CMAKE_PATH ${GTEST_HOME}/lib/native/v140/windesktop/msvcstl/dyn/rt-dyn/x64/${CMAKE_CONFIGURATION} GTEST_LIB_PATH) + + # MDd is for debug DLL and MD is for release DLL + # + if (${CMAKE_CONFIGURATION} STREQUAL debug) + set(COMPILE_OPTIONS ${COMPILE_OPTIONS} /MDd) + find_library(GTEST_LIB gtestd ${GTEST_LIB_PATH}) + set(CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG} /Od) + else() + set(COMPILE_OPTIONS ${COMPILE_OPTIONS} /MD /O2) + find_library(GTEST_LIB gtest ${GTEST_LIB_PATH}) + endif() else() - set(COMPILE_OPTIONS ${COMPILE_OPTIONS} /MD /O2) - find_library(GTEST_LIB gtest ${GTEST_LIB_PATH}) + # On Linux, use the googletest built from source (installed into build-output/googletest/linux/) + # + file(TO_CMAKE_PATH ${ENL_ROOT}/build-output/googletest/linux GTEST_BUILD_ROOT) + file(TO_CMAKE_PATH ${GTEST_BUILD_ROOT}/googletest-src/googletest/include GTEST_INCLUDE_DIR) + + # The Linux googletest build produces libgtest.a for all configurations + # (no separate debug/release naming convention). + # + file(TO_CMAKE_PATH ${GTEST_BUILD_ROOT}/lib/libgtest.a GTEST_LIB) + + # Use -fshort-wchar so wchar_t is 2 bytes, matching SQL Server's UTF-16 + # and the managed C# extension (consistent with R/Python/Java extensions). + # + target_compile_options(dotnet-core-CSharp-extension-test PRIVATE -fshort-wchar) + + # Link with dl for dlopen/dlsym and pthread for gtest + # + find_package(Threads REQUIRED) + target_link_libraries(dotnet-core-CSharp-extension-test Threads::Threads ${CMAKE_DL_LIBS}) endif() # This is not a standard include path so test projects need diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExtensionApiTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExtensionApiTests.cpp index 150167f6..0f6faa21 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExtensionApiTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExtensionApiTests.cpp @@ -11,8 +11,18 @@ //********************************************************************* #include "CSharpExtensionApiTests.h" +#ifndef _WIN32 +#include +#include +#include +#endif + using namespace std; +#ifdef _WIN32 namespace fs = experimental::filesystem; +#else +namespace fs = filesystem; +#endif namespace ExtensionApiTest { @@ -92,6 +102,7 @@ namespace ExtensionApiTest // void CSharpExtensionApiTests::SetUpPath() { +#ifdef _WIN32 char path[MAX_PATH+1] = {0}; GetModuleFileName(NULL, path, MAX_PATH); fs::path exePath = path; @@ -102,6 +113,24 @@ namespace ExtensionApiTest sm_extensionPath = (buildOutputPath / "dotnet-core-CSharp-extension/windows/release").string(); #endif sm_libPath = exePath.parent_path().string(); +#else + char path[PATH_MAX] = {0}; + ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (len == -1) + { + throw std::runtime_error( + std::string("readlink(\"/proc/self/exe\") failed: ") + std::strerror(errno)); + } + path[len] = '\0'; + fs::path exePath = path; + fs::path buildOutputPath = exePath.parent_path().parent_path().parent_path().parent_path(); + #if defined(_DEBUG) + sm_extensionPath = (buildOutputPath / "dotnet-core-CSharp-extension/linux/debug").string(); + #else + sm_extensionPath = (buildOutputPath / "dotnet-core-CSharp-extension/linux/release").string(); + #endif + sm_libPath = exePath.parent_path().string(); +#endif } // Name: CSharpExtensionApiTest::SetupVariables @@ -220,6 +249,7 @@ namespace ExtensionApiTest // void CSharpExtensionApiTests::GetHandles() { +#ifdef _WIN32 sm_libHandle = LoadLibrary((sm_extensionPath+"\\nativecsharpextension.dll").c_str()); EXPECT_TRUE(sm_libHandle != nullptr); @@ -260,6 +290,40 @@ namespace ExtensionApiTest sm_uninstallExternalLibraryFuncPtr = reinterpret_cast( GetProcAddress(sm_libHandle, "UninstallExternalLibrary")); EXPECT_TRUE(sm_uninstallExternalLibraryFuncPtr != nullptr); +#else + sm_libHandle = dlopen((sm_extensionPath+"/libnativecsharpextension.so").c_str(), RTLD_LAZY); + EXPECT_TRUE(sm_libHandle != nullptr) << "dlopen failed: " << dlerror(); + + sm_initFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "Init")); + EXPECT_TRUE(sm_initFuncPtr != nullptr); + + sm_initSessionFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "InitSession")); + EXPECT_TRUE(sm_initSessionFuncPtr != nullptr); + + sm_initColumnFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "InitColumn")); + EXPECT_TRUE(sm_initColumnFuncPtr != nullptr); + + sm_initParamFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "InitParam")); + EXPECT_TRUE(sm_initParamFuncPtr != nullptr); + + sm_executeFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "Execute")); + EXPECT_TRUE(sm_executeFuncPtr != nullptr); + + sm_getResultColumnFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "GetResultColumn")); + EXPECT_TRUE(sm_getResultColumnFuncPtr != nullptr); + + sm_getResultsFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "GetResults")); + EXPECT_TRUE(sm_getResultsFuncPtr != nullptr); + + sm_getOutputParamFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "GetOutputParam")); + EXPECT_TRUE(sm_getOutputParamFuncPtr != nullptr); + + sm_cleanupSessionFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "CleanupSession")); + EXPECT_TRUE(sm_cleanupSessionFuncPtr != nullptr); + + sm_cleanupFuncPtr = reinterpret_cast(dlsym(sm_libHandle, "Cleanup")); + EXPECT_TRUE(sm_cleanupFuncPtr != nullptr); +#endif } //---------------------------------------------------------------------------------------------- diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpGetOutputParamTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpGetOutputParamTests.cpp index f59015fa..842c3071 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpGetOutputParamTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpGetOutputParamTests.cpp @@ -560,25 +560,19 @@ namespace ExtensionApiTest EXPECT_EQ(outputSchemaColumnsNumber, 0); - const vector ExpectedParamValueStrings = { - // Test simple NCHAR(5) value with exact string length as the type allows i.e. here 5. - // - L"HELLO", - // Test NVARCHAR(6) value with string length more than the type allows - expected truncation. - // - L"C#Exte", - // Test a 0 length string - // - L"" , - // Test NCHAR(10) value with string length less than the type allows. - // - L"WORLD"}; + // Use raw wchar_t* pointers instead of std::wstring to avoid ABI + // issues with -fshort-wchar on Linux. + // + const wchar_t* expectedStr0 = L"HELLO"; + const wchar_t* expectedStr1 = L"C#Exte"; + const wchar_t* expectedStr2 = L""; + const wchar_t* expectedStr3 = L"WORLD"; vector expectedParamValues = { - ExpectedParamValueStrings[0].c_str(), - ExpectedParamValueStrings[1].c_str(), - ExpectedParamValueStrings[2].c_str(), - ExpectedParamValueStrings[3].c_str(), + expectedStr0, + expectedStr1, + expectedStr2, + expectedStr3, // Test None returned in a NVARCHAR(5) parameter. // @@ -591,10 +585,10 @@ namespace ExtensionApiTest // strLenOrInd is in bytes for NCHAR/NVARCHAR, so multiply by sizeof(wchar_t) // vector expectedStrLenOrInd = { - static_cast(ExpectedParamValueStrings[0].length() * sizeof(wchar_t)), - static_cast(ExpectedParamValueStrings[1].length() * sizeof(wchar_t)), - static_cast(ExpectedParamValueStrings[2].length() * sizeof(wchar_t)), - static_cast(ExpectedParamValueStrings[3].length() * sizeof(wchar_t)), + static_cast(GetWStringLength(expectedStr0) * sizeof(wchar_t)), + static_cast(GetWStringLength(expectedStr1) * sizeof(wchar_t)), + static_cast(GetWStringLength(expectedStr2) * sizeof(wchar_t)), + static_cast(GetWStringLength(expectedStr3) * sizeof(wchar_t)), SQL_NULL_DATA, SQL_NULL_DATA }; @@ -651,30 +645,34 @@ namespace ExtensionApiTest EXPECT_EQ(outputSchemaColumnsNumber, 0); - // Build expected values to match what the C# executor returns + // Build expected values to match what the C# executor returns. + // Use vector instead of wstring to avoid ABI issues with -fshort-wchar on Linux. // - wstring largeAsciiString(10000, L'A'); + vector largeAsciiData(10000, L'A'); + largeAsciiData.push_back(L'\0'); // Build Unicode pattern string: "你好世界€" repeated 1000 times = 5000 characters // - wstring unicodePattern = L"你好世界€"; - wstring largeUnicodeString; - largeUnicodeString.reserve(5000); + const wchar_t unicodePattern[] = L"你好世界€"; + SQLINTEGER patternLen = GetWStringLength(unicodePattern); + vector largeUnicodeData; + largeUnicodeData.reserve(5001); for (int i = 0; i < 1000; i++) { - largeUnicodeString += unicodePattern; + largeUnicodeData.insert(largeUnicodeData.end(), unicodePattern, unicodePattern + patternLen); } + largeUnicodeData.push_back(L'\0'); vector expectedParamValues = { - largeAsciiString.c_str(), - largeUnicodeString.c_str(), + largeAsciiData.data(), + largeUnicodeData.data(), nullptr }; // strLenOrInd is in bytes for NCHAR/NVARCHAR, so multiply by sizeof(wchar_t) // vector expectedStrLenOrInd = { - static_cast(largeAsciiString.length() * sizeof(wchar_t)), - static_cast(largeUnicodeString.length() * sizeof(wchar_t)), + static_cast((largeAsciiData.size() - 1) * sizeof(wchar_t)), + static_cast((largeUnicodeData.size() - 1) * sizeof(wchar_t)), SQL_NULL_DATA }; GetWStringOutputParam( @@ -728,29 +726,29 @@ namespace ExtensionApiTest EXPECT_EQ(outputSchemaColumnsNumber, 0); - // Build expected values to match what the C# executor returns + // Build expected values to match what the C# executor returns. + // Use raw wchar_t* pointers instead of std::wstring to avoid ABI + // issues with -fshort-wchar on Linux. // Note: Emoji are surrogate pairs in UTF-16, so 😀 = 2 wchar_t, 👍 = 2 wchar_t // - const vector ExpectedParamValueStrings = { - L"Hi\U0001F600\U0001F44D", // "Hi" + grinning face + thumbs up (6 UTF-16 code units) - L"Café résumé naïve", // Accented characters (17 chars) - L"Hello世界こんにちは", // Mixed scripts (12 chars) - L"€100 £50 ¥1000 ©®™" // Currency and special symbols (18 chars) - }; + const wchar_t* expectedStr0 = L"Hi\U0001F600\U0001F44D"; // "Hi" + grinning face + thumbs up (6 UTF-16 code units) + const wchar_t* expectedStr1 = L"Café résumé naïve"; // Accented characters (17 chars) + const wchar_t* expectedStr2 = L"Hello世界こんにちは"; // Mixed scripts (12 chars) + const wchar_t* expectedStr3 = L"€100 £50 ¥1000 ©®™"; // Currency and special symbols (18 chars) vector expectedParamValues = { - ExpectedParamValueStrings[0].c_str(), - ExpectedParamValueStrings[1].c_str(), - ExpectedParamValueStrings[2].c_str(), - ExpectedParamValueStrings[3].c_str() }; + expectedStr0, + expectedStr1, + expectedStr2, + expectedStr3 }; // strLenOrInd is in bytes for NCHAR/NVARCHAR, so multiply by sizeof(wchar_t) // vector expectedStrLenOrInd = { - static_cast(ExpectedParamValueStrings[0].length() * sizeof(wchar_t)), - static_cast(ExpectedParamValueStrings[1].length() * sizeof(wchar_t)), - static_cast(ExpectedParamValueStrings[2].length() * sizeof(wchar_t)), - static_cast(ExpectedParamValueStrings[3].length() * sizeof(wchar_t)) }; + static_cast(GetWStringLength(expectedStr0) * sizeof(wchar_t)), + static_cast(GetWStringLength(expectedStr1) * sizeof(wchar_t)), + static_cast(GetWStringLength(expectedStr2) * sizeof(wchar_t)), + static_cast(GetWStringLength(expectedStr3) * sizeof(wchar_t)) }; GetWStringOutputParam( expectedParamValues, @@ -880,15 +878,12 @@ namespace ExtensionApiTest { EXPECT_NE(paramValue, nullptr); - // strLen_or_Ind is in bytes, divide by sizeof(wchar_t) to get character count + // Compare raw UTF-16 bytes directly (avoids std::wstring which is + // ABI-incompatible with -fshort-wchar on Linux). // - SQLINTEGER charCount = strLen_or_Ind / sizeof(wchar_t); - wstring paramValueString(static_cast(paramValue), charCount); - - SQLINTEGER expectedCharCount = expectedStrLenOrInd[paramNumber] / sizeof(wchar_t); - wstring expectedParamValueString(expectedParamValues[paramNumber], expectedCharCount); - - EXPECT_EQ(paramValueString, expectedParamValueString); + EXPECT_EQ(memcmp(paramValue, + expectedParamValues[paramNumber], + expectedStrLenOrInd[paramNumber]), 0); } else { From db70570f73dfed3b65703552f67e917ab86fc33b Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 7 Apr 2026 01:13:51 -0700 Subject: [PATCH 16/45] CSharpExtension: self-contained publish for Linux Bundle the entire .NET runtime (hostfxr, coreclr, shared framework) in the extension tar.gz so it works in containers without a system .NET installation. Changes: - build-dotnet-core-CSharp-extension.sh: dotnet build -> dotnet publish --self-contained -r linux-x64 - restore-packages.sh: add -r linux-x64 to dotnet restore for runtime pack download during network-enabled phase - DotnetEnvironment.cpp: pass assembly_path to get_hostfxr_path() for self-contained hostfxr discovery; default dotnet_root to extension directory when DOTNET_ROOT env var is not set Fixes: Extension Init() failed with -8223344 (hostfxr not found) --- .../build-dotnet-core-CSharp-extension.sh | 14 ++++---- .../build/linux/restore-packages.sh | 6 ++-- .../src/native/DotnetEnvironment.cpp | 33 +++++++++++++++---- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index f328c724..e59d0a52 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -77,16 +77,18 @@ for BUILD_CONFIGURATION in "$@"; do popd > /dev/null - # Build managed code - echo "[Info] Building Microsoft.SqlServer.CSharpExtension dll..." + # Publish managed code as self-contained for linux-x64. + # This bundles the entire .NET runtime (hostfxr, coreclr, framework DLLs) + # into the output so the extension works without a system .NET installation. + echo "[Info] Publishing Microsoft.SqlServer.CSharpExtension (self-contained, linux-x64)..." DOTNET_MANAGED_SRC="$DOTNET_EXTENSION_HOME/src/managed" - dotnet build \ + dotnet publish \ "$DOTNET_MANAGED_SRC/Microsoft.SqlServer.CSharpExtension.csproj" \ - -m \ -c "$BUILD_CONFIGURATION" \ + -r linux-x64 \ + --self-contained \ -o "$BUILD_OUTPUT" \ - --no-restore \ - --no-dependencies + --no-restore echo "Success: Built dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." done diff --git a/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh b/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh index 4d104afc..f83890e9 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh @@ -63,10 +63,10 @@ MANAGED_PROJ="${DOTNET_EXTENSION_HOME}/src/managed/Microsoft.SqlServer.CSharpExt MANAGED_TEST_PROJ="${DOTNET_EXTENSION_HOME}/test/src/managed/Microsoft.SqlServer.CSharpExtensionTest.csproj" PARENT_NUGET_CONFIG="${ENL_ROOT}/../NuGet.Config" -echo "Info: Restoring NuGet packages for Microsoft.SqlServer.CSharpExtension..." +echo "Info: Restoring NuGet packages for Microsoft.SqlServer.CSharpExtension (linux-x64 self-contained)..." if [ -f "$PARENT_NUGET_CONFIG" ]; then echo "Info: Using NuGet.Config from parent repo: $PARENT_NUGET_CONFIG" - dotnet restore "$MANAGED_PROJ" --configfile "$PARENT_NUGET_CONFIG" + dotnet restore "$MANAGED_PROJ" --configfile "$PARENT_NUGET_CONFIG" -r linux-x64 check_exit_code "Success: NuGet packages restored (extension)" "Error: Failed to restore NuGet packages (extension)" echo "Info: Restoring NuGet packages for Microsoft.SqlServer.CSharpExtensionTest..." @@ -74,7 +74,7 @@ if [ -f "$PARENT_NUGET_CONFIG" ]; then check_exit_code "Success: NuGet packages restored (test)" "Error: Failed to restore NuGet packages (test)" else echo "Info: Parent NuGet.Config not found, using default" - dotnet restore "$MANAGED_PROJ" + dotnet restore "$MANAGED_PROJ" -r linux-x64 check_exit_code "Success: NuGet packages restored (extension)" "Error: Failed to restore NuGet packages (extension)" dotnet restore "$MANAGED_TEST_PROJ" diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index e7bf1fa7..cca56a04 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -197,14 +197,30 @@ bool DotnetEnvironment::load_hostfxr() string_t hostfxr_location = m_root_path + STR("\\hostfxr.dll"); #else // On Linux, use nethost to dynamically locate hostfxr. - // This honors DOTNET_ROOT and install_location files, which is more - // portable across distributions (e.g. RHEL vs Debian). + // First try self-contained discovery (assembly_path hint tells nethost + // to look for libhostfxr.so next to the managed assembly), then fall + // back to system-wide search via DOTNET_ROOT / install_location files. char buffer[HOSTFXR_PATH_BUFFER_SIZE]; size_t buffer_size = sizeof(buffer); - if (get_hostfxr_path(buffer, &buffer_size, nullptr) != 0) + + string_t assembly_path = m_root_path + STR("/Microsoft.SqlServer.CSharpExtension.dll"); + + get_hostfxr_parameters params; + params.size = sizeof(params); + params.assembly_path = assembly_path.c_str(); + params.dotnet_root = nullptr; + + int rc = get_hostfxr_path(buffer, &buffer_size, ¶ms); + if (rc != 0) { - LOG_ERROR("Failed to locate hostfxr via nethost"); - return false; + // Fall back to system-wide search (no assembly_path hint) + buffer_size = sizeof(buffer); + rc = get_hostfxr_path(buffer, &buffer_size, nullptr); + if (rc != 0) + { + LOG_ERROR("Failed to locate hostfxr via nethost"); + return false; + } } string_t hostfxr_location(buffer); #endif @@ -275,12 +291,17 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ } } #else - // On Linux, read DOTNET_ROOT from the environment (char-based) + // On Linux, use DOTNET_ROOT if set; otherwise default to the extension + // root path so hostfxr can find the self-contained shared framework. const char *dotnet_root_env = getenv("DOTNET_ROOT"); if (dotnet_root_env != nullptr) { params.dotnet_root = dotnet_root_env; } + else + { + params.dotnet_root = m_root_path.c_str(); + } #endif int rc = m_init_fptr(config_path, ¶ms, &cxt); From e5b1ea5149eec412deb0dd7641f45ad56cd2a863 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 7 Apr 2026 22:55:40 -0700 Subject: [PATCH 17/45] CSharpExtension: load bundled libhostfxr.so directly on Linux Load libhostfxr.so from the extension directory (self-contained deployment) rather than using get_hostfxr_path(), which may find an incompatible older hostfxr (e.g. .NET Core 3.x shipped with SQL Server). This mirrors the Windows approach of loading hostfxr.dll from the extension root. Falls back to nethost discovery for framework-dependent deployments where the bundled libhostfxr.so is not present. Fixes: hostfxr_initialize_for_runtime_config failed with 0x80008093 (Initialization for self-contained components is not supported) --- .../src/native/DotnetEnvironment.cpp | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index cca56a04..a7cca89e 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -15,6 +15,7 @@ #include "Windows.h" #else #include +#include #include #endif @@ -196,33 +197,25 @@ bool DotnetEnvironment::load_hostfxr() #ifdef _WIN32 string_t hostfxr_location = m_root_path + STR("\\hostfxr.dll"); #else - // On Linux, use nethost to dynamically locate hostfxr. - // First try self-contained discovery (assembly_path hint tells nethost - // to look for libhostfxr.so next to the managed assembly), then fall - // back to system-wide search via DOTNET_ROOT / install_location files. - char buffer[HOSTFXR_PATH_BUFFER_SIZE]; - size_t buffer_size = sizeof(buffer); + // For self-contained deployment, load the bundled libhostfxr.so directly + // from the extension directory. This is analogous to what Windows does + // with hostfxr.dll and avoids picking up an incompatible older hostfxr + // (e.g. .NET Core 3.x shipped with SQL Server). + string_t hostfxr_location = m_root_path + STR("/libhostfxr.so"); - string_t assembly_path = m_root_path + STR("/Microsoft.SqlServer.CSharpExtension.dll"); - - get_hostfxr_parameters params; - params.size = sizeof(params); - params.assembly_path = assembly_path.c_str(); - params.dotnet_root = nullptr; - - int rc = get_hostfxr_path(buffer, &buffer_size, ¶ms); - if (rc != 0) + // If the bundled hostfxr doesn't exist (framework-dependent deployment), + // fall back to nethost discovery. + if (access(hostfxr_location.c_str(), F_OK) != 0) { - // Fall back to system-wide search (no assembly_path hint) - buffer_size = sizeof(buffer); - rc = get_hostfxr_path(buffer, &buffer_size, nullptr); - if (rc != 0) + char buffer[HOSTFXR_PATH_BUFFER_SIZE]; + size_t buffer_size = sizeof(buffer); + if (get_hostfxr_path(buffer, &buffer_size, nullptr) != 0) { LOG_ERROR("Failed to locate hostfxr via nethost"); return false; } + hostfxr_location = string_t(buffer); } - string_t hostfxr_location(buffer); #endif void *lib = load_library(hostfxr_location.c_str()); if (lib == nullptr) From 429d7845073daf5a9f58b8f20253c7abf501be2a Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 8 Apr 2026 15:11:38 -0700 Subject: [PATCH 18/45] Add diagnostic logging to trace hostfxr loading path Adds [DIAG] cout messages to load_hostfxr() and get_dotnet() to trace: - m_root_path value - Whether bundled libhostfxr.so is found via access() - Which hostfxr path is actually loaded (bundled vs nethost fallback) - config_path and dotnet_root values passed to hostfxr - hostfxr_initialize_for_runtime_config return code --- .../src/native/DotnetEnvironment.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index a7cca89e..8b06450a 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -201,12 +201,15 @@ bool DotnetEnvironment::load_hostfxr() // from the extension directory. This is analogous to what Windows does // with hostfxr.dll and avoids picking up an incompatible older hostfxr // (e.g. .NET Core 3.x shipped with SQL Server). + cout << "[DIAG] m_root_path = " << m_root_path << endl; string_t hostfxr_location = m_root_path + STR("/libhostfxr.so"); + cout << "[DIAG] Checking bundled hostfxr at: " << hostfxr_location << endl; // If the bundled hostfxr doesn't exist (framework-dependent deployment), // fall back to nethost discovery. if (access(hostfxr_location.c_str(), F_OK) != 0) { + cout << "[DIAG] Bundled libhostfxr.so NOT found (access() failed), falling back to nethost" << endl; char buffer[HOSTFXR_PATH_BUFFER_SIZE]; size_t buffer_size = sizeof(buffer); if (get_hostfxr_path(buffer, &buffer_size, nullptr) != 0) @@ -215,8 +218,14 @@ bool DotnetEnvironment::load_hostfxr() return false; } hostfxr_location = string_t(buffer); + cout << "[DIAG] nethost resolved hostfxr to: " << hostfxr_location << endl; + } + else + { + cout << "[DIAG] Bundled libhostfxr.so FOUND, using it directly" << endl; } #endif + cout << "[DIAG] Loading hostfxr from: " << hostfxr_location << endl; void *lib = load_library(hostfxr_location.c_str()); if (lib == nullptr) { @@ -264,6 +273,7 @@ load_assembly_and_get_function_pointer_fn DotnetEnvironment::get_dotnet_load_ass // hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ LOG("DotnetEnvironment::get_dotnet"); + cout << "[DIAG] config_path = " << config_path << endl; hostfxr_handle cxt = nullptr; hostfxr_initialize_parameters params = { 0 }; @@ -295,9 +305,12 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ { params.dotnet_root = m_root_path.c_str(); } + cout << "[DIAG] dotnet_root = " << (params.dotnet_root ? params.dotnet_root : "(null)") << endl; #endif + cout << "[DIAG] Calling hostfxr_initialize_for_runtime_config..." << endl; int rc = m_init_fptr(config_path, ¶ms, &cxt); + cout << "[DIAG] hostfxr_initialize_for_runtime_config returned: " << to_hex_string(rc) << endl; if (rc != 0 || cxt == nullptr) { LOG_ERROR("Init failed: " + to_hex_string(rc)); From f90ad2abb96bc3f68d6564b8c8a9ebc04147e30f Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 8 Apr 2026 15:32:47 -0700 Subject: [PATCH 19/45] Fix: Use hostfxr_initialize_for_dotnet_command_line for self-contained Linux deployment hostfxr_initialize_for_runtime_config rejects self-contained runtimeconfig.json files (with 'includedFrameworks') returning error 0x80008093 (HostIncompatibleConfig). This is by design in .NET - that API only supports framework-dependent components. For self-contained deployments on Linux, use hostfxr_initialize_for_dotnet_command_line instead, which supports both framework-dependent and self-contained apps (.NET 5+). The hdt_load_assembly_and_get_function_pointer delegate works from command-line initialized contexts since .NET 5. Validated locally in WSL with bundled .NET 8.0.25 hostfxr - returns 0x00000000. --- .../include/DotnetEnvironment.h | 2 + .../src/native/DotnetEnvironment.cpp | 53 +++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index a32c41e8..a87f8643 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -87,10 +87,12 @@ class DotnetEnvironment private: hostfxr_initialize_for_runtime_config_fn m_init_fptr; + hostfxr_initialize_for_dotnet_command_line_fn m_init_cmdline_fptr; hostfxr_get_runtime_delegate_fn m_get_delegate_fptr; hostfxr_close_fn m_close_fptr; load_assembly_and_get_function_pointer_fn m_load_assembly_and_get_function_pointer; string_t m_root_path; + bool m_is_self_contained; // Convert a std::string to the platform string_t type. // On Windows this is UTF-8 -> UTF-16; on Linux it is a no-op. diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 8b06450a..9b2e86bc 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -45,7 +45,13 @@ DotnetEnvironment::DotnetEnvironment( std::string language_params, std::string language_path, std::string public_library_path, - std::string private_library_path) : m_root_path(convert_string(language_path)) + std::string private_library_path) : m_root_path(convert_string(language_path)), + m_init_fptr(nullptr), + m_init_cmdline_fptr(nullptr), + m_get_delegate_fptr(nullptr), + m_close_fptr(nullptr), + m_load_assembly_and_get_function_pointer(nullptr), + m_is_self_contained(false) { } @@ -210,6 +216,7 @@ bool DotnetEnvironment::load_hostfxr() if (access(hostfxr_location.c_str(), F_OK) != 0) { cout << "[DIAG] Bundled libhostfxr.so NOT found (access() failed), falling back to nethost" << endl; + m_is_self_contained = false; char buffer[HOSTFXR_PATH_BUFFER_SIZE]; size_t buffer_size = sizeof(buffer); if (get_hostfxr_path(buffer, &buffer_size, nullptr) != 0) @@ -222,7 +229,8 @@ bool DotnetEnvironment::load_hostfxr() } else { - cout << "[DIAG] Bundled libhostfxr.so FOUND, using it directly" << endl; + cout << "[DIAG] Bundled libhostfxr.so FOUND, using self-contained mode" << endl; + m_is_self_contained = true; } #endif cout << "[DIAG] Loading hostfxr from: " << hostfxr_location << endl; @@ -236,6 +244,19 @@ bool DotnetEnvironment::load_hostfxr() m_get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate"); m_close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close"); +#ifndef _WIN32 + // For self-contained deployments, hostfxr_initialize_for_runtime_config + // rejects runtimeconfig.json files with "includedFrameworks" (error 0x80008093). + // Instead, use hostfxr_initialize_for_dotnet_command_line which supports + // both framework-dependent and self-contained applications (.NET 5+). + if (m_is_self_contained) + { + m_init_cmdline_fptr = (hostfxr_initialize_for_dotnet_command_line_fn)get_export(lib, "hostfxr_initialize_for_dotnet_command_line"); + cout << "[DIAG] Loaded hostfxr_initialize_for_dotnet_command_line: " << (m_init_cmdline_fptr ? "OK" : "FAIL") << endl; + return (m_init_cmdline_fptr && m_get_delegate_fptr && m_close_fptr); + } +#endif + return (m_init_fptr && m_get_delegate_fptr && m_close_fptr); } @@ -308,9 +329,31 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ cout << "[DIAG] dotnet_root = " << (params.dotnet_root ? params.dotnet_root : "(null)") << endl; #endif - cout << "[DIAG] Calling hostfxr_initialize_for_runtime_config..." << endl; - int rc = m_init_fptr(config_path, ¶ms, &cxt); - cout << "[DIAG] hostfxr_initialize_for_runtime_config returned: " << to_hex_string(rc) << endl; + int rc = 0; + +#ifndef _WIN32 + if (m_is_self_contained && m_init_cmdline_fptr) + { + // For self-contained deployment, use hostfxr_initialize_for_dotnet_command_line. + // hostfxr_initialize_for_runtime_config rejects self-contained runtimeconfig.json + // files that contain "includedFrameworks" with error 0x80008093. + // hostfxr_initialize_for_dotnet_command_line supports both framework-dependent + // and self-contained applications (.NET 5+). + const string_t app_path = m_root_path + STR("/Microsoft.SqlServer.CSharpExtension.dll"); + const char_t *argv[] = { app_path.c_str() }; + cout << "[DIAG] Using hostfxr_initialize_for_dotnet_command_line (self-contained mode)" << endl; + cout << "[DIAG] argv[0] = " << argv[0] << endl; + rc = m_init_cmdline_fptr(1, argv, ¶ms, &cxt); + cout << "[DIAG] hostfxr_initialize_for_dotnet_command_line returned: " << to_hex_string(rc) << endl; + } + else +#endif + { + cout << "[DIAG] Using hostfxr_initialize_for_runtime_config (framework-dependent mode)" << endl; + rc = m_init_fptr(config_path, ¶ms, &cxt); + cout << "[DIAG] hostfxr_initialize_for_runtime_config returned: " << to_hex_string(rc) << endl; + } + if (rc != 0 || cxt == nullptr) { LOG_ERROR("Init failed: " + to_hex_string(rc)); From 007393b414c4e348ddb5fd09361e95b40315ced1 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 10 Apr 2026 13:59:37 -0700 Subject: [PATCH 20/45] Fix: Use hdt_get_function_pointer (Default ALC) for Linux self-contained .NET hosting - Use hostfxr_initialize_for_dotnet_command_line instead of hostfxr_initialize_for_runtime_config for self-contained deployments - Use hdt_get_function_pointer (type 6, Default ALC) instead of hdt_load_assembly_and_get_function_pointer (type 5, Isolated ALC) to resolve AssemblyLoadContext type cast failures - Add get_function_pointer_fn typedef and hdt_get_function_pointer enum - Dual-path call_managed_method: tries Default ALC first, falls back to Isolated ALC for backward compatibility --- .../include/DotnetEnvironment.h | 43 +++++++++++--- .../include/coreclr_delegates.h | 11 ++++ .../dotnet-core-CSharp/include/hostfxr.h | 3 +- .../src/managed/utils/DllUtils.cs | 2 + .../src/native/DotnetEnvironment.cpp | 56 +++++++++++-------- 5 files changed, 83 insertions(+), 32 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index a87f8643..df36bf34 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -70,13 +70,36 @@ class DotnetEnvironment const string_t ManagedExtensionType = ManagedExtensionName + STR(".CSharpExtension, ") + ManagedExtensionName; const string_t ManagedExtensionMethod = convert_string(method_name); const string_t DelegateTypeName = ManagedExtensionName + STR(".CSharpExtension+") + ManagedExtensionMethod + STR("Delegate, ") + ManagedExtensionName; - int rc = m_load_assembly_and_get_function_pointer( - ManagedExtensionPath.c_str(), - ManagedExtensionType.c_str(), - ManagedExtensionMethod.c_str(), - DelegateTypeName.c_str(), - nullptr, - (void**)&managed_func); + + int rc = -1; + if (m_get_function_pointer != nullptr) + { + // Preferred path: hdt_get_function_pointer loads into Default ALC. + // This ensures the managed extension and user DLLs loaded via Assembly.LoadFrom + // share the same assembly identity, allowing type casts to succeed. + rc = m_get_function_pointer( + ManagedExtensionType.c_str(), + ManagedExtensionMethod.c_str(), + DelegateTypeName.c_str(), + nullptr, /* load_context: nullptr = Default ALC */ + nullptr, /* reserved */ + (void**)&managed_func); + } + + if ((rc != 0 || managed_func == nullptr) && m_load_assembly_and_get_function_pointer != nullptr) + { + // Fallback: hdt_load_assembly_and_get_function_pointer (IsolatedComponentLoadContext). + // Used on Windows and non-self-contained deployments. + managed_func = nullptr; + rc = m_load_assembly_and_get_function_pointer( + ManagedExtensionPath.c_str(), + ManagedExtensionType.c_str(), + ManagedExtensionMethod.c_str(), + DelegateTypeName.c_str(), + nullptr, + (void**)&managed_func); + } + if (rc != 0 || managed_func == nullptr) { return E_FAIL; @@ -91,6 +114,7 @@ class DotnetEnvironment hostfxr_get_runtime_delegate_fn m_get_delegate_fptr; hostfxr_close_fn m_close_fptr; load_assembly_and_get_function_pointer_fn m_load_assembly_and_get_function_pointer; + get_function_pointer_fn m_get_function_pointer; string_t m_root_path; bool m_is_self_contained; @@ -121,9 +145,10 @@ class DotnetEnvironment // bool load_hostfxr(); - // Get desired function pointer for scenario for the loaded .NET Core + // Get the function pointer delegate for the loaded .NET Core. + // Uses hdt_get_function_pointer to load into Default ALC. // - load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(hostfxr_handle cxt); + get_function_pointer_fn get_dotnet_load_assembly(hostfxr_handle cxt); // Load and initialize .NET Core // diff --git a/language-extensions/dotnet-core-CSharp/include/coreclr_delegates.h b/language-extensions/dotnet-core-CSharp/include/coreclr_delegates.h index 9afa2d83..1fea5a1b 100644 --- a/language-extensions/dotnet-core-CSharp/include/coreclr_delegates.h +++ b/language-extensions/dotnet-core-CSharp/include/coreclr_delegates.h @@ -27,6 +27,17 @@ typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_and_get_function_pointer_f void *reserved /* Extensibility parameter (currently unused and must be 0) */, /*out*/ void **delegate /* Pointer where to store the function pointer result */); +// Signature of delegate returned by coreclr_delegate_type::hdt_get_function_pointer +// Loads assembly into Default AssemblyLoadContext (not IsolatedComponentLoadContext). +// This ensures types from the host and user DLLs share the same assembly identity. +typedef int (CORECLR_DELEGATE_CALLTYPE *get_function_pointer_fn)( + const char_t *type_name /* Assembly qualified type name */, + const char_t *method_name /* Public static method name compatible with delegateType */, + const char_t *delegate_type_name /* Assembly qualified delegate type name or null */, + void *load_context /* Load context (nullptr = Default ALC) */, + void *reserved /* Extensibility parameter (currently unused and must be 0) */, + /*out*/ void **delegate /* Pointer where to store the function pointer result */); + // Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default) typedef int (CORECLR_DELEGATE_CALLTYPE *component_entry_point_fn)(void *arg, int32_t arg_size_in_bytes); diff --git a/language-extensions/dotnet-core-CSharp/include/hostfxr.h b/language-extensions/dotnet-core-CSharp/include/hostfxr.h index 6d822986..028a127a 100644 --- a/language-extensions/dotnet-core-CSharp/include/hostfxr.h +++ b/language-extensions/dotnet-core-CSharp/include/hostfxr.h @@ -26,7 +26,8 @@ enum hostfxr_delegate_type hdt_winrt_activation, hdt_com_register, hdt_com_unregister, - hdt_load_assembly_and_get_function_pointer + hdt_load_assembly_and_get_function_pointer, + hdt_get_function_pointer }; typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_fn)(const int argc, const char_t **argv); diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs index 378e7cd8..c31fe182 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs @@ -36,6 +36,7 @@ public static Type GetUserDll(string userClassName, List dllList) // AppDomain.CurrentDomain.AssemblyResolve occurs when the resolution of an assembly fails. // AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; + foreach(string dllPath in dllList) { // Catch unexpected exception while loading other dlls @@ -43,6 +44,7 @@ public static Type GetUserDll(string userClassName, List dllList) try { Assembly userDll = Assembly.LoadFrom(dllPath); + Type userExecutorClass = userDll.GetType(userClassName); if (userExecutorClass != null) { diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 9b2e86bc..82ac4e36 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -51,6 +51,7 @@ DotnetEnvironment::DotnetEnvironment( m_get_delegate_fptr(nullptr), m_close_fptr(nullptr), m_load_assembly_and_get_function_pointer(nullptr), + m_get_function_pointer(nullptr), m_is_self_contained(false) { } @@ -80,9 +81,13 @@ short DotnetEnvironment::Init() return E_FAIL; } - m_load_assembly_and_get_function_pointer = get_dotnet_load_assembly(cxt); + // get_dotnet_load_assembly tries hdt_get_function_pointer first (Default ALC), + // falls back to hdt_load_assembly_and_get_function_pointer (IsolatedComponentLoadContext). + // On success with hdt_get_function_pointer, it returns the fn ptr and m_load_assembly_and_get_function_pointer stays null. + // On fallback, it returns nullptr and sets m_load_assembly_and_get_function_pointer. + m_get_function_pointer = get_dotnet_load_assembly(cxt); - if (m_load_assembly_and_get_function_pointer == nullptr) + if (m_get_function_pointer == nullptr && m_load_assembly_and_get_function_pointer == nullptr) { return E_FAIL; } @@ -207,15 +212,12 @@ bool DotnetEnvironment::load_hostfxr() // from the extension directory. This is analogous to what Windows does // with hostfxr.dll and avoids picking up an incompatible older hostfxr // (e.g. .NET Core 3.x shipped with SQL Server). - cout << "[DIAG] m_root_path = " << m_root_path << endl; string_t hostfxr_location = m_root_path + STR("/libhostfxr.so"); - cout << "[DIAG] Checking bundled hostfxr at: " << hostfxr_location << endl; // If the bundled hostfxr doesn't exist (framework-dependent deployment), // fall back to nethost discovery. if (access(hostfxr_location.c_str(), F_OK) != 0) { - cout << "[DIAG] Bundled libhostfxr.so NOT found (access() failed), falling back to nethost" << endl; m_is_self_contained = false; char buffer[HOSTFXR_PATH_BUFFER_SIZE]; size_t buffer_size = sizeof(buffer); @@ -225,15 +227,12 @@ bool DotnetEnvironment::load_hostfxr() return false; } hostfxr_location = string_t(buffer); - cout << "[DIAG] nethost resolved hostfxr to: " << hostfxr_location << endl; } else { - cout << "[DIAG] Bundled libhostfxr.so FOUND, using self-contained mode" << endl; m_is_self_contained = true; } #endif - cout << "[DIAG] Loading hostfxr from: " << hostfxr_location << endl; void *lib = load_library(hostfxr_location.c_str()); if (lib == nullptr) { @@ -252,7 +251,6 @@ bool DotnetEnvironment::load_hostfxr() if (m_is_self_contained) { m_init_cmdline_fptr = (hostfxr_initialize_for_dotnet_command_line_fn)get_export(lib, "hostfxr_initialize_for_dotnet_command_line"); - cout << "[DIAG] Loaded hostfxr_initialize_for_dotnet_command_line: " << (m_init_cmdline_fptr ? "OK" : "FAIL") << endl; return (m_init_cmdline_fptr && m_get_delegate_fptr && m_close_fptr); } #endif @@ -264,16 +262,33 @@ bool DotnetEnvironment::load_hostfxr() // Name: DotnetEnvironment::get_dotnet_load_assembly // // Description: -// load assembly function pointer from the path. +// Get the function pointer delegate for the loaded .NET Core. +// Uses hdt_get_function_pointer to load into Default ALC, ensuring type identity +// compatibility with user DLLs loaded via Assembly.LoadFrom. +// Falls back to hdt_load_assembly_and_get_function_pointer for Windows/non-self-contained. // -load_assembly_and_get_function_pointer_fn DotnetEnvironment::get_dotnet_load_assembly(hostfxr_handle cxt) +get_function_pointer_fn DotnetEnvironment::get_dotnet_load_assembly(hostfxr_handle cxt) { LOG("DotnetEnvironment::get_dotnet_load_assembly"); - // Load .NET Core - void *load_assembly_and_get_function_pointer = nullptr; - // Get the load assembly function pointer + // Try hdt_get_function_pointer first (loads into Default ALC). + // This is required for self-contained Linux deployments to ensure + // the managed extension and user DLLs share the same assembly context. + void *get_fn_ptr = nullptr; int rc = m_get_delegate_fptr( + cxt, + hdt_get_function_pointer, + &get_fn_ptr); + if (rc == 0 && get_fn_ptr != nullptr) + { + m_close_fptr(cxt); + return (get_function_pointer_fn)get_fn_ptr; + } + + // Fallback to hdt_load_assembly_and_get_function_pointer (IsolatedComponentLoadContext). + // This path is used on Windows and non-self-contained deployments. + void *load_assembly_and_get_function_pointer = nullptr; + rc = m_get_delegate_fptr( cxt, hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer); @@ -282,8 +297,12 @@ load_assembly_and_get_function_pointer_fn DotnetEnvironment::get_dotnet_load_ass LOG_ERROR("Get delegate failed: " + to_hex_string(rc)); } + // Store the load_assembly_and_get_function_pointer for the fallback path + m_load_assembly_and_get_function_pointer = (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; + m_close_fptr(cxt); - return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; + // Return nullptr to signal that the caller should use m_load_assembly_and_get_function_pointer + return nullptr; } //-------------------------------------------------------------------------------------------------- @@ -294,7 +313,6 @@ load_assembly_and_get_function_pointer_fn DotnetEnvironment::get_dotnet_load_ass // hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ LOG("DotnetEnvironment::get_dotnet"); - cout << "[DIAG] config_path = " << config_path << endl; hostfxr_handle cxt = nullptr; hostfxr_initialize_parameters params = { 0 }; @@ -326,7 +344,6 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ { params.dotnet_root = m_root_path.c_str(); } - cout << "[DIAG] dotnet_root = " << (params.dotnet_root ? params.dotnet_root : "(null)") << endl; #endif int rc = 0; @@ -341,17 +358,12 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ // and self-contained applications (.NET 5+). const string_t app_path = m_root_path + STR("/Microsoft.SqlServer.CSharpExtension.dll"); const char_t *argv[] = { app_path.c_str() }; - cout << "[DIAG] Using hostfxr_initialize_for_dotnet_command_line (self-contained mode)" << endl; - cout << "[DIAG] argv[0] = " << argv[0] << endl; rc = m_init_cmdline_fptr(1, argv, ¶ms, &cxt); - cout << "[DIAG] hostfxr_initialize_for_dotnet_command_line returned: " << to_hex_string(rc) << endl; } else #endif { - cout << "[DIAG] Using hostfxr_initialize_for_runtime_config (framework-dependent mode)" << endl; rc = m_init_fptr(config_path, ¶ms, &cxt); - cout << "[DIAG] hostfxr_initialize_for_runtime_config returned: " << to_hex_string(rc) << endl; } if (rc != 0 || cxt == nullptr) From 047ecce1b8e85a815f173f93ce8f2ed917c8738f Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Mon, 11 May 2026 12:14:53 -0700 Subject: [PATCH 21/45] Fix: use hostfxr_initialize_for_runtime_config with framework-dependent runtimeconfig hostfxr_initialize_for_dotnet_command_line returns 0x80008093 (InvalidConfigFile) because .NET does not support self-contained component initialization. Instead: - Post-process runtimeconfig.json: replace includedFrameworks with framework - Create shared/Microsoft.NETCore.App// symlink to extension root - Always use hostfxr_initialize_for_runtime_config (works for both modes) - Include shared/ directory in tar.gz - Fix double 0x prefix in to_hex_string --- .../build-dotnet-core-CSharp-extension.sh | 32 +++++++++++++ ...create-dotnet-core-CSharp-extension-tar.sh | 7 +++ .../include/DotnetEnvironment.h | 1 - .../src/native/DotnetEnvironment.cpp | 46 ++++--------------- 4 files changed, 49 insertions(+), 37 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index e59d0a52..003bd4bc 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -90,6 +90,38 @@ for BUILD_CONFIGURATION in "$@"; do -o "$BUILD_OUTPUT" \ --no-restore + # Post-process: transform the self-contained runtimeconfig.json for component hosting. + # hostfxr rejects self-contained component initialization (error 0x80008093) because + # runtimeconfig.json contains "includedFrameworks". We convert it to use "framework" + # (framework-dependent format) and create a shared/ symlink so hostfxr can resolve + # the bundled runtime via hostfxr_initialize_for_runtime_config. + RUNTIMECONFIG="$BUILD_OUTPUT/Microsoft.SqlServer.CSharpExtension.runtimeconfig.json" + if grep -q '"includedFrameworks"' "$RUNTIMECONFIG" 2>/dev/null; then + FRAMEWORK_VERSION=$(python3 -c " +import json, sys +with open(sys.argv[1]) as f: + cfg = json.load(f) +ro = cfg.get('runtimeOptions', {}) +included = ro.get('includedFrameworks', []) +if not included: + sys.exit(1) +ro['framework'] = included[0] +del ro['includedFrameworks'] +with open(sys.argv[1], 'w') as f: + json.dump(cfg, f, indent=2) +print(included[0]['version']) +" "$RUNTIMECONFIG") + + if [ -n "$FRAMEWORK_VERSION" ]; then + # Create framework directory structure with symlink. + # hostfxr resolves frameworks at /shared/// + # The symlink points back to root where all self-contained files reside. + mkdir -p "$BUILD_OUTPUT/shared/Microsoft.NETCore.App" + ln -sfn "../../../" "$BUILD_OUTPUT/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" + echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION)" + fi + fi + echo "Success: Built dotnet-core-CSharp-extension for $BUILD_CONFIGURATION configuration." done diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh index 4c021ab3..473421e2 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh @@ -51,6 +51,13 @@ for BUILD_CONFIGURATION in "$@"; do done fi + # Include the shared/ directory (framework symlink for component hosting). + # The build script creates shared/Microsoft.NETCore.App// as a symlink + # back to root so hostfxr can resolve the bundled .NET runtime. + if [ -d "$BUILD_OUTPUT/shared" ]; then + FILES_TO_COMPRESS+=("shared") + fi + # Package the binaries pushd "$BUILD_OUTPUT" > /dev/null tar -czvf "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension.tar.gz" "${FILES_TO_COMPRESS[@]}" diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index df36bf34..1e577bf7 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -110,7 +110,6 @@ class DotnetEnvironment private: hostfxr_initialize_for_runtime_config_fn m_init_fptr; - hostfxr_initialize_for_dotnet_command_line_fn m_init_cmdline_fptr; hostfxr_get_runtime_delegate_fn m_get_delegate_fptr; hostfxr_close_fn m_close_fptr; load_assembly_and_get_function_pointer_fn m_load_assembly_and_get_function_pointer; diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 82ac4e36..5a3be82d 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -47,7 +47,6 @@ DotnetEnvironment::DotnetEnvironment( std::string public_library_path, std::string private_library_path) : m_root_path(convert_string(language_path)), m_init_fptr(nullptr), - m_init_cmdline_fptr(nullptr), m_get_delegate_fptr(nullptr), m_close_fptr(nullptr), m_load_assembly_and_get_function_pointer(nullptr), @@ -138,7 +137,7 @@ string DotnetEnvironment::to_hex_string(int value) { LOG("DotnetEnvironment::to_hex_string"); std::stringstream s; - s << "0x" << std::hex << std::showbase << value; + s << "0x" << std::hex << value; return s.str(); } @@ -243,18 +242,6 @@ bool DotnetEnvironment::load_hostfxr() m_get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate"); m_close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close"); -#ifndef _WIN32 - // For self-contained deployments, hostfxr_initialize_for_runtime_config - // rejects runtimeconfig.json files with "includedFrameworks" (error 0x80008093). - // Instead, use hostfxr_initialize_for_dotnet_command_line which supports - // both framework-dependent and self-contained applications (.NET 5+). - if (m_is_self_contained) - { - m_init_cmdline_fptr = (hostfxr_initialize_for_dotnet_command_line_fn)get_export(lib, "hostfxr_initialize_for_dotnet_command_line"); - return (m_init_cmdline_fptr && m_get_delegate_fptr && m_close_fptr); - } -#endif - return (m_init_fptr && m_get_delegate_fptr && m_close_fptr); } @@ -333,38 +320,25 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ } } #else - // On Linux, use DOTNET_ROOT if set; otherwise default to the extension - // root path so hostfxr can find the self-contained shared framework. + // On Linux, for self-contained (bundled) deployments, set dotnet_root to + // the extension directory so hostfxr finds the runtime via the shared/ symlink. + // The build script transforms the self-contained runtimeconfig.json to look + // framework-dependent ("framework" instead of "includedFrameworks") and creates + // a shared/Microsoft.NETCore.App// symlink pointing back to root. + // For framework-dependent deployments, leave dotnet_root as nullptr + // so hostfxr uses its default discovery mechanism. const char *dotnet_root_env = getenv("DOTNET_ROOT"); if (dotnet_root_env != nullptr) { params.dotnet_root = dotnet_root_env; } - else + else if (m_is_self_contained) { params.dotnet_root = m_root_path.c_str(); } #endif - int rc = 0; - -#ifndef _WIN32 - if (m_is_self_contained && m_init_cmdline_fptr) - { - // For self-contained deployment, use hostfxr_initialize_for_dotnet_command_line. - // hostfxr_initialize_for_runtime_config rejects self-contained runtimeconfig.json - // files that contain "includedFrameworks" with error 0x80008093. - // hostfxr_initialize_for_dotnet_command_line supports both framework-dependent - // and self-contained applications (.NET 5+). - const string_t app_path = m_root_path + STR("/Microsoft.SqlServer.CSharpExtension.dll"); - const char_t *argv[] = { app_path.c_str() }; - rc = m_init_cmdline_fptr(1, argv, ¶ms, &cxt); - } - else -#endif - { - rc = m_init_fptr(config_path, ¶ms, &cxt); - } + int rc = m_init_fptr(config_path, ¶ms, &cxt); if (rc != 0 || cxt == nullptr) { From a756bad54d9a5de958b58cd6e0b33b5f23e0de63 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 12 May 2026 09:33:07 -0700 Subject: [PATCH 22/45] Fix: Replace shared/ symlink with hard links for SQL Server tar extraction SQL Server's tar extraction does not preserve symbolic links when extracting CREATE EXTERNAL LANGUAGE payloads. The shared/Microsoft.NETCore.App// symlink (pointing to ../../../) was silently dropped, causing hostfxr to fail with 0x80008096 (FrameworkMissingFailure) - 'No frameworks were found'. Replace the directory symlink with hard links for each .dll and .so file. Hard links are stored efficiently in tar (as link entries with zero additional tar size) and are reliably extracted by all tar implementations - either as real hard links or as file copies. --- .../build-dotnet-core-CSharp-extension.sh | 19 ++++++++++++++----- ...create-dotnet-core-CSharp-extension-tar.sh | 7 ++++--- .../src/native/DotnetEnvironment.cpp | 6 ++++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index 003bd4bc..510fa177 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -113,12 +113,21 @@ print(included[0]['version']) " "$RUNTIMECONFIG") if [ -n "$FRAMEWORK_VERSION" ]; then - # Create framework directory structure with symlink. + # Create framework directory structure with hard links. # hostfxr resolves frameworks at /shared/// - # The symlink points back to root where all self-contained files reside. - mkdir -p "$BUILD_OUTPUT/shared/Microsoft.NETCore.App" - ln -sfn "../../../" "$BUILD_OUTPUT/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" - echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION)" + # which must contain the runtime DLLs and native libraries. + # + # We use hard links (not symlinks) because SQL Server's tar extraction + # does not preserve symbolic links. Hard links are stored efficiently + # in tar (as link entries with zero additional size) and are reliably + # extracted by all tar implementations -- either as hard links or as + # file copies. + FRAMEWORK_DIR="$BUILD_OUTPUT/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" + mkdir -p "$FRAMEWORK_DIR" + for f in "$BUILD_OUTPUT"/*.dll "$BUILD_OUTPUT"/*.so; do + [ -e "$f" ] && ln "$f" "$FRAMEWORK_DIR/$(basename "$f")" + done + echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION, $(ls "$FRAMEWORK_DIR" | wc -l) hard-linked files)" fi fi diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh index 473421e2..a6cbfd77 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh @@ -51,9 +51,10 @@ for BUILD_CONFIGURATION in "$@"; do done fi - # Include the shared/ directory (framework symlink for component hosting). - # The build script creates shared/Microsoft.NETCore.App// as a symlink - # back to root so hostfxr can resolve the bundled .NET runtime. + # Include the shared/ directory (framework hard links for component hosting). + # The build script creates shared/Microsoft.NETCore.App// with hard links + # to the root DLLs/SOs so hostfxr can resolve the bundled .NET runtime. + # Hard links are used because SQL Server's tar extraction does not preserve symlinks. if [ -d "$BUILD_OUTPUT/shared" ]; then FILES_TO_COMPRESS+=("shared") fi diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 5a3be82d..1cbb1d2d 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -321,10 +321,12 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ } #else // On Linux, for self-contained (bundled) deployments, set dotnet_root to - // the extension directory so hostfxr finds the runtime via the shared/ symlink. + // the extension directory so hostfxr finds the runtime via the shared/ directory. // The build script transforms the self-contained runtimeconfig.json to look // framework-dependent ("framework" instead of "includedFrameworks") and creates - // a shared/Microsoft.NETCore.App// symlink pointing back to root. + // a shared/Microsoft.NETCore.App// directory with hard links to the + // root DLLs/SOs. Hard links are used because SQL Server's tar extraction does + // not preserve symbolic links when extracting CREATE EXTERNAL LANGUAGE payloads. // For framework-dependent deployments, leave dotnet_root as nullptr // so hostfxr uses its default discovery mechanism. const char *dotnet_root_env = getenv("DOTNET_ROOT"); From d2ef1b59fd4b12d5c23bfda76299419300b5971c Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 12 May 2026 16:07:03 -0700 Subject: [PATCH 23/45] Use file copies instead of hard links for framework dir SQL Server's CREATE EXTERNAL LANGUAGE tar extraction silently drops both symbolic links AND hard links. Use actual file copies (cp) to ensure framework DLLs/SOs are always present after extraction. Previous attempts: - Symlinks (ln -sfn): dropped by tar extraction -> 0x80008096 - Hard links (ln): also dropped by tar extraction -> 0x80008096 - File copies (cp): regular file entries, always extracted correctly --- .../linux/build-dotnet-core-CSharp-extension.sh | 15 +++++++-------- .../create-dotnet-core-CSharp-extension-tar.sh | 9 +++++---- .../src/native/DotnetEnvironment.cpp | 7 ++++--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index 510fa177..8762070d 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -113,21 +113,20 @@ print(included[0]['version']) " "$RUNTIMECONFIG") if [ -n "$FRAMEWORK_VERSION" ]; then - # Create framework directory structure with hard links. + # Create framework directory structure with file copies. # hostfxr resolves frameworks at /shared/// # which must contain the runtime DLLs and native libraries. # - # We use hard links (not symlinks) because SQL Server's tar extraction - # does not preserve symbolic links. Hard links are stored efficiently - # in tar (as link entries with zero additional size) and are reliably - # extracted by all tar implementations -- either as hard links or as - # file copies. + # We use file copies because SQL Server's internal tar extraction + # (used by CREATE EXTERNAL LANGUAGE) does not preserve symbolic links + # or hard links -- both are silently dropped during extraction. + # Copying the files ensures they are always present after extraction. FRAMEWORK_DIR="$BUILD_OUTPUT/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" mkdir -p "$FRAMEWORK_DIR" for f in "$BUILD_OUTPUT"/*.dll "$BUILD_OUTPUT"/*.so; do - [ -e "$f" ] && ln "$f" "$FRAMEWORK_DIR/$(basename "$f")" + [ -e "$f" ] && cp "$f" "$FRAMEWORK_DIR/$(basename "$f")" done - echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION, $(ls "$FRAMEWORK_DIR" | wc -l) hard-linked files)" + echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION, $(ls "$FRAMEWORK_DIR" | wc -l) copied files)" fi fi diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh index a6cbfd77..31785857 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh @@ -51,10 +51,11 @@ for BUILD_CONFIGURATION in "$@"; do done fi - # Include the shared/ directory (framework hard links for component hosting). - # The build script creates shared/Microsoft.NETCore.App// with hard links - # to the root DLLs/SOs so hostfxr can resolve the bundled .NET runtime. - # Hard links are used because SQL Server's tar extraction does not preserve symlinks. + # Include the shared/ directory (framework file copies for component hosting). + # The build script creates shared/Microsoft.NETCore.App// with copies + # of the root DLLs/SOs so hostfxr can resolve the bundled .NET runtime. + # File copies are used because SQL Server's tar extraction does not preserve + # symlinks or hard links. if [ -d "$BUILD_OUTPUT/shared" ]; then FILES_TO_COMPRESS+=("shared") fi diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 1cbb1d2d..5b1378e4 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -324,9 +324,10 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ // the extension directory so hostfxr finds the runtime via the shared/ directory. // The build script transforms the self-contained runtimeconfig.json to look // framework-dependent ("framework" instead of "includedFrameworks") and creates - // a shared/Microsoft.NETCore.App// directory with hard links to the - // root DLLs/SOs. Hard links are used because SQL Server's tar extraction does - // not preserve symbolic links when extracting CREATE EXTERNAL LANGUAGE payloads. + // a shared/Microsoft.NETCore.App// directory with copies of the + // root DLLs/SOs. File copies are used because SQL Server's tar extraction does + // not preserve symbolic links or hard links when extracting CREATE EXTERNAL + // LANGUAGE payloads. // For framework-dependent deployments, leave dotnet_root as nullptr // so hostfxr uses its default discovery mechanism. const char *dotnet_root_env = getenv("DOTNET_ROOT"); From 78f75eab1c08fc1c292208f7bbdc41e7e3425c14 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 12 May 2026 23:33:31 -0700 Subject: [PATCH 24/45] Add diagnostic logging to DotnetEnvironment for framework resolution debugging Log dotnet_root path, shared/ directory existence, framework version directories, runtimeconfig.json content, and relevant environment variables before calling hostfxr_initialize_for_runtime_config. This helps diagnose the 0x80008096 (FrameworkMissingFailure) error. --- .../src/native/DotnetEnvironment.cpp | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 5b1378e4..86e27f9b 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -27,6 +27,12 @@ #include #include +#ifndef _WIN32 +#include +#include +#include +#endif + using namespace std; using string_t = std::basic_string; @@ -341,6 +347,104 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ } #endif +#ifndef _WIN32 + // Diagnostic logging: verify dotnet_root and shared/ directory presence + // before calling hostfxr_initialize_for_runtime_config. + if (params.dotnet_root != nullptr) + { + LOG("dotnet_root: " + std::string(params.dotnet_root)); + + // Check shared/ directory + std::string shared_dir = std::string(params.dotnet_root) + "/shared"; + struct stat st; + if (stat(shared_dir.c_str(), &st) == 0) + { + LOG("shared/ exists (mode=" + std::to_string(st.st_mode) + " uid=" + std::to_string(st.st_uid) + " gid=" + std::to_string(st.st_gid) + ")"); + + std::string app_dir = shared_dir + "/Microsoft.NETCore.App"; + if (stat(app_dir.c_str(), &st) == 0) + { + LOG("Microsoft.NETCore.App/ exists (mode=" + std::to_string(st.st_mode) + ")"); + + // List version subdirectories + DIR *d = opendir(app_dir.c_str()); + if (d) + { + struct dirent *de; + while ((de = readdir(d)) != nullptr) + { + if (de->d_name[0] != '.') + LOG(" framework version: " + std::string(de->d_name)); + } + closedir(d); + } + } + else + { + LOG_ERROR("Microsoft.NETCore.App/ NOT found (errno=" + std::to_string(errno) + ")"); + } + } + else + { + LOG_ERROR("shared/ NOT found at " + shared_dir + " (errno=" + std::to_string(errno) + ")"); + + // List root directory contents for debugging + DIR *d = opendir(params.dotnet_root); + if (d) + { + struct dirent *de; + int count = 0; + while ((de = readdir(d)) != nullptr) + { + if (de->d_name[0] != '.') + { + if (count < 30) + LOG(" root entry: " + std::string(de->d_name) + " (type=" + std::to_string(de->d_type) + ")"); + count++; + } + } + LOG(" total entries: " + std::to_string(count)); + closedir(d); + } + else + { + LOG_ERROR("Cannot opendir dotnet_root (errno=" + std::to_string(errno) + ")"); + } + } + } + else + { + LOG("dotnet_root is nullptr"); + } + + // Log runtimeconfig path and check it exists + { + std::string cfg(config_path); + LOG("config_path: " + cfg); + struct stat cfgst; + if (stat(cfg.c_str(), &cfgst) != 0) + LOG_ERROR("config_path NOT found (errno=" + std::to_string(errno) + ")"); + + // Read and log first 500 chars of runtimeconfig for verification + std::ifstream rcf(cfg); + if (rcf.is_open()) + { + std::string content((std::istreambuf_iterator(rcf)), + std::istreambuf_iterator()); + if (content.size() > 500) content.resize(500); + LOG("runtimeconfig content: " + content); + } + } + + // Log DOTNET_ROOT and DOTNET_MULTILEVEL_LOOKUP env vars + { + const char *dr = getenv("DOTNET_ROOT"); + LOG("env DOTNET_ROOT=" + std::string(dr ? dr : "(null)")); + const char *ml = getenv("DOTNET_MULTILEVEL_LOOKUP"); + LOG("env DOTNET_MULTILEVEL_LOOKUP=" + std::string(ml ? ml : "(null)")); + } +#endif + int rc = m_init_fptr(config_path, ¶ms, &cxt); if (rc != 0 || cxt == nullptr) From 6ca7453248f71688554dec8849b6212411a5c83f Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 13 May 2026 03:08:43 -0700 Subject: [PATCH 25/45] Add COREHOST_TRACE and version dir file listing diagnostics --- .../src/native/DotnetEnvironment.cpp | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 86e27f9b..d9e05d14 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -366,7 +366,7 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ { LOG("Microsoft.NETCore.App/ exists (mode=" + std::to_string(st.st_mode) + ")"); - // List version subdirectories + // List version subdirectories and their contents DIR *d = opendir(app_dir.c_str()); if (d) { @@ -374,7 +374,35 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ while ((de = readdir(d)) != nullptr) { if (de->d_name[0] != '.') + { LOG(" framework version: " + std::string(de->d_name)); + + // List files inside this version directory + std::string ver_dir = app_dir + "/" + std::string(de->d_name); + DIR *vd = opendir(ver_dir.c_str()); + if (vd) + { + struct dirent *ve; + int fcount = 0; + bool has_deps = false, has_coreclr = false, has_dotversion = false; + while ((ve = readdir(vd)) != nullptr) + { + if (ve->d_name[0] != '.') + fcount++; + if (std::string(ve->d_name) == "Microsoft.NETCore.App.deps.json") + has_deps = true; + if (std::string(ve->d_name) == "libcoreclr.so") + has_coreclr = true; + if (std::string(ve->d_name) == ".version") + has_dotversion = true; + } + closedir(vd); + LOG(" file count: " + std::to_string(fcount) + + " deps.json=" + std::to_string(has_deps) + + " libcoreclr.so=" + std::to_string(has_coreclr) + + " .version=" + std::to_string(has_dotversion)); + } + } } closedir(d); } @@ -443,10 +471,44 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ const char *ml = getenv("DOTNET_MULTILEVEL_LOOKUP"); LOG("env DOTNET_MULTILEVEL_LOOKUP=" + std::string(ml ? ml : "(null)")); } + + // Enable COREHOST_TRACE for detailed hostfxr diagnostics + setenv("COREHOST_TRACE", "1", 1); + setenv("COREHOST_TRACEFILE", "/tmp/hostfxr_trace.log", 1); + setenv("COREHOST_TRACE_VERBOSITY", "4", 1); #endif int rc = m_init_fptr(config_path, ¶ms, &cxt); +#ifndef _WIN32 + // Read and log the COREHOST_TRACE output + { + std::ifstream trace("/tmp/hostfxr_trace.log"); + if (trace.is_open()) + { + std::string line; + int linecount = 0; + while (std::getline(trace, line)) + { + if (linecount < 200) + LOG("TRACE: " + line); + linecount++; + } + if (linecount >= 200) + LOG("TRACE: ... (truncated, total " + std::to_string(linecount) + " lines)"); + trace.close(); + } + else + { + LOG_ERROR("Could not read /tmp/hostfxr_trace.log"); + } + // Clean up trace env vars + unsetenv("COREHOST_TRACE"); + unsetenv("COREHOST_TRACEFILE"); + unsetenv("COREHOST_TRACE_VERBOSITY"); + } +#endif + if (rc != 0 || cxt == nullptr) { LOG_ERROR("Init failed: " + to_hex_string(rc)); From df5a770c653344265ba80d9d4d489bc8f51a7ecd Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 13 May 2026 07:24:08 -0700 Subject: [PATCH 26/45] Fix: Copy .deps.json and .version files to shared framework dir hostfxr requires Microsoft.NETCore.App.deps.json to recognize a framework version directory as valid. The build script was only copying *.dll and *.so, missing the .deps.json and .version files that hostfxr needs for framework resolution. This caused error 0x80008096 (FrameworkMissingFailure). --- .../build/linux/build-dotnet-core-CSharp-extension.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index 8762070d..e7578f98 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -123,10 +123,17 @@ print(included[0]['version']) # Copying the files ensures they are always present after extraction. FRAMEWORK_DIR="$BUILD_OUTPUT/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" mkdir -p "$FRAMEWORK_DIR" - for f in "$BUILD_OUTPUT"/*.dll "$BUILD_OUTPUT"/*.so; do + # Copy all runtime files from .NET shared framework. + # The source is the published output which contains the framework files. + # hostfxr requires Microsoft.NETCore.App.deps.json to recognize a valid framework. + for f in "$BUILD_OUTPUT"/*.dll "$BUILD_OUTPUT"/*.so "$BUILD_OUTPUT"/*.json; do [ -e "$f" ] && cp "$f" "$FRAMEWORK_DIR/$(basename "$f")" done - echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION, $(ls "$FRAMEWORK_DIR" | wc -l) copied files)" + # Also copy hidden files like .version that hostfxr may need + for f in "$BUILD_OUTPUT"/.[!.]* ; do + [ -e "$f" ] && cp "$f" "$FRAMEWORK_DIR/$(basename "$f")" + done + echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION, $(ls -a "$FRAMEWORK_DIR" | wc -l) copied files)" fi fi From 8fc51e0e93209a45787ef4d4f72496dcd80c46e6 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 13 May 2026 12:53:10 -0700 Subject: [PATCH 27/45] Fix: Copy Microsoft.NETCore.App.deps.json from SDK shared framework dir The .deps.json file only exists in the .NET SDK's shared/Microsoft.NETCore.App/ directory, NOT in the self-contained publish output. hostfxr requires this file to recognize a framework version as valid. Without it, hostfxr reports FrameworkMissingFailure (0x80008096) even when all DLLs/SOs are present. --- .../build-dotnet-core-CSharp-extension.sh | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index e7578f98..f6122004 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -123,17 +123,37 @@ print(included[0]['version']) # Copying the files ensures they are always present after extraction. FRAMEWORK_DIR="$BUILD_OUTPUT/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" mkdir -p "$FRAMEWORK_DIR" - # Copy all runtime files from .NET shared framework. - # The source is the published output which contains the framework files. - # hostfxr requires Microsoft.NETCore.App.deps.json to recognize a valid framework. - for f in "$BUILD_OUTPUT"/*.dll "$BUILD_OUTPUT"/*.so "$BUILD_OUTPUT"/*.json; do + # Copy DLLs and SOs from the self-contained publish output to the framework dir. + for f in "$BUILD_OUTPUT"/*.dll "$BUILD_OUTPUT"/*.so; do [ -e "$f" ] && cp "$f" "$FRAMEWORK_DIR/$(basename "$f")" done - # Also copy hidden files like .version that hostfxr may need - for f in "$BUILD_OUTPUT"/.[!.]* ; do - [ -e "$f" ] && cp "$f" "$FRAMEWORK_DIR/$(basename "$f")" - done - echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION, $(ls -a "$FRAMEWORK_DIR" | wc -l) copied files)" + # hostfxr requires Microsoft.NETCore.App.deps.json to recognize a valid + # framework version directory. This file is NOT in the self-contained publish + # output -- it only exists in the .NET SDK's shared framework directory. + # Find and copy it from the SDK installation. + DOTNET_SDK_ROOT=$(dirname "$(dirname "$(command -v dotnet)")") + SDK_FX_DIR="$DOTNET_SDK_ROOT/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" + if [ ! -d "$SDK_FX_DIR" ]; then + # Try the standard /usr/share/dotnet location + SDK_FX_DIR="/usr/share/dotnet/shared/Microsoft.NETCore.App/$FRAMEWORK_VERSION" + fi + if [ -f "$SDK_FX_DIR/Microsoft.NETCore.App.deps.json" ]; then + cp "$SDK_FX_DIR/Microsoft.NETCore.App.deps.json" "$FRAMEWORK_DIR/" + echo " Copied Microsoft.NETCore.App.deps.json from SDK" + else + echo "WARNING: Microsoft.NETCore.App.deps.json not found in SDK at $SDK_FX_DIR" + # As a fallback, search for it under dotnet root + FOUND_DEPS=$(find /usr -name "Microsoft.NETCore.App.deps.json" -path "*/$FRAMEWORK_VERSION/*" 2>/dev/null | head -1) + if [ -n "$FOUND_DEPS" ]; then + cp "$FOUND_DEPS" "$FRAMEWORK_DIR/" + echo " Found and copied deps.json from $FOUND_DEPS" + fi + fi + # Also copy .version if available + if [ -f "$SDK_FX_DIR/.version" ]; then + cp "$SDK_FX_DIR/.version" "$FRAMEWORK_DIR/" + fi + echo "Success: Configured for component hosting (framework $FRAMEWORK_VERSION, $(ls -a "$FRAMEWORK_DIR" | wc -l) files)" fi fi From 698d13d60a6ede79d72d088e7626c5715c17e143 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 13 May 2026 17:28:42 -0700 Subject: [PATCH 28/45] Fix assembly load fallback: always obtain both delegates for call_managed_method The get_dotnet_load_assembly function previously returned immediately after obtaining hdt_get_function_pointer without also obtaining hdt_load_assembly_and_get_function_pointer. This meant that when the Default ALC failed to resolve Microsoft.SqlServer.CSharpExtension by name (as happens in component-host scenarios on Linux), the fallback path in call_managed_method was never available because m_load_assembly_and_get_function_pointer was null. Fix: Always obtain both delegates. The explicit-path delegate (hdt_load_assembly_and_get_function_pointer) serves as a reliable fallback when name-based resolution via hdt_get_function_pointer fails. Also: switch diagnostic logging from LOG() (debug-only) to LOG_ERROR() (always on) for key initialization checkpoints so failures are visible in Release builds. --- .../include/DotnetEnvironment.h | 14 +- .../src/native/DotnetEnvironment.cpp | 214 ++++++------------ 2 files changed, 84 insertions(+), 144 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index 1e577bf7..8d281bb9 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -84,12 +84,18 @@ class DotnetEnvironment nullptr, /* load_context: nullptr = Default ALC */ nullptr, /* reserved */ (void**)&managed_func); + + if (rc != 0 || managed_func == nullptr) + { + LOG_ERROR("get_function_pointer failed for " + method_name + ": " + to_hex_string(rc) + " (will try fallback)"); + } } if ((rc != 0 || managed_func == nullptr) && m_load_assembly_and_get_function_pointer != nullptr) { // Fallback: hdt_load_assembly_and_get_function_pointer (IsolatedComponentLoadContext). - // Used on Windows and non-self-contained deployments. + // Used on Windows and non-self-contained deployments, or when Default ALC + // cannot resolve the assembly by name (e.g. component host scenario). managed_func = nullptr; rc = m_load_assembly_and_get_function_pointer( ManagedExtensionPath.c_str(), @@ -98,10 +104,16 @@ class DotnetEnvironment DelegateTypeName.c_str(), nullptr, (void**)&managed_func); + + if (rc != 0 || managed_func == nullptr) + { + LOG_ERROR("load_assembly_and_get_function_pointer also failed for " + method_name + ": " + to_hex_string(rc)); + } } if (rc != 0 || managed_func == nullptr) { + LOG_ERROR("All managed method resolution failed for " + method_name + ", rc=" + to_hex_string(rc)); return E_FAIL; } diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index d9e05d14..07d49c99 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -30,7 +30,6 @@ #ifndef _WIN32 #include #include -#include #endif using namespace std; @@ -86,10 +85,10 @@ short DotnetEnvironment::Init() return E_FAIL; } - // get_dotnet_load_assembly tries hdt_get_function_pointer first (Default ALC), - // falls back to hdt_load_assembly_and_get_function_pointer (IsolatedComponentLoadContext). - // On success with hdt_get_function_pointer, it returns the fn ptr and m_load_assembly_and_get_function_pointer stays null. - // On fallback, it returns nullptr and sets m_load_assembly_and_get_function_pointer. + // get_dotnet_load_assembly always obtains both delegates when available. + // m_get_function_pointer (Default ALC) is preferred for type identity, + // m_load_assembly_and_get_function_pointer (explicit path) is the fallback + // for when the Default ALC cannot resolve the assembly by name. m_get_function_pointer = get_dotnet_load_assembly(cxt); if (m_get_function_pointer == nullptr && m_load_assembly_and_get_function_pointer == nullptr) @@ -255,45 +254,55 @@ bool DotnetEnvironment::load_hostfxr() // Name: DotnetEnvironment::get_dotnet_load_assembly // // Description: -// Get the function pointer delegate for the loaded .NET Core. -// Uses hdt_get_function_pointer to load into Default ALC, ensuring type identity -// compatibility with user DLLs loaded via Assembly.LoadFrom. -// Falls back to hdt_load_assembly_and_get_function_pointer for Windows/non-self-contained. +// Get the function pointer delegates for the loaded .NET Core. +// Always obtains BOTH hdt_get_function_pointer (Default ALC) and +// hdt_load_assembly_and_get_function_pointer (explicit path) so call_managed_method +// can fall back to the explicit-path delegate when name-based resolution fails. +// This is critical for self-contained Linux deployments where the component host's +// Default ALC may not resolve application assemblies by name. // get_function_pointer_fn DotnetEnvironment::get_dotnet_load_assembly(hostfxr_handle cxt) { LOG("DotnetEnvironment::get_dotnet_load_assembly"); - // Try hdt_get_function_pointer first (loads into Default ALC). - // This is required for self-contained Linux deployments to ensure + // Always get hdt_load_assembly_and_get_function_pointer as a fallback. + // This loads assemblies by explicit file path into an IsolatedComponentLoadContext + // and works reliably in all deployment scenarios. + void *load_assembly_and_get_function_pointer = nullptr; + int rc2 = m_get_delegate_fptr( + cxt, + hdt_load_assembly_and_get_function_pointer, + &load_assembly_and_get_function_pointer); + if (rc2 == 0 && load_assembly_and_get_function_pointer != nullptr) + { + m_load_assembly_and_get_function_pointer = (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; + } + else + { + LOG_ERROR("Get load_assembly_and_get_function_pointer delegate failed: " + to_hex_string(rc2)); + } + + // Try hdt_get_function_pointer (loads into Default ALC). + // This is preferred for self-contained Linux deployments to ensure // the managed extension and user DLLs share the same assembly context. void *get_fn_ptr = nullptr; int rc = m_get_delegate_fptr( cxt, hdt_get_function_pointer, &get_fn_ptr); + + m_close_fptr(cxt); + if (rc == 0 && get_fn_ptr != nullptr) { - m_close_fptr(cxt); return (get_function_pointer_fn)get_fn_ptr; } - // Fallback to hdt_load_assembly_and_get_function_pointer (IsolatedComponentLoadContext). - // This path is used on Windows and non-self-contained deployments. - void *load_assembly_and_get_function_pointer = nullptr; - rc = m_get_delegate_fptr( - cxt, - hdt_load_assembly_and_get_function_pointer, - &load_assembly_and_get_function_pointer); - if (rc != 0 || load_assembly_and_get_function_pointer == nullptr) + if (m_load_assembly_and_get_function_pointer == nullptr) { - LOG_ERROR("Get delegate failed: " + to_hex_string(rc)); + LOG_ERROR("Both get_function_pointer and load_assembly_and_get_function_pointer delegates failed"); } - // Store the load_assembly_and_get_function_pointer for the fallback path - m_load_assembly_and_get_function_pointer = (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; - - m_close_fptr(cxt); // Return nullptr to signal that the caller should use m_load_assembly_and_get_function_pointer return nullptr; } @@ -348,25 +357,28 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ #endif #ifndef _WIN32 - // Diagnostic logging: verify dotnet_root and shared/ directory presence - // before calling hostfxr_initialize_for_runtime_config. + // Concise diagnostic logging for runtime initialization. + // Uses LOG_ERROR so it's visible in Release builds. if (params.dotnet_root != nullptr) { - LOG("dotnet_root: " + std::string(params.dotnet_root)); + LOG_ERROR("CSharpExt: dotnet_root=" + std::string(params.dotnet_root) + " self_contained=" + std::to_string(m_is_self_contained)); - // Check shared/ directory + // Verify shared/ framework directory std::string shared_dir = std::string(params.dotnet_root) + "/shared"; struct stat st; - if (stat(shared_dir.c_str(), &st) == 0) + if (stat(shared_dir.c_str(), &st) != 0) + { + LOG_ERROR("CSharpExt: shared/ NOT found at " + shared_dir + " (errno=" + std::to_string(errno) + ")"); + } + else { - LOG("shared/ exists (mode=" + std::to_string(st.st_mode) + " uid=" + std::to_string(st.st_uid) + " gid=" + std::to_string(st.st_gid) + ")"); - std::string app_dir = shared_dir + "/Microsoft.NETCore.App"; - if (stat(app_dir.c_str(), &st) == 0) + if (stat(app_dir.c_str(), &st) != 0) + { + LOG_ERROR("CSharpExt: Microsoft.NETCore.App/ NOT found (errno=" + std::to_string(errno) + ")"); + } + else { - LOG("Microsoft.NETCore.App/ exists (mode=" + std::to_string(st.st_mode) + ")"); - - // List version subdirectories and their contents DIR *d = opendir(app_dir.c_str()); if (d) { @@ -375,146 +387,62 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ { if (de->d_name[0] != '.') { - LOG(" framework version: " + std::string(de->d_name)); - - // List files inside this version directory std::string ver_dir = app_dir + "/" + std::string(de->d_name); DIR *vd = opendir(ver_dir.c_str()); + int fcount = 0; + bool has_deps = false; if (vd) { struct dirent *ve; - int fcount = 0; - bool has_deps = false, has_coreclr = false, has_dotversion = false; while ((ve = readdir(vd)) != nullptr) { - if (ve->d_name[0] != '.') - fcount++; - if (std::string(ve->d_name) == "Microsoft.NETCore.App.deps.json") - has_deps = true; - if (std::string(ve->d_name) == "libcoreclr.so") - has_coreclr = true; - if (std::string(ve->d_name) == ".version") - has_dotversion = true; + if (ve->d_name[0] != '.') fcount++; + if (std::string(ve->d_name) == "Microsoft.NETCore.App.deps.json") has_deps = true; } closedir(vd); - LOG(" file count: " + std::to_string(fcount) + - " deps.json=" + std::to_string(has_deps) + - " libcoreclr.so=" + std::to_string(has_coreclr) + - " .version=" + std::to_string(has_dotversion)); } + LOG_ERROR("CSharpExt: framework " + std::string(de->d_name) + " files=" + std::to_string(fcount) + " deps.json=" + std::to_string(has_deps)); } } closedir(d); } } - else - { - LOG_ERROR("Microsoft.NETCore.App/ NOT found (errno=" + std::to_string(errno) + ")"); - } - } - else - { - LOG_ERROR("shared/ NOT found at " + shared_dir + " (errno=" + std::to_string(errno) + ")"); - - // List root directory contents for debugging - DIR *d = opendir(params.dotnet_root); - if (d) - { - struct dirent *de; - int count = 0; - while ((de = readdir(d)) != nullptr) - { - if (de->d_name[0] != '.') - { - if (count < 30) - LOG(" root entry: " + std::string(de->d_name) + " (type=" + std::to_string(de->d_type) + ")"); - count++; - } - } - LOG(" total entries: " + std::to_string(count)); - closedir(d); - } - else - { - LOG_ERROR("Cannot opendir dotnet_root (errno=" + std::to_string(errno) + ")"); - } } - } - else - { - LOG("dotnet_root is nullptr"); - } - // Log runtimeconfig path and check it exists - { + // Verify runtimeconfig.json exists std::string cfg(config_path); - LOG("config_path: " + cfg); struct stat cfgst; if (stat(cfg.c_str(), &cfgst) != 0) - LOG_ERROR("config_path NOT found (errno=" + std::to_string(errno) + ")"); + LOG_ERROR("CSharpExt: runtimeconfig NOT found: " + cfg); + else + LOG_ERROR("CSharpExt: runtimeconfig OK: " + cfg + " size=" + std::to_string(cfgst.st_size)); - // Read and log first 500 chars of runtimeconfig for verification - std::ifstream rcf(cfg); - if (rcf.is_open()) - { - std::string content((std::istreambuf_iterator(rcf)), - std::istreambuf_iterator()); - if (content.size() > 500) content.resize(500); - LOG("runtimeconfig content: " + content); - } + // Verify managed assembly exists + std::string managed_dll = std::string(params.dotnet_root) + "/Microsoft.SqlServer.CSharpExtension.dll"; + if (stat(managed_dll.c_str(), &cfgst) != 0) + LOG_ERROR("CSharpExt: managed DLL NOT found: " + managed_dll); + else + LOG_ERROR("CSharpExt: managed DLL OK: " + managed_dll + " size=" + std::to_string(cfgst.st_size)); } - - // Log DOTNET_ROOT and DOTNET_MULTILEVEL_LOOKUP env vars + else { - const char *dr = getenv("DOTNET_ROOT"); - LOG("env DOTNET_ROOT=" + std::string(dr ? dr : "(null)")); - const char *ml = getenv("DOTNET_MULTILEVEL_LOOKUP"); - LOG("env DOTNET_MULTILEVEL_LOOKUP=" + std::string(ml ? ml : "(null)")); + LOG_ERROR("CSharpExt: dotnet_root is nullptr"); } - - // Enable COREHOST_TRACE for detailed hostfxr diagnostics - setenv("COREHOST_TRACE", "1", 1); - setenv("COREHOST_TRACEFILE", "/tmp/hostfxr_trace.log", 1); - setenv("COREHOST_TRACE_VERBOSITY", "4", 1); #endif int rc = m_init_fptr(config_path, ¶ms, &cxt); -#ifndef _WIN32 - // Read and log the COREHOST_TRACE output - { - std::ifstream trace("/tmp/hostfxr_trace.log"); - if (trace.is_open()) - { - std::string line; - int linecount = 0; - while (std::getline(trace, line)) - { - if (linecount < 200) - LOG("TRACE: " + line); - linecount++; - } - if (linecount >= 200) - LOG("TRACE: ... (truncated, total " + std::to_string(linecount) + " lines)"); - trace.close(); - } - else - { - LOG_ERROR("Could not read /tmp/hostfxr_trace.log"); - } - // Clean up trace env vars - unsetenv("COREHOST_TRACE"); - unsetenv("COREHOST_TRACEFILE"); - unsetenv("COREHOST_TRACE_VERBOSITY"); - } -#endif - if (rc != 0 || cxt == nullptr) { - LOG_ERROR("Init failed: " + to_hex_string(rc)); + LOG_ERROR("CSharpExt: hostfxr_initialize_for_runtime_config failed: " + to_hex_string(rc)); if (cxt) m_close_fptr(cxt); return nullptr; } + +#ifndef _WIN32 + LOG_ERROR("CSharpExt: hostfxr init succeeded, rc=" + to_hex_string(rc)); +#endif + return cxt; } From 863c8b8bfec6a1dfb0f448b7bf73c3c8c044365b Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 13 May 2026 18:06:23 -0700 Subject: [PATCH 29/45] Fix: include Logger.h in DotnetEnvironment.h for LOG_ERROR macro --- .../dotnet-core-CSharp/include/DotnetEnvironment.h | 1 + 1 file changed, 1 insertion(+) diff --git a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index 8d281bb9..4ceed2b8 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -27,6 +27,7 @@ #include #include #include +#include "Logger.h" #ifdef _WIN32 #define STR(s) L ## s From 69dbddfab9c94f33542d269ac4d3a0c9a0be8d91 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Wed, 13 May 2026 18:54:59 -0700 Subject: [PATCH 30/45] Fix: add pragma once to Logger.h to prevent double inclusion --- language-extensions/dotnet-core-CSharp/include/Logger.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/include/Logger.h b/language-extensions/dotnet-core-CSharp/include/Logger.h index fa43d0b8..3c389ebf 100644 --- a/language-extensions/dotnet-core-CSharp/include/Logger.h +++ b/language-extensions/dotnet-core-CSharp/include/Logger.h @@ -8,6 +8,8 @@ // Wrapper class around logging to standardize logging messages and errors. // //********************************************************************* +#pragma once + #include #include From 4bb81d457f2dc3765d6c2b1640644b76860e35ad Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Thu, 14 May 2026 11:42:50 -0700 Subject: [PATCH 31/45] Add early-load + per-entry diagnostics to libnativecsharpextension.so --- .../dotnet-core-CSharp/src/native/Logger.cpp | 94 ++++++++++++++++++- .../src/native/nativecsharpextension.cpp | 14 +++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp index c44b011b..79e29cc3 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp @@ -10,10 +10,97 @@ //********************************************************************* #include #include +#include +#include +#include +#include +#include #include "Logger.h" +#ifndef _WIN32 +#include +#include +#endif + using namespace std; +namespace +{ + // Diagnostic log file path. /tmp is writable in the sandbox; the file + // gets persisted in the sandbox dir if /tmp is bind-mounted. We also + // write to stderr so output is captured in launchpadd-stderr. + constexpr const char *DIAG_LOG_PATH = "/tmp/csharpext-diag.log"; + + std::mutex g_log_mutex; + + // Get a timestamp + pid prefix for log lines. + std::string get_log_prefix() + { + auto now = std::chrono::system_clock::now(); + auto t = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()).count() % 1000; + std::tm tm_buf; +#ifdef _WIN32 + gmtime_s(&tm_buf, &t); +#else + gmtime_r(&t, &tm_buf); +#endif + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf); + std::ostringstream oss; + oss << buf << "." << ms; +#ifndef _WIN32 + oss << " pid=" << getpid(); +#endif + oss << " CSharpExt: "; + return oss.str(); + } + + // Write the message to stderr AND to the diagnostic file. + // Best-effort: never throw, never block on file errors. + void write_diag(const std::string &line) + { + std::lock_guard lock(g_log_mutex); + // stderr goes to launchpadd-stderr in the SQL container. + std::cerr << line << std::endl; + std::cerr.flush(); +#ifndef _WIN32 + // Also append to a file under /tmp so we can find it even if + // stderr is consumed/redirected by the satellite host. + try + { + std::ofstream f(DIAG_LOG_PATH, std::ios::app); + if (f.is_open()) + { + f << line << std::endl; + } + } + catch (...) + { + // ignore + } +#endif + } +} + +#ifndef _WIN32 +// Constructor function: runs the moment dlopen() loads this .so successfully. +// If we ever see this line in /tmp/csharpext-diag.log, we know exthost +// succeeded loading the extension. Absence of this line proves dlopen failed. +__attribute__((constructor)) +static void csharp_extension_loaded() +{ + write_diag(get_log_prefix() + "[ctor] libnativecsharpextension.so LOADED via dlopen"); +} + +__attribute__((destructor)) +static void csharp_extension_unloaded() +{ + write_diag(get_log_prefix() + "[dtor] libnativecsharpextension.so being unloaded"); +} +#endif + //-------------------------------------------------------------------------------------------------- // Name: Logger::Log // @@ -31,9 +118,14 @@ void Logger::Log(const string &msg) // Name: Logger::LogError // // Description: -// Logs an error to stderr +// Logs an error to stderr (and to /tmp/csharpext-diag.log on Linux for +// postmortem retrieval — exthost may swallow stderr). // void Logger::LogError(const string &errorMsg) { +#ifdef _WIN32 cerr << "Error: " << errorMsg < #define nameof(x) #x @@ -50,6 +51,7 @@ std::string UTF8PtrToStr(SQLCHAR* str, SQLULEN len) // SQLUSMALLINT GetInterfaceVersion() { + LOG_ERROR("[entry] GetInterfaceVersion"); return EXTERNAL_LANGUAGE_EXTENSION_API; } @@ -73,6 +75,9 @@ SQLRETURN Init( SQLULEN privateLibraryPathLen ) { + LOG_ERROR(std::string("[entry] Init languagePath='") + + (languagePath ? std::string(reinterpret_cast(languagePath), languagePathLen) : std::string("")) + + "'"); LOG("nativecsharpextension::Init"); g_dotnet_runtime = new DotnetEnvironment( UTF8PtrToStr(languageParams, languageParamsLen), @@ -117,6 +122,7 @@ SQLRETURN InitSession( SQLUSMALLINT outputDataNameLength ) { + LOG_ERROR("[entry] InitSession"); LOG("nativecsharpextension::InitSession"); return g_dotnet_runtime->call_managed_method(nameof(InitSession), sessionId, @@ -155,6 +161,7 @@ SQLRETURN InitColumn( SQLSMALLINT orderByNumber ) { + LOG_ERROR("[entry] InitColumn"); LOG("nativecsharpextension::InitColumn"); return g_dotnet_runtime->call_managed_method(nameof(InitColumn), sessionId, @@ -193,6 +200,7 @@ SQLRETURN InitParam( SQLSMALLINT inputOutputType ) { + LOG_ERROR("[entry] InitParam"); LOG("nativecsharpextension::InitParam"); return g_dotnet_runtime->call_managed_method(nameof(InitParam), sessionId, @@ -228,6 +236,7 @@ SQLRETURN Execute( SQLUSMALLINT *outputSchemaColumnsNumber ) { + LOG_ERROR("[entry] Execute"); LOG("nativecsharpextension::Execute"); return g_dotnet_runtime->call_managed_method(nameof(Execute), sessionId, @@ -257,6 +266,7 @@ SQLRETURN GetResultColumn( SQLSMALLINT *nullable ) { + LOG_ERROR("[entry] GetResultColumn"); LOG("nativecsharpextension::GetResultColumn"); return g_dotnet_runtime->call_managed_method(nameof(GetResultColumn), sessionId, @@ -285,6 +295,7 @@ SQLRETURN GetResults( SQLINTEGER ***strLen_or_Ind ) { + LOG_ERROR("[entry] GetResults"); LOG("nativecsharpextension::GetResults"); return g_dotnet_runtime->call_managed_method(nameof(GetResults), sessionId, @@ -311,6 +322,7 @@ SQLRETURN GetOutputParam( SQLINTEGER *strLen_or_Ind ) { + LOG_ERROR("[entry] GetOutputParam"); LOG("nativecsharpextension::GetOutputParam"); return g_dotnet_runtime->call_managed_method(nameof(GetOutputParam), sessionId, @@ -332,6 +344,7 @@ SQLRETURN GetOutputParam( // SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT taskId) { + LOG_ERROR("[entry] CleanupSession"); LOG("nativecsharpextension::CleanupSession"); return g_dotnet_runtime->call_managed_method(nameof(CleanupSession), sessionId, @@ -349,6 +362,7 @@ SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT taskId) // SQLRETURN Cleanup() { + LOG_ERROR("[entry] Cleanup"); LOG("nativecsharpextension::Cleanup"); // Clear the managed-side callbacks delegates before tearing down the From 17c3c1aea88821068775c1c8fd0b0ab5fca03fb4 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 15 May 2026 11:02:49 -0700 Subject: [PATCH 32/45] Write diagnostics to /var/opt/mssql/log/csharpext-diag.log so PVS captures it --- .../dotnet-core-CSharp/src/native/Logger.cpp | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp index 79e29cc3..e9e3da07 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp @@ -26,10 +26,26 @@ using namespace std; namespace { - // Diagnostic log file path. /tmp is writable in the sandbox; the file - // gets persisted in the sandbox dir if /tmp is bind-mounted. We also - // write to stderr so output is captured in launchpadd-stderr. - constexpr const char *DIAG_LOG_PATH = "/tmp/csharpext-diag.log"; + // Diagnostic log file paths - try multiple locations so at least one + // gets captured by the test infrastructure. + // + // PRIMARY: /var/opt/mssql/log/csharpext-diag.log + // - The SQL Server log directory (auto-collected as + // sql01_MSSQLSERVER_csharpext-diag.log in PVS test attachments) + // - May NOT be writable from the extension sandbox (different uid/ns) + // + // SECONDARY: /var/opt/mssql-extensibility/log/csharpext-diag.log + // - Extension-specific log dir, writable by extension sandbox uid + // - Sometimes auto-collected by PVS, sometimes not + // + // TERTIARY: /tmp/csharpext-diag.log + // - Always writable but may not be collected (tmpfs / cleaned up) + static const char* const DIAG_LOG_PATHS[] = { + "/var/opt/mssql/log/csharpext-diag.log", + "/var/opt/mssql-extensibility/log/csharpext-diag.log", + "/tmp/csharpext-diag.log", + nullptr + }; std::mutex g_log_mutex; @@ -57,7 +73,7 @@ namespace return oss.str(); } - // Write the message to stderr AND to the diagnostic file. + // Write the message to stderr AND to the diagnostic file(s). // Best-effort: never throw, never block on file errors. void write_diag(const std::string &line) { @@ -66,19 +82,22 @@ namespace std::cerr << line << std::endl; std::cerr.flush(); #ifndef _WIN32 - // Also append to a file under /tmp so we can find it even if - // stderr is consumed/redirected by the satellite host. - try + // Try writing to ALL diagnostic paths so at least one is captured by + // PVS test attachments. Each is best-effort - silently ignore failures. + for (const char* const* p = DIAG_LOG_PATHS; *p != nullptr; ++p) { - std::ofstream f(DIAG_LOG_PATH, std::ios::app); - if (f.is_open()) + try { - f << line << std::endl; + std::ofstream f(*p, std::ios::app); + if (f.is_open()) + { + f << line << std::endl; + } + } + catch (...) + { + // ignore - try next path } - } - catch (...) - { - // ignore } #endif } From 236b1511eaf395bb08b3c0a078f9e56a78f86476 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 15 May 2026 13:01:43 -0700 Subject: [PATCH 33/45] Linux: emit .zip alongside .tar.gz (SQL Linux launchpad uses unzip, not tar) --- ...create-dotnet-core-CSharp-extension-tar.sh | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh index 31785857..9128e551 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh @@ -54,15 +54,32 @@ for BUILD_CONFIGURATION in "$@"; do # Include the shared/ directory (framework file copies for component hosting). # The build script creates shared/Microsoft.NETCore.App// with copies # of the root DLLs/SOs so hostfxr can resolve the bundled .NET runtime. - # File copies are used because SQL Server's tar extraction does not preserve + # File copies are used because SQL Server's archive extraction does not preserve # symlinks or hard links. if [ -d "$BUILD_OUTPUT/shared" ]; then FILES_TO_COMPRESS+=("shared") fi - # Package the binaries + # Package the binaries. + # + # IMPORTANT: SQL Server on Linux extracts CREATE EXTERNAL LANGUAGE archives + # using its built-in zip code (the same path R/Python/Java use). Tar.gz is + # NOT supported on Linux - exthost would silently fail to find the .so and + # exit with status 9 immediately after launch. Use zip + .zip extension to + # match the format expected by the satellite/launchpad. + # + # Both .tar.gz and .zip outputs are produced for backward compatibility: + # - dotnet-core-CSharp-lang-extension-linux.zip (NEW - used by Linux launchpad) + # - dotnet-core-CSharp-lang-extension.tar.gz (legacy/Windows-style, kept for compat) pushd "$BUILD_OUTPUT" > /dev/null - tar -czvf "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension.tar.gz" "${FILES_TO_COMPRESS[@]}" + + # Primary: zip archive (Linux SQL Server expects this format) + rm -f "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension-linux.zip" + zip -r "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension-linux.zip" "${FILES_TO_COMPRESS[@]}" > /dev/null + check_error $? "Failed to create zip for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" + + # Legacy: tar.gz (kept for any callers still referencing the old name) + tar -czf "$BUILD_OUTPUT/packages/dotnet-core-CSharp-lang-extension.tar.gz" "${FILES_TO_COMPRESS[@]}" check_error $? "Failed to create tarball for dotnet-core-CSharp-extension for configuration=$BUILD_CONFIGURATION" popd > /dev/null From 09aafd7d5f1cab406bd564c5a74786f109821173 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Mon, 18 May 2026 16:39:36 -0700 Subject: [PATCH 34/45] Add early-priority constructor (101) with POSIX syscalls for pre-stdcxx-init diagnostics --- .../dotnet-core-CSharp/src/native/Logger.cpp | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp index e9e3da07..e00f1deb 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp @@ -20,6 +20,8 @@ #ifndef _WIN32 #include #include +#include +#include #endif using namespace std; @@ -104,9 +106,40 @@ namespace } #ifndef _WIN32 -// Constructor function: runs the moment dlopen() loads this .so successfully. -// If we ever see this line in /tmp/csharpext-diag.log, we know exthost -// succeeded loading the extension. Absence of this line proves dlopen failed. +// Highest-priority constructor (101 - lowest user-allowed number = runs first). +// Bypasses get_log_prefix() (which uses C++ iostreams, chrono, etc) to avoid +// any dependency on global initialization order. Uses pure POSIX syscalls. +// This is the FIRST evidence we have that the .so was actually loaded by exthost. +__attribute__((constructor(101))) +static void csharp_extension_loaded_early() +{ + const char *paths[] = { + "/var/opt/mssql/log/csharpext-diag.log", + "/var/opt/mssql-extensibility/log/csharpext-diag.log", + "/tmp/csharpext-diag.log", + nullptr + }; + char buf[512]; + int n = snprintf(buf, sizeof(buf), + "[ctor-early] libnativecsharpextension.so LOADED pid=%d uid=%d euid=%d gid=%d\n", + (int)getpid(), (int)getuid(), (int)geteuid(), (int)getgid()); + if (n < 0) return; + // stderr - direct write, bypassing any iostreams init + if (write(STDERR_FILENO, buf, (size_t)n) < 0) { /* ignore */ } + // Then files (best-effort) + for (const char* const* p = paths; *p != nullptr; ++p) + { + int fd = open(*p, O_WRONLY | O_APPEND | O_CREAT, 0666); + if (fd >= 0) + { + if (write(fd, buf, (size_t)n) < 0) { /* ignore */ } + close(fd); + } + } +} + +// Higher-level constructor: runs after C++ globals are initialized. +// Uses Logger.LogError (which uses iostreams) so test outputs are consistent. __attribute__((constructor)) static void csharp_extension_loaded() { From baedcf558a8b1c2b1b3235ab6585bfaf27002b3c Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Mon, 18 May 2026 19:57:39 -0700 Subject: [PATCH 35/45] Diag log: prioritize /tmp and /home/mssql_satellite (visible in satellite sandbox) --- .../dotnet-core-CSharp/src/native/Logger.cpp | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp index e00f1deb..e8f0194d 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp @@ -31,21 +31,28 @@ namespace // Diagnostic log file paths - try multiple locations so at least one // gets captured by the test infrastructure. // - // PRIMARY: /var/opt/mssql/log/csharpext-diag.log - // - The SQL Server log directory (auto-collected as - // sql01_MSSQLSERVER_csharpext-diag.log in PVS test attachments) - // - May NOT be writable from the extension sandbox (different uid/ns) + // CRITICAL: SQL Linux extension runs INSIDE a sandboxed namespace where: + // - /var/opt/mssql/log is NOT mounted (invisible to satellite uid) + // - /var/opt/mssql-extensibility/log is NOT bind-mounted (invisible) + // - The only writable host-visible paths are: + // /tmp (tmpfs in ns) + // /home/mssql_satellite/externallanguagessandboxpath (bind-mounted, ro for libs) + // /home/mssql_satellite/externallanguagessandboxtemppath (bind-mounted, may be rw) + // /home/mssql_satellite (the sandbox HOME) // - // SECONDARY: /var/opt/mssql-extensibility/log/csharpext-diag.log - // - Extension-specific log dir, writable by extension sandbox uid - // - Sometimes auto-collected by PVS, sometimes not + // For PVS test capture (looks at host /var/opt/mssql/log/), we use a + // trick: write to a directory THAT IS bind-mounted, where the host + // checkinstallextensibility.sh has pre-created a symlink targeting + // /var/opt/mssql/log/csharpext-diag.log. // - // TERTIARY: /tmp/csharpext-diag.log - // - Always writable but may not be collected (tmpfs / cleaned up) + // The simplest reliable path is /tmp (tmpfs inside the satellite ns - + // doesn't survive the session but proves the .so was loaded). + // We also try /home/mssql_satellite which is the satellite uid's home. static const char* const DIAG_LOG_PATHS[] = { + "/tmp/csharpext-diag.log", + "/home/mssql_satellite/csharpext-diag.log", "/var/opt/mssql/log/csharpext-diag.log", "/var/opt/mssql-extensibility/log/csharpext-diag.log", - "/tmp/csharpext-diag.log", nullptr }; @@ -114,9 +121,10 @@ __attribute__((constructor(101))) static void csharp_extension_loaded_early() { const char *paths[] = { + "/tmp/csharpext-diag.log", + "/home/mssql_satellite/csharpext-diag.log", "/var/opt/mssql/log/csharpext-diag.log", "/var/opt/mssql-extensibility/log/csharpext-diag.log", - "/tmp/csharpext-diag.log", nullptr }; char buf[512]; From 6cb56a077abffa709703a6b06eb4bd699f0518f8 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 19 May 2026 09:44:46 -0700 Subject: [PATCH 36/45] Linux: statically link libstdc++/libgcc (fixes GLIBCXX_3.4.32 not found at dlopen on Ubuntu 22.04-based mssql containers) --- .../build/linux/build-dotnet-core-CSharp-extension.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index f6122004..3a0a830c 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -51,6 +51,16 @@ for BUILD_CONFIGURATION in "$@"; do -o libnativecsharpextension.so "-I$DOTNET_NATIVE_INCLUDE" "-I$EXTENSION_HOST_INCLUDE" + # Statically link libstdc++ and libgcc so the resulting .so does NOT + # depend on the build host's libstdc++.so.6 version. + # Required because the build hosts (Ubuntu 24.04 / GCC 13+) introduce + # symbols like _ZSt21ios_base_library_initv@GLIBCXX_3.4.32 that don't + # exist on the runtime hosts (SQL Server containers may ship older + # libstdc++.so.6). Without these flags, dlopen() fails with: + # "version `GLIBCXX_3.4.32' not found" + # and the extension never loads. + -static-libstdc++ + -static-libgcc ) if [ "$BUILD_CONFIGURATION" = "debug" ]; then From a1981bbe8f669e8cf1b73a1f582b6eac58d8fe45 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Tue, 19 May 2026 15:44:20 -0700 Subject: [PATCH 37/45] Build with g++-11 (Ubuntu 22.04 toolchain) for glibc 2.34 compat with SQL runtime --- .../build-dotnet-core-CSharp-extension.sh | 16 ++++++++-- .../build/linux/restore-packages.sh | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index 3a0a830c..e4403480 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -39,8 +39,20 @@ for BUILD_CONFIGURATION in "$@"; do EXTENSION_HOST_INCLUDE="$ENL_ROOT/extension-host/include" DOTNET_NATIVE_LIB="$DOTNET_EXTENSION_HOME/lib" - # Determine C++ compiler - CXX="${CXX:-c++}" + # Determine C++ compiler. + # Prefer g++-11 (Ubuntu 22.04 toolchain) to match the SQL Server runtime + # container's glibc 2.35 / libstdc++ GLIBCXX_3.4.30 ABI. Newer compilers + # (Ubuntu 24.04 / gcc 13) generate references to GLIBC_2.38 symbols + # (e.g. __isoc23_strtoul) that don't exist in the runtime, causing + # dlopen() to fail silently and the extension to never load. + if [ -z "${CXX:-}" ]; then + if command -v g++-11 &>/dev/null; then + CXX="g++-11" + else + CXX="c++" + fi + fi + echo "[Info] Using C++ compiler: $CXX ($($CXX --version | head -1))" # Build compiler arguments CC_ARGS=( diff --git a/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh b/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh index f83890e9..ceb63bb8 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh @@ -33,6 +33,38 @@ else check_exit_code "Success: Installed .NET SDK 8.0" "Error: Failed to install .NET SDK 8.0" fi +# Install gcc-11 / g++-11 if not present. +# +# IMPORTANT: SQL Server's mssql-server-extensibility binaries (including the +# satellite/exthost) are built and shipped against Ubuntu 22.04 (glibc 2.35, +# libstdc++ GLIBCXX_3.4.30). When the extension .so is built with newer +# toolchains (Ubuntu 24.04 / gcc 13), it requires: +# - GLIBCXX_3.4.32 (e.g. std::ios_base_library_init - GCC 13 auto-inject) +# - GLIBC_2.38 (e.g. __isoc23_strtoul) +# which don't exist on the runtime container. dlopen() then silently fails +# with "version `GLIBC_2.38' not found" and the extension is never loaded. +# +# Static-linking libstdc++ + building with gcc-11 produces a binary whose +# external symbol requirements stay within glibc 2.34, compatible with both +# Ubuntu 22.04 and 24.04 SQL Server runtimes. +if command -v gcc-11 &>/dev/null && command -v g++-11 &>/dev/null; then + echo "Info: gcc-11 already installed ($(gcc-11 --version | head -1))" +else + echo "Info: Installing gcc-11/g++-11 (matches Ubuntu 22.04 toolchain - SQL runtime ABI)..." + # Ensure universe repo is enabled (gcc-11 lives there on Ubuntu 24.04) + if command -v add-apt-repository &>/dev/null; then + add-apt-repository -y universe 2>/dev/null || true + fi + apt-get update -qq || true + apt-get install -y --no-install-recommends gcc-11 g++-11 || { + # gcc-11 not available; fall back to default and hope GLIBC backcompat works. + # Caller will see GLIBC_2.38 requirement in the .so and dlopen will fail + # on Ubuntu 22.04-based runtimes - but we've at least tried. + echo "Warning: gcc-11 install failed - falling back to default toolchain" + echo "Warning: Built .so may require glibc > 2.35 (incompatible with SQL Server 2022 Ubuntu 22.04 SFP)" + } +fi + # Copy libnethost.a from the .NET SDK runtime packs into the extension's lib directory. # The SDK ships this as part of the AppHost pack. # From f717f9217fd8e2747a459543448b3229e97bcba6 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Thu, 27 Aug 2026 17:13:14 -0700 Subject: [PATCH 38/45] Use the GCC 11 dynamic runtime for CSharp on Linux --- .../linux/build-dotnet-core-CSharp-extension.sh | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh index e4403480..d5d0cc1d 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -63,16 +63,9 @@ for BUILD_CONFIGURATION in "$@"; do -o libnativecsharpextension.so "-I$DOTNET_NATIVE_INCLUDE" "-I$EXTENSION_HOST_INCLUDE" - # Statically link libstdc++ and libgcc so the resulting .so does NOT - # depend on the build host's libstdc++.so.6 version. - # Required because the build hosts (Ubuntu 24.04 / GCC 13+) introduce - # symbols like _ZSt21ios_base_library_initv@GLIBCXX_3.4.32 that don't - # exist on the runtime hosts (SQL Server containers may ship older - # libstdc++.so.6). Without these flags, dlopen() fails with: - # "version `GLIBCXX_3.4.32' not found" - # and the extension never loads. - -static-libstdc++ - -static-libgcc + # Use g++-11's dynamic runtime. It stays within the RHEL 9/Ubuntu 22.04 + # GLIBCXX ceiling without embedding a second C++ runtime and unwinder + # in the extension host process. ) if [ "$BUILD_CONFIGURATION" = "debug" ]; then From 34c95772f00f5c052ad6b28f0ae496e8a252d3c9 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 28 Aug 2026 00:17:31 -0700 Subject: [PATCH 39/45] Smoke test CSharp runtime initialization on Linux --- .../build/linux/smoke-test-extension.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py diff --git a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py new file mode 100644 index 00000000..d1fe9b09 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import ctypes +import os +from pathlib import Path + + +build_output = Path(__file__).resolve().parents[4] / "build-output" / "dotnet-core-CSharp-extension" / "linux" / "release" +extension_path = build_output / "libnativecsharpextension.so" + +extension = ctypes.CDLL(str(extension_path), mode=os.RTLD_NOW | os.RTLD_LOCAL) + +extension.GetInterfaceVersion.restype = ctypes.c_ushort +print(f"GetInterfaceVersion={extension.GetInterfaceVersion()}") + +extension.Init.argtypes = [ + ctypes.c_char_p, + ctypes.c_ulonglong, + ctypes.c_char_p, + ctypes.c_ulonglong, + ctypes.c_char_p, + ctypes.c_ulonglong, + ctypes.c_char_p, + ctypes.c_ulonglong, +] +extension.Init.restype = ctypes.c_short + +root = os.fsencode(build_output) +empty = b"" +result = extension.Init(empty, 0, root, len(root), empty, 0, empty, 0) +if result != 0: + raise RuntimeError(f"CSharp extension Init failed with SQLRETURN {result}") + +extension.Cleanup.restype = ctypes.c_short +result = extension.Cleanup() +if result != 0: + raise RuntimeError(f"CSharp extension Cleanup failed with SQLRETURN {result}") + +print("CSharp extension load, Init, and Cleanup succeeded") \ No newline at end of file From 87a74ed0a6143ea2566b0171d9dc6ad99c76eaa5 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 28 Aug 2026 01:03:55 -0700 Subject: [PATCH 40/45] Prefer the bundled CSharp runtime on Linux --- .../build/linux/smoke-test-extension.py | 3 +++ .../src/native/DotnetEnvironment.cpp | 12 +++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py index d1fe9b09..74625f9a 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py +++ b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py @@ -12,6 +12,9 @@ extension.GetInterfaceVersion.restype = ctypes.c_ushort print(f"GetInterfaceVersion={extension.GetInterfaceVersion()}") +# A bundled deployment must not inherit another product's .NET installation. +os.environ["DOTNET_ROOT"] = "/nonexistent/ambient-dotnet-root" + extension.Init.argtypes = [ ctypes.c_char_p, ctypes.c_ulonglong, diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index 07d49c99..7474f9e5 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -343,16 +343,14 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ // root DLLs/SOs. File copies are used because SQL Server's tar extraction does // not preserve symbolic links or hard links when extracting CREATE EXTERNAL // LANGUAGE payloads. - // For framework-dependent deployments, leave dotnet_root as nullptr - // so hostfxr uses its default discovery mechanism. - const char *dotnet_root_env = getenv("DOTNET_ROOT"); - if (dotnet_root_env != nullptr) + // For framework-dependent deployments, honor an ambient DOTNET_ROOT when present. + if (m_is_self_contained) { - params.dotnet_root = dotnet_root_env; + params.dotnet_root = m_root_path.c_str(); } - else if (m_is_self_contained) + else { - params.dotnet_root = m_root_path.c_str(); + params.dotnet_root = getenv("DOTNET_ROOT"); } #endif From b4f47bb9c127c9abad0a4c0adb4d119a564f9bb8 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 28 Aug 2026 15:12:44 -0700 Subject: [PATCH 41/45] Smoke test debug and release CSharp payloads --- .../build/linux/smoke-test-extension.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py index 74625f9a..3b2ff9e8 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py +++ b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py @@ -1,10 +1,15 @@ #!/usr/bin/env python3 import ctypes import os +import sys from pathlib import Path -build_output = Path(__file__).resolve().parents[4] / "build-output" / "dotnet-core-CSharp-extension" / "linux" / "release" +if len(sys.argv) != 2 or sys.argv[1] not in {"debug", "release"}: + raise SystemExit(f"Usage: {Path(sys.argv[0]).name} ") + +configuration = sys.argv[1] +build_output = Path(__file__).resolve().parents[4] / "build-output" / "dotnet-core-CSharp-extension" / "linux" / configuration extension_path = build_output / "libnativecsharpextension.so" extension = ctypes.CDLL(str(extension_path), mode=os.RTLD_NOW | os.RTLD_LOCAL) @@ -38,4 +43,4 @@ if result != 0: raise RuntimeError(f"CSharp extension Cleanup failed with SQLRETURN {result}") -print("CSharp extension load, Init, and Cleanup succeeded") \ No newline at end of file +print(f"CSharp {configuration} extension load, Init, and Cleanup succeeded") \ No newline at end of file From 90ca99add43b33bb8602da55ec87ac3b272b54c3 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 28 Aug 2026 15:55:16 -0700 Subject: [PATCH 42/45] Smoke test CSharp user assembly execution --- .../build/linux/smoke-test-extension.py | 82 ++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py index 3b2ff9e8..1867be5f 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py +++ b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py @@ -20,6 +20,8 @@ # A bundled deployment must not inherit another product's .NET installation. os.environ["DOTNET_ROOT"] = "/nonexistent/ambient-dotnet-root" +test_output = build_output.parents[2] / "dotnet-core-CSharp-extension-test" / "linux" / configuration + extension.Init.argtypes = [ ctypes.c_char_p, ctypes.c_ulonglong, @@ -33,14 +35,90 @@ extension.Init.restype = ctypes.c_short root = os.fsencode(build_output) +public_library_path = os.fsencode(test_output) empty = b"" -result = extension.Init(empty, 0, root, len(root), empty, 0, empty, 0) +result = extension.Init( + empty, + 0, + root, + len(root), + public_library_path, + len(public_library_path), + empty, + 0, +) if result != 0: raise RuntimeError(f"CSharp extension Init failed with SQLRETURN {result}") +class SqlGuid(ctypes.Structure): + _fields_ = [ + ("data1", ctypes.c_uint32), + ("data2", ctypes.c_uint16), + ("data3", ctypes.c_uint16), + ("data4", ctypes.c_ubyte * 8), + ] + + +script = b"Microsoft.SqlServer.CSharpExtensionTest.CSharpTestExecutor" +input_name = b"InputDataSet" +output_name = b"OutputDataSet" +session_id = SqlGuid(1, 2, 3, (ctypes.c_ubyte * 8)(4, 5, 6, 7, 8, 9, 10, 11)) + +extension.InitSession.restype = ctypes.c_short +extension.InitSession.argtypes = [ + SqlGuid, + ctypes.c_ushort, + ctypes.c_ushort, + ctypes.c_char_p, + ctypes.c_ulonglong, + ctypes.c_ushort, + ctypes.c_ushort, + ctypes.c_char_p, + ctypes.c_ushort, + ctypes.c_char_p, + ctypes.c_ushort, +] +result = extension.InitSession( + session_id, + 0, + 1, + script, + len(script), + 0, + 0, + input_name, + len(input_name), + output_name, + len(output_name), +) +if result != 0: + raise RuntimeError(f"CSharp extension InitSession failed with SQLRETURN {result}") + +extension.Execute.restype = ctypes.c_short +extension.Execute.argtypes = [ + SqlGuid, + ctypes.c_ushort, + ctypes.c_ulonglong, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_ushort), +] +output_columns = ctypes.c_ushort() +result = extension.Execute(session_id, 0, 0, None, None, ctypes.byref(output_columns)) +if result != 0: + raise RuntimeError( + f"CSharp extension Execute failed with SQLRETURN {result}; test output: {test_output}" + ) + +extension.CleanupSession.restype = ctypes.c_short +extension.CleanupSession.argtypes = [SqlGuid, ctypes.c_ushort] +result = extension.CleanupSession(session_id, 0) +if result != 0: + raise RuntimeError(f"CSharp extension CleanupSession failed with SQLRETURN {result}") + extension.Cleanup.restype = ctypes.c_short result = extension.Cleanup() if result != 0: raise RuntimeError(f"CSharp extension Cleanup failed with SQLRETURN {result}") -print(f"CSharp {configuration} extension load, Init, and Cleanup succeeded") \ No newline at end of file +print(f"CSharp {configuration} extension load, session Execute, and Cleanup succeeded") \ No newline at end of file From b6268a92ad970af97cf88ebf3b26eb15469f2242 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 28 Aug 2026 18:09:13 -0700 Subject: [PATCH 43/45] Load CSharp executors in the extension context --- .../dotnet-core-CSharp/src/managed/utils/DllUtils.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs index c31fe182..ef2349b5 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs @@ -12,6 +12,7 @@ using System.Linq; using System.IO; using System.Reflection; +using System.Runtime.Loader; using System.Collections.Generic; using Microsoft.SqlServer.CSharpExtension.SDK; @@ -37,13 +38,17 @@ public static Type GetUserDll(string userClassName, List dllList) // AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; - foreach(string dllPath in dllList) + AssemblyLoadContext extensionLoadContext = + AssemblyLoadContext.GetLoadContext(typeof(AbstractSqlServerExtensionExecutor).Assembly); + + foreach(string dllPath in dllList.Where( + path => string.Equals(Path.GetExtension(path), ".dll", StringComparison.OrdinalIgnoreCase))) { // Catch unexpected exception while loading other dlls // try { - Assembly userDll = Assembly.LoadFrom(dllPath); + Assembly userDll = extensionLoadContext.LoadFromAssemblyPath(Path.GetFullPath(dllPath)); Type userExecutorClass = userDll.GetType(userClassName); if (userExecutorClass != null) From 5036691e28e5234c475fcb875483d0975248a6fd Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 28 Aug 2026 22:14:58 -0700 Subject: [PATCH 44/45] Handle absent optional CSharp library paths --- .../dotnet-core-CSharp/build/linux/smoke-test-extension.py | 5 +++-- .../dotnet-core-CSharp/src/managed/utils/DllUtils.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py index 1867be5f..8884f3a6 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py +++ b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py @@ -36,6 +36,7 @@ root = os.fsencode(build_output) public_library_path = os.fsencode(test_output) +private_library_path = os.fsencode(test_output / "nonexistent-private-library-path") empty = b"" result = extension.Init( empty, @@ -44,8 +45,8 @@ len(root), public_library_path, len(public_library_path), - empty, - 0, + private_library_path, + len(private_library_path), ) if result != 0: raise RuntimeError(f"CSharp extension Init failed with SQLRETURN {result}") diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs index ef2349b5..4d669729 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/DllUtils.cs @@ -86,12 +86,12 @@ public static List CreateDllList( ListdllList = new List(); if(string.IsNullOrEmpty(userLibName)) { - if (!string.IsNullOrEmpty(privatePath)) + if (!string.IsNullOrEmpty(privatePath) && Directory.Exists(privatePath)) { dllList.AddRange(Directory.GetFiles(privatePath)); } - if (!string.IsNullOrEmpty(publicPath)) + if (!string.IsNullOrEmpty(publicPath) && Directory.Exists(publicPath)) { dllList.AddRange(Directory.GetFiles(publicPath)); } From 4d0809d94f3c14694f30ccd873bfc16b828270a2 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Sat, 29 Aug 2026 02:11:19 -0700 Subject: [PATCH 45/45] Make installed CSharp libraries execution-readable --- .../build/linux/smoke-test-extension.py | 49 ++++++++++++++++++- .../src/managed/CSharpExtension.cs | 21 ++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py index 8884f3a6..25cbf0aa 100644 --- a/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py +++ b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import ctypes import os +import shutil +import stat import sys from pathlib import Path @@ -21,6 +23,14 @@ os.environ["DOTNET_ROOT"] = "/nonexistent/ambient-dotnet-root" test_output = build_output.parents[2] / "dotnet-core-CSharp-extension-test" / "linux" / configuration +smoke_root = build_output / "session-smoke" +staged_library = smoke_root / "staged" / "Microsoft.SqlServer.CSharpExtensionTest.dll" +installed_library_dir = smoke_root / "installed" +shutil.rmtree(smoke_root, ignore_errors=True) +staged_library.parent.mkdir(parents=True) +installed_library_dir.mkdir(parents=True) +shutil.copy2(test_output / staged_library.name, staged_library) +staged_library.chmod(stat.S_IRUSR | stat.S_IWUSR) extension.Init.argtypes = [ ctypes.c_char_p, @@ -35,7 +45,7 @@ extension.Init.restype = ctypes.c_short root = os.fsencode(build_output) -public_library_path = os.fsencode(test_output) +public_library_path = os.fsencode(installed_library_dir) private_library_path = os.fsencode(test_output / "nonexistent-private-library-path") empty = b"" result = extension.Init( @@ -65,6 +75,42 @@ class SqlGuid(ctypes.Structure): output_name = b"OutputDataSet" session_id = SqlGuid(1, 2, 3, (ctypes.c_ubyte * 8)(4, 5, 6, 7, 8, 9, 10, 11)) +extension.InstallExternalLibrary.restype = ctypes.c_short +extension.InstallExternalLibrary.argtypes = [ + SqlGuid, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_int, + ctypes.POINTER(ctypes.c_char_p), + ctypes.POINTER(ctypes.c_int), +] +library_name = os.fsencode(staged_library.name) +library_file = os.fsencode(staged_library) +library_install_directory = os.fsencode(installed_library_dir) +library_error = ctypes.c_char_p() +library_error_length = ctypes.c_int() +result = extension.InstallExternalLibrary( + session_id, + library_name, + len(library_name), + library_file, + len(library_file), + library_install_directory, + len(library_install_directory), + ctypes.byref(library_error), + ctypes.byref(library_error_length), +) +if result != 0: + message = library_error.value.decode(errors="replace") if library_error.value else "" + raise RuntimeError(f"CSharp library install failed with SQLRETURN {result}: {message}") + +installed_library = installed_library_dir / staged_library.name +if not installed_library.stat().st_mode & stat.S_IROTH: + raise RuntimeError(f"Installed library is not readable by the SQL execution identity: {installed_library}") + extension.InitSession.restype = ctypes.c_short extension.InitSession.argtypes = [ SqlGuid, @@ -122,4 +168,5 @@ class SqlGuid(ctypes.Structure): if result != 0: raise RuntimeError(f"CSharp extension Cleanup failed with SQLRETURN {result}") +shutil.rmtree(smoke_root) print(f"CSharp {configuration} extension load, session Execute, and Cleanup succeeded") \ No newline at end of file diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index fd1346d2..3893d52a 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -1007,7 +1007,7 @@ private static void InstallRawDll( "already exists in the install directory and is not owned by this library."); } - File.Copy(libFilePath, installedDllPath, false); + CopyLibraryFile(libFilePath, installedDllPath); // Track the raw-DLL install in a manifest too. This is what makes // ALTER from raw-DLL to ZIP work: the ZIP path's CheckForConflicts @@ -1283,7 +1283,7 @@ private static void ExtractContentToInstallDir( { continue; } - File.Copy(file, Path.Combine(installDir, Path.GetFileName(file)), false); + CopyLibraryFile(file, Path.Combine(installDir, Path.GetFileName(file))); } foreach (string dir in Directory.GetDirectories(contentRoot)) @@ -1310,7 +1310,20 @@ private static void CreateAlias( string alias = Path.Combine(installDir, aliasFileName); if (File.Exists(aliasSrc)) { - File.Copy(aliasSrc, alias, false); + CopyLibraryFile(aliasSrc, alias); + } + } + + private static void CopyLibraryFile(string source, string destination) + { + File.Copy(source, destination, false); + + if (!OperatingSystem.IsWindows()) + { + UnixFileMode mode = File.GetUnixFileMode(destination); + File.SetUnixFileMode( + destination, + mode | UnixFileMode.GroupRead | UnixFileMode.OtherRead); } } @@ -1505,7 +1518,7 @@ private static void CopyDirectory(string sourceDir, string destDir) // overwrite: false so that if filesystem state changed between the // conflict check and here (TOCTOU), we fail loud rather than silently // replacing a file belonging to another library. - File.Copy(file, destFile, false); + CopyLibraryFile(file, destFile); } foreach (string dir in Directory.GetDirectories(sourceDir))