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..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/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 c84b6cd2..a32c41e8 100644 --- a/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h +++ b/language-extensions/dotnet-core-CSharp/include/DotnetEnvironment.h @@ -10,13 +10,33 @@ //********************************************************************* #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 +#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,10 +66,10 @@ 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; + 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(), @@ -72,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/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..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/native/DotnetEnvironment.cpp b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp index bc2f8471..e7bf1fa7 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/DotnetEnvironment.cpp @@ -10,7 +10,14 @@ //********************************************************************* #include "DotnetEnvironment.h" #include "Logger.h" + +#ifdef _WIN32 #include "Windows.h" +#else +#include +#include +#endif + #include #include #include @@ -19,12 +26,14 @@ #include #include -#define STR(s) L ## s -#define CH(c) L ## c - 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 +44,7 @@ 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)) { } @@ -57,7 +66,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) { @@ -74,6 +83,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 +114,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 +139,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 +166,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 +193,27 @@ 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 + // 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) + { + 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"); @@ -190,6 +262,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,6 +274,14 @@ hostfxr_handle DotnetEnvironment::get_dotnet(const char_t *config_path){ params.dotnet_root = dotnet_root_buffer.data(); } } +#else + // 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); if (rc != 0 || cxt == nullptr) 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; 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..b1a135f4 --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/test/build/linux/run-dotnet-core-CSharp-extension-test.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Set environment variables +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Export ENL_ROOT so the test executable can resolve test_packages via getenv("ENL_ROOT"). +export 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..8a9fbc25 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,48 @@ 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); + + sm_installExternalLibraryFuncPtr = reinterpret_cast( + dlsym(sm_libHandle, "InstallExternalLibrary")); + EXPECT_TRUE(sm_installExternalLibraryFuncPtr != nullptr); + + sm_uninstallExternalLibraryFuncPtr = reinterpret_cast( + dlsym(sm_libHandle, "UninstallExternalLibrary")); + EXPECT_TRUE(sm_uninstallExternalLibraryFuncPtr != 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 { diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpLibraryTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpLibraryTests.cpp index ea163807..0c749a1f 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpLibraryTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpLibraryTests.cpp @@ -15,12 +15,39 @@ #include #include #include +#ifndef _WIN32 +#include +#include +#include +#include +#endif using namespace std; namespace fs = std::filesystem; namespace ExtensionApiTest { + // Helper: get the full path to the running test executable, cross-platform. + // + static fs::path GetExecutablePath() + { +#ifdef _WIN32 + char path[MAX_PATH + 1] = { 0 }; + GetModuleFileName(NULL, path, MAX_PATH); + return fs::path(path); +#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'; + return fs::path(path); +#endif + } + // Helper: get path to test_packages directory // static string GetPackagesPath() @@ -36,9 +63,7 @@ namespace ExtensionApiTest else { // Fallback: navigate from executable path - char path[MAX_PATH + 1] = { 0 }; - GetModuleFileName(NULL, path, MAX_PATH); - fs::path buildOutputPath = fs::path(path).parent_path().parent_path().parent_path().parent_path(); + fs::path buildOutputPath = GetExecutablePath().parent_path().parent_path().parent_path().parent_path(); result = (buildOutputPath.parent_path().parent_path() / "language-extensions" / "dotnet-core-CSharp" / "test" / "test_packages").string(); } @@ -50,9 +75,7 @@ namespace ExtensionApiTest // static string CreateInstallDir() { - char path[MAX_PATH + 1] = { 0 }; - GetModuleFileName(NULL, path, MAX_PATH); - string installDir = (fs::path(path).parent_path() / "testInstallLibs").string(); + string installDir = (GetExecutablePath().parent_path() / "testInstallLibs").string(); if (fs::exists(installDir)) { @@ -95,7 +118,13 @@ namespace ExtensionApiTest { if (libError != nullptr) { +#ifdef _WIN32 LocalFree(reinterpret_cast(libError)); +#else + // On Linux, .NET's Marshal.AllocHGlobal is backed by malloc, + // so the matching deallocator is free(). + free(libError); +#endif } } @@ -2017,9 +2046,7 @@ namespace ExtensionApiTest // Construct a path that does NOT exist. Use a sibling of the // standard testInstallLibs path so we know the parent directory // is writable but the target itself is absent. - char path[MAX_PATH + 1] = { 0 }; - GetModuleFileName(NULL, path, MAX_PATH); - fs::path missing = fs::path(path).parent_path() / "testInstallLibs-missing"; + fs::path missing = GetExecutablePath().parent_path() / "testInstallLibs-missing"; if (fs::exists(missing)) { fs::remove_all(missing); diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp index c2968d51..9b337eb5 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp @@ -72,9 +72,15 @@ namespace ExtensionApiTest } } +#ifdef _WIN32 #define RESOLVE_SET_HOST_CALLBACKS() \ reinterpret_cast( \ GetProcAddress(sm_libHandle, "SetHostCallbacks")) +#else +#define RESOLVE_SET_HOST_CALLBACKS() \ + reinterpret_cast( \ + dlsym(sm_libHandle, "SetHostCallbacks")) +#endif //---------------------------------------------------------------------------------------------- // Name: SetHostCallbacks_SymbolIsExported