diff --git a/language-extensions/dotnet-core-CSharp/README.md b/language-extensions/dotnet-core-CSharp/README.md index 4fb63dfd..dc732e57 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/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/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. ## Testing (Optional) 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..d5d0cc1d --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/build-dotnet-core-CSharp-extension.sh @@ -0,0 +1,178 @@ +#!/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. + # 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=( + -shared + -fPIC + -fshort-wchar + -std=c++17 + -o libnativecsharpextension.so + "-I$DOTNET_NATIVE_INCLUDE" + "-I$EXTENSION_HOST_INCLUDE" + # 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 + 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 + + # 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 publish \ + "$DOTNET_MANAGED_SRC/Microsoft.SqlServer.CSharpExtension.csproj" \ + -c "$BUILD_CONFIGURATION" \ + -r linux-x64 \ + --self-contained \ + -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 file copies. + # hostfxr resolves frameworks at /shared/// + # which must contain the runtime DLLs and native libraries. + # + # 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" + # 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 + # 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 + + 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..9128e551 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/create-dotnet-core-CSharp-extension-tar.sh @@ -0,0 +1,87 @@ +#!/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 + + # 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 archive extraction does not preserve + # symlinks or hard links. + if [ -d "$BUILD_OUTPUT/shared" ]; then + FILES_TO_COMPRESS+=("shared") + fi + + # 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 + + # 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 + + echo "Success: Compressed 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 new file mode 100644 index 00000000..ceb63bb8 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/restore-packages.sh @@ -0,0 +1,116 @@ +#!/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 + +# 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. +# +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 (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" -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..." + 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" -r linux-x64 + 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/build/linux/smoke-test-extension.py b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py new file mode 100644 index 00000000..25cbf0aa --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/build/linux/smoke-test-extension.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +import ctypes +import os +import shutil +import stat +import sys +from pathlib import Path + + +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) + +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" + +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, + 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) +public_library_path = os.fsencode(installed_library_dir) +private_library_path = os.fsencode(test_output / "nonexistent-private-library-path") +empty = b"" +result = extension.Init( + empty, + 0, + root, + len(root), + public_library_path, + len(public_library_path), + private_library_path, + len(private_library_path), +) +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.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, + 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}") + +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/include/DotnetEnvironment.h b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h index c84b6cd2..4ceed2b8 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -10,13 +10,34 @@ //********************************************************************* #pragma once +#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 +#include "Logger.h" +#ifdef _WIN32 #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; using string_t = std::basic_string; @@ -46,19 +67,54 @@ 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; - const string_t ManagedExtensionMethod = to_utf16_str(method_name); - const string_t DelegateTypeName = ManagedExtensionName + STR(".CSharpExtension+") + to_utf16_str(method_name) + 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); + const string_t ManagedExtensionMethod = convert_string(method_name); + const string_t DelegateTypeName = ManagedExtensionName + STR(".CSharpExtension+") + ManagedExtensionMethod + STR("Delegate, ") + ManagedExtensionName; + + 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) + { + 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, 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(), + ManagedExtensionType.c_str(), + ManagedExtensionMethod.c_str(), + 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; } @@ -70,11 +126,20 @@ 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; + + // 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); - // Convert utf8_str to utf16_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. // @@ -92,9 +157,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/Logger.h b/language-extensions/dotnet-core-CSharp/include/Logger.h index 2a96ec0a..3c389ebf 100644 --- a/language-extensions/dotnet-core-CSharp/include/Logger.h +++ b/language-extensions/dotnet-core-CSharp/include/Logger.h @@ -8,9 +8,10 @@ // Wrapper class around logging to standardize logging messages and errors. // //********************************************************************* +#pragma once + #include #include -#include using namespace std; #define LOG(msg) Logger::Log(msg) 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/include/nativecsharpextension.h b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h index 4fe35739..3320dcb3 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/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/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)) 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..4d669729 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; @@ -36,13 +37,19 @@ 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) + + 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) { @@ -79,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)); } diff --git a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index bc2f8471..7474f9e5 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -10,7 +10,15 @@ //********************************************************************* #include "DotnetEnvironment.h" #include "Logger.h" + +#ifdef _WIN32 #include "Windows.h" +#else +#include +#include +#include +#endif + #include #include #include @@ -19,12 +27,19 @@ #include #include -#define STR(s) L ## s -#define CH(c) L ## c +#ifndef _WIN32 +#include +#include +#endif 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 // @@ -35,7 +50,13 @@ 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) : m_root_path(convert_string(language_path)), + m_init_fptr(nullptr), + 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) { } @@ -57,16 +78,20 @@ 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) { return E_FAIL; } - m_load_assembly_and_get_function_pointer = get_dotnet_load_assembly(cxt); + // 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_load_assembly_and_get_function_pointer == nullptr) + if (m_get_function_pointer == nullptr && m_load_assembly_and_get_function_pointer == nullptr) { return E_FAIL; } @@ -74,6 +99,23 @@ short DotnetEnvironment::Init() return S_OK; } +//-------------------------------------------------------------------------------------------------- +// 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 // @@ -88,6 +130,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 @@ -99,7 +142,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(); } @@ -112,7 +155,20 @@ string DotnetEnvironment::to_hex_string(int value) void* DotnetEnvironment::load_library(const char_t *path) { LOG("DotnetEnvironment::load_library"); +#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; } @@ -126,13 +182,26 @@ void* DotnetEnvironment::load_library(const char_t *path) void* DotnetEnvironment::get_export(void *h, const char *name) { LOG("DotnetEnvironment::get_export"); +#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; } //-------------------------------------------------------------------------------------------------- -// Name: DotnetEnvironment::get_export +// Name: DotnetEnvironment::load_hostfxr // // Description: // Load hostfxr and get desired exports @@ -140,8 +209,40 @@ void* DotnetEnvironment::get_export(void *h, const char *name) bool DotnetEnvironment::load_hostfxr() { LOG("DotnetEnvironment::load_hostfxr"); +#ifdef _WIN32 string_t hostfxr_location = m_root_path + STR("\\hostfxr.dll"); +#else + // 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"); + + // If the bundled hostfxr doesn't exist (framework-dependent deployment), + // fall back to nethost discovery. + if (access(hostfxr_location.c_str(), F_OK) != 0) + { + 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) + { + LOG_ERROR("Failed to locate hostfxr via nethost"); + return false; + } + hostfxr_location = string_t(buffer); + } + else + { + m_is_self_contained = true; + } +#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"); @@ -153,26 +254,57 @@ bool DotnetEnvironment::load_hostfxr() // Name: DotnetEnvironment::get_dotnet_load_assembly // // Description: -// load assembly function pointer from the path. +// 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. // -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 - int rc = m_get_delegate_fptr( + // 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 (rc != 0 || load_assembly_and_get_function_pointer == nullptr) + 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 delegate failed: " + to_hex_string(rc)); + 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); - return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; + + if (rc == 0 && get_fn_ptr != nullptr) + { + return (get_function_pointer_fn)get_fn_ptr; + } + + if (m_load_assembly_and_get_function_pointer == nullptr) + { + LOG_ERROR("Both get_function_pointer and load_assembly_and_get_function_pointer delegates failed"); + } + + // Return nullptr to signal that the caller should use m_load_assembly_and_get_function_pointer + return nullptr; } //-------------------------------------------------------------------------------------------------- @@ -190,6 +322,7 @@ 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 DWORD requiredSize = GetEnvironmentVariableW(L"DOTNET_ROOT", nullptr, 0); std::vector dotnet_root_buffer; @@ -201,14 +334,113 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ params.dotnet_root = dotnet_root_buffer.data(); } } +#else + // On Linux, for self-contained (bundled) deployments, set dotnet_root to + // 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 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, honor an ambient DOTNET_ROOT when present. + if (m_is_self_contained) + { + params.dotnet_root = m_root_path.c_str(); + } + else + { + params.dotnet_root = getenv("DOTNET_ROOT"); + } +#endif + +#ifndef _WIN32 + // Concise diagnostic logging for runtime initialization. + // Uses LOG_ERROR so it's visible in Release builds. + if (params.dotnet_root != nullptr) + { + LOG_ERROR("CSharpExt: dotnet_root=" + std::string(params.dotnet_root) + " self_contained=" + std::to_string(m_is_self_contained)); + + // 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) + { + LOG_ERROR("CSharpExt: shared/ NOT found at " + shared_dir + " (errno=" + std::to_string(errno) + ")"); + } + else + { + std::string app_dir = shared_dir + "/Microsoft.NETCore.App"; + if (stat(app_dir.c_str(), &st) != 0) + { + LOG_ERROR("CSharpExt: Microsoft.NETCore.App/ NOT found (errno=" + std::to_string(errno) + ")"); + } + else + { + DIR *d = opendir(app_dir.c_str()); + if (d) + { + struct dirent *de; + while ((de = readdir(d)) != nullptr) + { + if (de->d_name[0] != '.') + { + 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; + 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; + } + closedir(vd); + } + LOG_ERROR("CSharpExt: framework " + std::string(de->d_name) + " files=" + std::to_string(fcount) + " deps.json=" + std::to_string(has_deps)); + } + } + closedir(d); + } + } + } + + // Verify runtimeconfig.json exists + std::string cfg(config_path); + struct stat cfgst; + if (stat(cfg.c_str(), &cfgst) != 0) + LOG_ERROR("CSharpExt: runtimeconfig NOT found: " + cfg); + else + LOG_ERROR("CSharpExt: runtimeconfig OK: " + cfg + " size=" + std::to_string(cfgst.st_size)); + + // 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)); + } + else + { + LOG_ERROR("CSharpExt: dotnet_root is nullptr"); + } +#endif int rc = m_init_fptr(config_path, ¶ms, &cxt); + 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; } diff --git a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp index 89d85852..e8f0194d 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/Logger.cpp @@ -10,12 +10,157 @@ //********************************************************************* #include #include -#include -#include +#include +#include +#include +#include +#include #include "Logger.h" +#ifndef _WIN32 +#include +#include +#include +#include +#endif + using namespace std; +namespace +{ + // Diagnostic log file paths - try multiple locations so at least one + // gets captured by the test infrastructure. + // + // 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) + // + // 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. + // + // 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", + nullptr + }; + + 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(s). + // 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 + // 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) + { + try + { + std::ofstream f(*p, std::ios::app); + if (f.is_open()) + { + f << line << std::endl; + } + } + catch (...) + { + // ignore - try next path + } + } +#endif + } +} + +#ifndef _WIN32 +// 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[] = { + "/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", + 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() +{ + 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 // @@ -33,9 +178,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 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 {