Skip to content

Enable and package the C# language extension on Linux - #110

Draft
SicongLiu2000 wants to merge 45 commits into
mainfrom
dev/sicongliu/csharp-linux-rebased
Draft

Enable and package the C# language extension on Linux#110
SicongLiu2000 wants to merge 45 commits into
mainfrom
dev/sicongliu/csharp-linux-rebased

Conversation

@SicongLiu2000

Copy link
Copy Markdown
Contributor

Why

DsMainDev C# Linux E2E was enabled on six x64 distros, but every board failed in Setup because the official data-sql-language-extensions-linux 1.0.0-CI-master-20260813.1 package contains R/Python/Java/ONNX only. Consequently mssql-server has no /opt/mssql-extensibility/lib/dotnet-core-CSharp-lang-extension-linux.zip to install.

This branch rebases the Linux implementation from #64 onto current main and carries forward the proven runtime/package fixes from dev/sicongliu/linuxTest without that branch's obsolete v2-host reset.

What

  • Linux build and restore scripts for the .NET C# extension.
  • nethost/hostfxr discovery and framework-dependent runtime layout that survives SQL archive extraction.
  • Linux native unit-test support.
  • .zip output required by CREATE EXTERNAL LANGUAGE, while retaining .tar.gz compatibility output.
  • GCC 11 plus static libstdc++/libgcc for Ubuntu 22.04 / RHEL compatibility.
  • Preserves current main's decimal support, NVARCHAR work, XEvent logging, and v3 SetHostCallbacks implementation.

Validation

  • Rebased cleanly onto main (9a897b70).
  • Current main ancestry includes decimal support commit 7992ab3.
  • Verified v3 SetHostCallbacks native/managed code remains present.
  • Verified archive script emits dotnet-core-CSharp-lang-extension-linux.zip.
  • DsMainDev PR build 232992183 passed before product execution.
  • Current six-distro PVS runs all fail at the same missing-file Setup step; no C# test body executes. Once the PR package is available, mssql-server will consume it, copy the archive, rebuild all six images, and rerun the matrix.

Related: #64

SQL Server on Linux expects a .tar.gz file instead of a .zip when using
CREATE EXTERNAL LANGUAGE.
We will use the nethost to locate the system's installed hostfxr, as
using a local libhostfxr.so breaks the .NET runtime lookup.

nethost contains the logic needed to find the .NET root based on the
DOTNET_ROOT environment variable or the install_location configuration
file, which we will require for .NET runtime lookup on diverging Linux
distributions (e.g. RHEL and Debian packages not installing .NET in the
same base directories).
- Replace PowerShell build scripts with bash (build, archive, restore)
- Remove checked-in libnethost.a; fetch at build time via restore-packages.sh
- Add libnethost.a existence guard in build script
- Add convert_string() abstraction in DotnetEnvironment
- Conditional nethost.h include (Linux only), HOSTFXR_PATH_BUFFER_SIZE constant
- Add LOG_ERROR on hostfxr/dlopen/dlsym failures (release-safe, not just assert)
- Guard S_OK/E_FAIL macros (#ifndef) on non-Windows
- Add Linux test build/run scripts and CMake platform support
- Add -fshort-wchar and replace std::wstring with vector<wchar_t>/raw wchar_t* in
  test code to avoid ABI mismatch under -fshort-wchar on Linux
- Add platform-conditional HintPath in test csproj
- Add Linux dlopen/dlsym equivalents in test API code; throw on readlink failure

(cherry picked from commit 2497aa2)
Bundle the entire .NET runtime (hostfxr, coreclr, shared framework) in
the extension tar.gz so it works in containers without a system .NET
installation.

Changes:
- build-dotnet-core-CSharp-extension.sh: dotnet build -> dotnet publish
  --self-contained -r linux-x64
- restore-packages.sh: add -r linux-x64 to dotnet restore for runtime
  pack download during network-enabled phase
- DotnetEnvironment.cpp: pass assembly_path to get_hostfxr_path() for
  self-contained hostfxr discovery; default dotnet_root to extension
  directory when DOTNET_ROOT env var is not set

Fixes: Extension Init() failed with -8223344 (hostfxr not found)
Load libhostfxr.so from the extension directory (self-contained deployment)
rather than using get_hostfxr_path(), which may find an incompatible older
hostfxr (e.g. .NET Core 3.x shipped with SQL Server). This mirrors the
Windows approach of loading hostfxr.dll from the extension root.

Falls back to nethost discovery for framework-dependent deployments where
the bundled libhostfxr.so is not present.

Fixes: hostfxr_initialize_for_runtime_config failed with 0x80008093
(Initialization for self-contained components is not supported)
Adds [DIAG] cout messages to load_hostfxr() and get_dotnet() to trace:
- m_root_path value
- Whether bundled libhostfxr.so is found via access()
- Which hostfxr path is actually loaded (bundled vs nethost fallback)
- config_path and dotnet_root values passed to hostfxr
- hostfxr_initialize_for_runtime_config return code
…d Linux deployment

hostfxr_initialize_for_runtime_config rejects self-contained runtimeconfig.json
files (with 'includedFrameworks') returning error 0x80008093 (HostIncompatibleConfig).
This is by design in .NET - that API only supports framework-dependent components.

For self-contained deployments on Linux, use hostfxr_initialize_for_dotnet_command_line
instead, which supports both framework-dependent and self-contained apps (.NET 5+).
The hdt_load_assembly_and_get_function_pointer delegate works from command-line
initialized contexts since .NET 5.

Validated locally in WSL with bundled .NET 8.0.25 hostfxr - returns 0x00000000.
…ned .NET hosting

- Use hostfxr_initialize_for_dotnet_command_line instead of
  hostfxr_initialize_for_runtime_config for self-contained deployments
- Use hdt_get_function_pointer (type 6, Default ALC) instead of
  hdt_load_assembly_and_get_function_pointer (type 5, Isolated ALC)
  to resolve AssemblyLoadContext type cast failures
- Add get_function_pointer_fn typedef and hdt_get_function_pointer enum
- Dual-path call_managed_method: tries Default ALC first, falls back
  to Isolated ALC for backward compatibility
…nt runtimeconfig

hostfxr_initialize_for_dotnet_command_line returns 0x80008093 (InvalidConfigFile)
because .NET does not support self-contained component initialization. Instead:
- Post-process runtimeconfig.json: replace includedFrameworks with framework
- Create shared/Microsoft.NETCore.App/<version>/ symlink to extension root
- Always use hostfxr_initialize_for_runtime_config (works for both modes)
- Include shared/ directory in tar.gz
- Fix double 0x prefix in to_hex_string
…ction

SQL Server's tar extraction does not preserve symbolic links when extracting
CREATE EXTERNAL LANGUAGE payloads. The shared/Microsoft.NETCore.App/<version>/
symlink (pointing to ../../../) was silently dropped, causing hostfxr to fail
with 0x80008096 (FrameworkMissingFailure) - 'No frameworks were found'.

Replace the directory symlink with hard links for each .dll and .so file.
Hard links are stored efficiently in tar (as link entries with zero additional
tar size) and are reliably extracted by all tar implementations - either as
real hard links or as file copies.
SQL Server's CREATE EXTERNAL LANGUAGE tar extraction silently drops
both symbolic links AND hard links. Use actual file copies (cp) to
ensure framework DLLs/SOs are always present after extraction.

Previous attempts:
- Symlinks (ln -sfn): dropped by tar extraction -> 0x80008096
- Hard links (ln): also dropped by tar extraction -> 0x80008096
- File copies (cp): regular file entries, always extracted correctly
…debugging

Log dotnet_root path, shared/ directory existence, framework version
directories, runtimeconfig.json content, and relevant environment
variables before calling hostfxr_initialize_for_runtime_config.
This helps diagnose the 0x80008096 (FrameworkMissingFailure) error.
hostfxr requires Microsoft.NETCore.App.deps.json to recognize a framework
version directory as valid. The build script was only copying *.dll and *.so,
missing the .deps.json and .version files that hostfxr needs for framework
resolution. This caused error 0x80008096 (FrameworkMissingFailure).
The .deps.json file only exists in the .NET SDK's shared/Microsoft.NETCore.App/
directory, NOT in the self-contained publish output. hostfxr requires this file
to recognize a framework version as valid. Without it, hostfxr reports
FrameworkMissingFailure (0x80008096) even when all DLLs/SOs are present.
…aged_method

The get_dotnet_load_assembly function previously returned immediately after
obtaining hdt_get_function_pointer without also obtaining
hdt_load_assembly_and_get_function_pointer. This meant that when the
Default ALC failed to resolve Microsoft.SqlServer.CSharpExtension by name
(as happens in component-host scenarios on Linux), the fallback path in
call_managed_method was never available because
m_load_assembly_and_get_function_pointer was null.

Fix: Always obtain both delegates. The explicit-path delegate
(hdt_load_assembly_and_get_function_pointer) serves as a reliable fallback
when name-based resolution via hdt_get_function_pointer fails.

Also: switch diagnostic logging from LOG() (debug-only) to LOG_ERROR()
(always on) for key initialization checkpoints so failures are visible
in Release builds.
@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Validation update: internal package pipeline build 178433468 succeeded after adding C# restore/build/archive/staging and nuspec entries. Package data-sql-language-extensions-linux 1.0.0-PR-csharp-linux-package-20260827.2 physically contains both debug/release C# Linux zips. Downstream mssql-server validation is blocked on feed promotion permissions, not source/build correctness: AddPackage is denied on SqlUnified and SQLPAL-Scratch, and CreateFeed is denied for a temporary SQL Linux feed. Required next step is feed-owner promotion of the exact package (184,631,226 bytes; SHA-256 AC796751DD67BDACD9FDE77C05EEE7129A96BEBAC3B8A07074A2E0BD4EBB3337) into SQLPAL-Unified.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Six focused Linux PVS arms on drop 18.0.263-2-1842 all reached C# Setup, then exthost exited immediately before protocol initialization (one Failed Setup + one harness pass per distro; 0x80004004). Mounted Ubuntu 22.04 session 98c782be-6bf0-65c4-3c03-bcdf3768760c confirms the process launches and all child processes exit in the same second; SQL's later (0,0) abort is cleanup. The shipped native C# library has only libc in DT_NEEDED and embeds libstdc++ plus the GCC unwinder because both -static-libstdc++ and -static-libgcc remained after the final build switched to g++-11. The minimal fix removes those obsolete static flags: dynamic g++-11 stays within the RHEL 9/Ubuntu 22.04 GLIBCXX ceiling without loading a second runtime/unwinder into exthost. Bash syntax and diff checks pass; new DSLE artifact validation will verify DT_NEEDED and rerun the same matrix.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Drop 18.0.263-2-1844 reproduced the same deterministic Setup abort on five completed distros after switching to dynamic g++-11. Each run still has Passed=1/Failed=1 (Setup), with no C# cases executing. Mounted Ubuntu 22 session a0d5cfc0-a2b1-2bee-3494-fba29d1cf766 shows exthost reports failure (SQL receives abort 1,1), but no CSharpExt constructor/Init diagnostic is retained, indicating failure before or during RTLD_NOW load. Registration selects libnativecsharpextension.so and all required v3 exports are present. Added a Linux hosted smoke gate that loads the release .so with RTLD_NOW and calls GetInterfaceVersion, Init, and Cleanup; both PR and Official pipelines now run it before packaging. Payload 34c9577, wrapper d7b31d405ed62215983eed7b22f9d257d50fdefd, diagnostic build 178634582.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Hosted smoke build 178634582 passed dlopen, interface v3, bundled hostfxr, managed Init, and Cleanup. This proves package/runtime integrity outside SQL. The remaining PVS-only difference is ambient process environment. DotnetEnvironment's comment required bundled deployments to use their own extension root, but implementation prioritized ambient DOTNET_ROOT even after selecting bundled libhostfxr.so. Fixed precedence so self-contained mode always uses m_root_path; framework-dependent mode still honors ambient DOTNET_ROOT. The smoke test now poisons DOTNET_ROOT with /nonexistent/ambient-dotnet-root, making this invariant executable. Payload 87a74ed, wrapper 6456fa40ae57631d65aff1b4d4cfe6053df28890, validation build 178638295.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Final drop 1845 still fails Setup on five normal distros. Mounted Ubuntu 22 session 5a8c8b42-7a32-0801-9482-bfeef0a51183 proves the library now loads, bundled root is selected, hostfxr initializes, managed Init succeeds, and InstallExternalLibrary succeeds (ErrorCode 0). PVS uses the DEBUG archive: runtimeconfig 258 bytes, managed DLL 62,976 bytes. The hosted smoke tested only RELEASE: runtimeconfig 329 bytes, managed DLL 59,392 bytes. Updated smoke-test-extension.py to require debug|release and pipeline invokes each in a separate process. Payload b4f47bb; wrapper e41e1ce34aaa26ed71e513ef51e2d5ff3922b115; diagnostic build 178738077.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Both Debug and Release load/Init/Cleanup smoke passed in build 178738077. Final PVS logs show the bundled runtime and managed Init succeed; external E2E DLL installation also succeeds. The remaining untested boundary is user assembly loading/execution: PVS script Microsoft.SqlServer.CSharpExtensionE2ETest.E2EPassThroughExecutor runs from an installed DLL. Added a session smoke using the existing Microsoft.SqlServer.CSharpExtensionTest.CSharpTestExecutor: build its managed/native test project, pass that output as publicLibraryPath, then call InitSession, zero-row Execute, CleanupSession, and Cleanup for Debug and Release. Payload 90ca99a; wrapper c1dd2a45350c42671e88b7c896220f9cc76ab839; build 178744132.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Session smoke build 178757020 reproduced the PVS failure in both Debug and Release. Root cause: hostfxr fallback loads Microsoft.SqlServer.CSharpExtension in an IsolatedComponentLoadContext, but DllUtils used Assembly.LoadFrom for user executors, loading them into the default context. The executor's AbstractSqlServerExtensionExecutor therefore had a different assembly identity and cast failed. The same scan also attempted to load PDB/JSON files as assemblies. Fix b6268a9 loads user DLLs through the AssemblyLoadContext owning AbstractSqlServerExtensionExecutor and filters discovery to .dll. Wrapper c2bbda362b5f1a1d8c1cceb553ceabb98e53ff68; direct session-smoke validation build 178764534.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Latest Linux PVS diagnosis: five distros reached C# Execute, then failed in DllUtils.CreateDllList with DirectoryNotFoundException for /home/mssql_satellite/externallibrariessandboxprivatepath. SQL passes the optional private path even when no private library is mounted. Commit 5036691 now skips absent optional library roots and makes the permanent Debug/Release session smoke pass a nonempty nonexistent private path. GitHub checks are green; internal DSLE validation is running as build 178780126.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Final drop 18.0.263-2-1849 results: Ubuntu 22.04, Ubuntu 24.04, RHEL 9, RHEL 10, and Azure Linux 3 x64 each completed 26 PlatformLinux C# E2E records with 26 passed, 0 failed, 0 not-executed, and 0 inconclusive. This validates the installed-library permission fix in real SQL execution. Ubuntu 26.04 failed before SQL startup and executed no C# tests; a matched official-drop control (18.0.263-2-115) reproduced the identical startup timeout. Six-distro qualification is blocked on the Ubuntu 26 image/startup owner, not on this payload.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

QUALIFIED on Linux x64. Final chain: payload 4d0809d -> wrapper 8518c24d52d8a8cbc0ea02c4092cfd2f95c4386f -> package 1.0.0-PR-csharp-linux-package-20260829.4 (182523934 bytes, SHA-256 7458DA8EB9C4020CDB2DB117927BE983502015D62E7F053CE99CBFAD4BD1A367) -> consumer a58ccea4b951fd49f4e272743bdb49496818227a -> build 233577302 -> drop 18.0.263-2-1855. Ubuntu 22/24/26, RHEL 9/10, and Azure Linux 3 x64 each executed 26 PlatformLinux C# E2E records: 26 passed, 0 failed, 0 not-executed, 0 inconclusive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants