diff --git a/.github/workflows/windows-cross-compile.yml b/.github/workflows/windows-cross-compile.yml index 26f80beda12..791fe526de3 100644 --- a/.github/workflows/windows-cross-compile.yml +++ b/.github/workflows/windows-cross-compile.yml @@ -15,6 +15,10 @@ on: - 'vm/**' - 'Ports/WindowsPort/**' - 'maven/windows/**' + - 'scripts/gen-windows-uwp-stubs.py' + - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java' + - 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java' + - 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml' - '!vm/**/README.md' - '!vm/**/readme.md' - '!vm/**/docs/**' @@ -25,6 +29,10 @@ on: - 'vm/**' - 'Ports/WindowsPort/**' - 'maven/windows/**' + - 'scripts/gen-windows-uwp-stubs.py' + - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java' + - 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java' + - 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml' concurrency: # Only the latest revision needs validation. PR numbers keep forks isolated; @@ -155,13 +163,156 @@ jobs: test -f core/target/classes/com/codename1/ui/Form.class test -f windows/target/classes/com/codename1/impl/windows/WindowsImplementation.class + # A UWP (Xbox Developer Mode) build compiles the desktop-only natives + # out and links generated stubs in their place; a native added to one of + # those files without regenerating would fail only the UWP link below. + - name: UWP stubs are current + run: python3 scripts/gen-windows-uwp-stubs.py --check + + # Builds the desktop exe and the UWP one (app partition, WindowsApp.lib + # only, /APPCONTAINER): the UWP link is the gate that every Win32 call the + # port makes in that build exists inside the app container. - name: Cross-compile the native Windows .exe (clang-cl + xwin) env: CN1_XWIN_SYSROOT: ${{ runner.temp }}/xwin-sdk CN1_CLANG_CL: clang-cl CN1_LLVM_RC: llvm-rc + CN1_UWP_EXE_OUT: ${{ github.workspace }}/uwp-smoke/app.exe run: | cd vm JAVA_HOME="$JDK_8_HOME" mvn -B -pl tests -am test \ '-Dtest=CleanTargetIntegrationTest#crossCompilesWindowsExeWithXwin' \ '-Dsurefire.failIfNoSpecifiedTests=false' + test -f "$CN1_UWP_EXE_OUT" + + # The development certificate windows.xbox builds create, made by the + # builder's own openssl commands; the Windows job signs with it, so a + # certificate Windows refuses fails there. + - name: Create the Xbox development certificate (WindowsNativeBuilder) + env: + CN1_XBOX_DEVCERT_OUT: ${{ github.workspace }}/uwp-smoke/devcert + RETRY_ONLY_MATCHING: transient + RETRY_ATTEMPTS: '3' + RETRY_DELAY_SECONDS: '60' + run: | + set -euo pipefail + mkdir -p ~/.codenameone + cp maven/UpdateCodenameOne.jar ~/.codenameone/ + cd maven + JAVA_HOME="$JDK_8_HOME" bash ../scripts/ci/retry.sh mvn -B -pl build-engine -am install \ + -DskipTests -Dmaven.javadoc.skip=true -Plocal-dev-javase + JAVA_HOME="$JDK_8_HOME" mvn -B -pl build-engine test -Plocal-dev-javase \ + '-Dtest=WindowsNativeBuilderArchTest' -Dsurefire.failIfNoSpecifiedTests=false + test -s "$CN1_XBOX_DEVCERT_OUT/dev.pfx" + test -s "$CN1_XBOX_DEVCERT_OUT/dev.cer" + + # The manifest is pinned to WindowsNativeBuilder.buildXboxAppxManifest by + # WindowsNativeBuilderArchTest, so the run below installs what windows.xbox + # builds ship. + - name: Upload the UWP smoke app + uses: actions/upload-artifact@v4 + with: + name: uwp-smoke + path: | + uwp-smoke/app.exe + uwp-smoke/devcert/dev.pfx + uwp-smoke/devcert/dev.cer + maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml + if-no-files-found: error + retention-days: 3 + + # Phase 1's proof without a console: the cross-built UWP app is packed, signed, + # installed and launched as a real UWP activation (app container, CoreWindow, + # swap chain) on a Windows runner. The app writes a marker into its package + # LocalState once its first form has painted; no marker fails the job. + uwp-run: + name: install + launch the UWP package (Windows x64) + needs: cross-compile + runs-on: windows-latest + timeout-minutes: 20 + steps: + - name: Download the UWP smoke app + uses: actions/download-artifact@v4 + with: + name: uwp-smoke + path: uwp-smoke + + - name: Pack, sign and install + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $root = Resolve-Path uwp-smoke + $layout = Join-Path $env:RUNNER_TEMP 'uwp-layout' + New-Item -ItemType Directory -Force (Join-Path $layout 'Assets') | Out-Null + Copy-Item (Join-Path $root 'uwp-smoke/app.exe') (Join-Path $layout 'app.exe') + $manifest = Get-ChildItem -Recurse $root -Filter 'xbox-smoke-AppxManifest.xml' | Select-Object -First 1 + Copy-Item $manifest.FullName (Join-Path $layout 'AppxManifest.xml') + Add-Type -AssemblyName System.Drawing + foreach ($logo in 'StoreLogo','Square150x150Logo','Square44x44Logo','SplashScreen') { + $bmp = New-Object System.Drawing.Bitmap 150,150 + $g = [System.Drawing.Graphics]::FromImage($bmp); $g.Clear([System.Drawing.Color]::SteelBlue); $g.Dispose() + $bmp.Save((Join-Path $layout "Assets/$logo.png"), [System.Drawing.Imaging.ImageFormat]::Png); $bmp.Dispose() + } + $kit = Get-ChildItem 'C:/Program Files (x86)/Windows Kits/10/bin/*/x64/makeappx.exe' | + Sort-Object FullName -Descending | Select-Object -First 1 + $bin = $kit.DirectoryName + $msix = Join-Path $env:RUNNER_TEMP 'UwpSmoke.msix' + & "$bin/makeappx.exe" pack /o /d $layout /p $msix + if ($LASTEXITCODE -ne 0) { throw "makeappx failed" } + # Signed with the certificate WindowsNativeBuilder created (subject + # CN=UwpSmoke, the manifest Publisher; empty PKCS#12 password). + $pfx = Join-Path $root 'uwp-smoke/devcert/dev.pfx' + $cer = Join-Path $root 'uwp-smoke/devcert/dev.cer' + & "$bin/signtool.exe" sign /fd SHA256 /f $pfx $msix + if ($LASTEXITCODE -ne 0) { throw "signtool rejected the builder's development certificate" } + Import-Certificate -FilePath $cer -CertStoreLocation 'Cert:/LocalMachine/TrustedPeople' | Out-Null + $unlock = 'HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion/AppModelUnlock' + New-Item -Force $unlock | Out-Null + Set-ItemProperty $unlock AllowAllTrustedApps 1 -Type DWord + Set-ItemProperty $unlock AllowDevelopmentWithoutDevLicense 1 -Type DWord + Add-AppxPackage -Path $msix + Get-AppxPackage com.codename1.uwpsmoke | Format-List Name,PackageFamilyName,InstallLocation,SignatureKind + + - name: Launch and wait for the first frame + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $pkg = Get-AppxPackage com.codename1.uwpsmoke + $pfn = $pkg.PackageFamilyName + $state = Join-Path $env:LOCALAPPDATA "Packages/$pfn/LocalState" + Start-Process "shell:AppsFolder\$pfn!App" + $marker = $null + for ($i = 0; $i -lt 90 -and -not $marker; $i++) { + Start-Sleep -Seconds 1 + if (Test-Path $state) { + $marker = Get-ChildItem -Recurse $state -Filter 'uwp-smoke.txt' -ErrorAction SilentlyContinue | Select-Object -First 1 + } + } + Add-Type -AssemblyName System.Drawing, System.Windows.Forms + $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height + $g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size); $g.Dispose() + New-Item -ItemType Directory -Force uwp-run | Out-Null + $bmp.Save('uwp-run/desktop.png', [System.Drawing.Imaging.ImageFormat]::Png) + Get-Process app -ErrorAction SilentlyContinue | Format-Table Id,ProcessName,StartTime + if (-not $marker) { + Get-WinEvent -LogName 'Microsoft-Windows-AppModel-Runtime/Admin' -MaxEvents 20 -ErrorAction SilentlyContinue | + Format-List TimeCreated,Id,Message + Get-WinEvent -FilterHashtable @{LogName='Application'; Id=1000,1001} -MaxEvents 5 -ErrorAction SilentlyContinue | + Format-List TimeCreated,Message + if (Test-Path $state) { Get-ChildItem -Recurse $state | Format-Table FullName,Length } + throw "the UWP app wrote no marker in 90s: it did not reach its first frame" + } + $text = Get-Content $marker.FullName -Raw + Write-Host "marker: $text" + if ($text -notmatch '^CN1-UWP-STARTED [1-9][0-9]*x[1-9][0-9]*') { throw "unexpected marker: $text" } + Get-Process app -ErrorAction SilentlyContinue | Stop-Process -Force + + - name: Upload the desktop capture + if: always() + uses: actions/upload-artifact@v4 + with: + name: uwp-run-capture + path: uwp-run/ + if-no-files-found: ignore + retention-days: 7 diff --git a/Ports/WindowsPort/nativeSources/cn1_windows.h b/Ports/WindowsPort/nativeSources/cn1_windows.h index b38a2d29aaa..828c9d1c815 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows.h +++ b/Ports/WindowsPort/nativeSources/cn1_windows.h @@ -59,6 +59,60 @@ #include #include +#if defined(CN1_WINDOWS_UWP) +/* + * UWP (Xbox Developer Mode) build: the app container's API partition drops a + * few Win32 calls that have a direct app-partition equivalent. They are mapped + * here once, so the shared file code reads the same in both builds. + * CreateFileW -> CreateFile2 (same handle, same flags, split differently); + * CharUpperW -> an invariant-locale LCMapStringEx in place. A template file is + * never used by this port, so CreateFile2 ignores that argument. + */ +static __inline HANDLE cn1UwpCreateFileW(LPCWSTR name, DWORD access, DWORD share, + LPSECURITY_ATTRIBUTES sa, DWORD disposition, DWORD flagsAndAttributes, HANDLE templateFile) { + CREATEFILE2_EXTENDED_PARAMETERS ext; + (void) templateFile; + ZeroMemory(&ext, sizeof(ext)); + ext.dwSize = sizeof(ext); + ext.dwFileAttributes = flagsAndAttributes & 0x0000FFFF; + ext.dwFileFlags = flagsAndAttributes & 0xFFF00000; + ext.dwSecurityQosFlags = flagsAndAttributes & 0x000F0000; + ext.lpSecurityAttributes = sa; + return CreateFile2(name, access, share, disposition, &ext); +} +#undef CreateFileW +#define CreateFileW cn1UwpCreateFileW + +static __inline HANDLE cn1UwpCreateFileA(LPCSTR name, DWORD access, DWORD share, + LPSECURITY_ATTRIBUTES sa, DWORD disposition, DWORD flagsAndAttributes, HANDLE templateFile) { + WCHAR wide[MAX_PATH]; + if (MultiByteToWideChar(CP_UTF8, 0, name, -1, wide, MAX_PATH) <= 0) { + SetLastError(ERROR_FILENAME_EXCED_RANGE); + return INVALID_HANDLE_VALUE; + } + return cn1UwpCreateFileW(wide, access, share, sa, disposition, flagsAndAttributes, templateFile); +} +#undef CreateFileA +#define CreateFileA cn1UwpCreateFileA + +static __inline LPWSTR cn1UwpCharUpperW(LPWSTR s) { + int len = (int) wcslen(s); + LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_UPPERCASE, s, len, s, len, NULL, NULL, 0); + return s; +} +#undef CharUpperW +#define CharUpperW cn1UwpCharUpperW + +/* Declared by the SDK for the desktop partition only, but exported by kernel32 + * on every Windows, the Xbox included. It is how a null dereference becomes a + * NullPointerException (cn1WinFaultToException), so the UWP build keeps it. + * Developer Mode sideloading does not run the Store's API certification. */ +#ifdef __cplusplus +extern "C" +#endif +WINBASEAPI PVOID WINAPI AddVectoredExceptionHandler(ULONG First, PVECTORED_EXCEPTION_HANDLER Handler); +#endif /* CN1_WINDOWS_UWP */ + #ifdef __cplusplus /* C++ method-call macros for the COBJMACROS-style call sites. */ #include "cn1_windows_comc.h" @@ -71,6 +125,16 @@ extern "C" { /* The ParparVM runtime is C; keep its declarations C-linked under C++. */ #include "cn1_globals.h" +#if defined(CN1_WINDOWS_UWP) +/* The app's local data folder (ApplicationData.Current.LocalFolder) into buf, + * NUL terminated. Returns 0 when it cannot be read. cn1_windows_corewindow.cpp. */ +int cn1UwpLocalFolder(WCHAR* buf, int capacity); +#endif + +/* COM, the fault handlers and the input ring, shared by every host's + * initDisplay. 0 when already initialised. cn1_windows_window.cpp. */ +int cn1WinInitRuntime(void); + /* ------------------------------------------------------------------ events */ /* diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp index 4dd96c2f93a..0ec1aa4fef2 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp @@ -22,7 +22,7 @@ */ /* Native virtual accessibility tree for the lightweight Win32 port. */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ /* xwin may provide a newer MSVC STL than the clang-cl installed by the Linux * cross-compile runner. This file uses only the long-stable string/vector diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_audiorec.c b/Ports/WindowsPort/nativeSources/cn1_windows_audiorec.c index 085e224dbb5..35256b6f0a2 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_audiorec.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_audiorec.c @@ -35,7 +35,7 @@ * and closes. */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include "cn1_windows.h" #include diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_camera.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_camera.cpp index 5edb9dcd2c3..b818ab4e95a 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_camera.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_camera.cpp @@ -36,7 +36,7 @@ * (negative stride); both are handled when packing into CN1's 0xAARRGGBB ints. */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #define WIN32_LEAN_AND_MEAN #include diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_chrome.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_chrome.cpp index 3e5db5c6bdf..86b5ba730df 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_chrome.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_chrome.cpp @@ -25,7 +25,7 @@ /* Integrated main-window title bar. The window keeps WS_OVERLAPPEDWINDOW: * DWM owns the real caption buttons and DefWindowProc owns system commands, * resize tracking, caption double-click and the window system menu. */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include "cn1_windows.h" #include #include diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_corewindow.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_corewindow.cpp new file mode 100644 index 00000000000..3d66c650d40 --- /dev/null +++ b/Ports/WindowsPort/nativeSources/cn1_windows_corewindow.cpp @@ -0,0 +1,663 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The UWP host of the Windows port: what cn1_windows_window.cpp is for a Win32 + * window, this is for a CoreWindow, which is the only kind of window the app + * container on an Xbox in Developer Mode will give an application. Compiled only + * with CN1_WINDOWS_UWP (build hint windows.xbox=true). + * + * Startup. The process cannot begin in the translated main: its first thread + * must go to CoreApplication::Run, which creates the window and then calls + * IFrameworkView::Run back on that same thread. The translator renames the Java + * main to cn1JavaMain in this build (ByteCodeClass), main below starts + * CoreApplication, and Run calls cn1JavaMain. From there the threading model is + * the desktop one unchanged: this thread runs Display.init and then owns the + * event pump (pumpMessages -> CoreDispatcher::ProcessEvents); the CoreWindow + * handlers push into the shared input ring; the EDT drains it and paints. + * + * Rendering. Direct2D draws through a device context on a D3D11 device rather + * than an HWND render target. Codename One repaints only dirty regions and + * relies on the rest of the surface surviving a present, which a flip-model + * swap chain does not do, so the EDT draws into a persistent target bitmap and + * each flush copies it to the back buffer and presents. Mutable images keep + * using WIC render targets from the same (multi-threaded) factory. + * + * Input. Pointer events arrive in view pixels and are scaled to physical ones. + * Keys arrive as virtual keys, and on an Xbox that includes the controller: + * a CoreWindow app is not in mouse mode, so the D-pad, the sticks and A/B/X/Y + * come through KeyDown as VirtualKey_Gamepad*. WindowsImplementation maps those + * to game actions. + */ + +#if defined(_WIN32) && defined(CN1_WINDOWS_UWP) + +#include "cn1_windows.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::ApplicationModel::Core; +using namespace ABI::Windows::ApplicationModel::Activation; +using namespace ABI::Windows::UI::Core; + +extern "C" int cn1JavaMain(int argc, char* argv[]); + +/* ------------------------------------------------------------------ state */ + +static int g_argc; +static char** g_argv; +static ComPtr g_window; +static ComPtr g_dispatcher; +static volatile LONG g_closed; + +/* Physical pixels per view pixel (2.0 on a 1080p Xbox output). Written on the + * UI thread before the EDT exists and on DPI change; read by input handlers on + * the same thread. */ +static float g_scale = 1.0f; + +static ComPtr g_d3d; +static ComPtr g_swapChain; +static ComPtr g_dc; +static ComPtr g_canvas; /* what the EDT draws into; survives presents */ +static ComPtr g_backBuffer; + +static float cn1UwpScale(void) { + ComPtr statics; + ComPtr info; + ComPtr info2; + DOUBLE raw = 1.0; + if (SUCCEEDED(GetActivationFactory(HStringReference( + RuntimeClass_Windows_Graphics_Display_DisplayInformation).Get(), &statics)) + && SUCCEEDED(statics->GetForCurrentView(&info)) + && SUCCEEDED(info.As(&info2)) + && SUCCEEDED(info2->get_RawPixelsPerViewPixel(&raw)) && raw > 0) { + return (float) raw; + } + return 1.0f; +} + +static int cn1UwpToPixels(float viewPixels) { + return (int) floorf(viewPixels * g_scale + 0.5f); +} + +/* The window's size in physical pixels. */ +static void cn1UwpWindowPixels(int* w, int* h) { + Rect bounds = {}; + if (g_window) { + g_window->get_Bounds(&bounds); + } + *w = cn1UwpToPixels(bounds.Width); + *h = cn1UwpToPixels(bounds.Height); + if (*w < 1) *w = 1; + if (*h < 1) *h = 1; +} + +/* ------------------------------------------------------------ rendering */ + +static HRESULT cn1UwpBindBackBuffer(void) { + ComPtr surface; + HRESULT hr = g_swapChain->GetBuffer(0, IID_PPV_ARGS(&surface)); + if (FAILED(hr)) { + return hr; + } + D2D1_BITMAP_PROPERTIES1 props = D2D1::BitmapProperties1( + D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE), 96.0f, 96.0f); + return g_dc->CreateBitmapFromDxgiSurface(surface.Get(), &props, &g_backBuffer); +} + +static HRESULT cn1UwpCreateCanvas(int w, int h) { + D2D1_BITMAP_PROPERTIES1 props = D2D1::BitmapProperties1(D2D1_BITMAP_OPTIONS_TARGET, + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), 96.0f, 96.0f); + g_canvas.Reset(); + HRESULT hr = g_dc->CreateBitmap(D2D1::SizeU((UINT32) w, (UINT32) h), NULL, 0, &props, &g_canvas); + if (SUCCEEDED(hr)) { + g_dc->SetTarget(g_canvas.Get()); + } + return hr; +} + +static int cn1UwpCreateDevice(int w, int h) { + static const D3D_FEATURE_LEVEL levels[] = { + D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_1 + }; + HRESULT hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, D3D11_CREATE_DEVICE_BGRA_SUPPORT, + levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, &g_d3d, NULL, NULL); + if (FAILED(hr)) { + hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_WARP, NULL, D3D11_CREATE_DEVICE_BGRA_SUPPORT, + levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, &g_d3d, NULL, NULL); + } + if (FAILED(hr)) { + cn1WindowsLog("initDisplay(uwp): no D3D11 device"); + return 0; + } + ComPtr dxgiDevice; + ComPtr adapter; + ComPtr dxgiFactory; + if (FAILED(g_d3d.As(&dxgiDevice)) || FAILED(dxgiDevice->GetAdapter(&adapter)) + || FAILED(adapter->GetParent(IID_PPV_ARGS(&dxgiFactory)))) { + cn1WindowsLog("initDisplay(uwp): no DXGI factory"); + return 0; + } + + /* Multi-threaded, as on the desktop: this thread owns the window, the EDT + * draws. The factory is kept as the ID2D1Factory every other file uses. */ + ComPtr factory; + D2D1_FACTORY_OPTIONS options = {}; + if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, __uuidof(ID2D1Factory1), &options, + (void**) factory.GetAddressOf()))) { + cn1WindowsLog("initDisplay(uwp): no Direct2D factory"); + return 0; + } + ComPtr d2dDevice; + if (FAILED(factory->CreateDevice(dxgiDevice.Get(), &d2dDevice)) + || FAILED(d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &g_dc))) { + cn1WindowsLog("initDisplay(uwp): no Direct2D device context"); + return 0; + } + /* 96 DPI: one Direct2D unit is one pixel, which is what Codename One draws in. */ + g_dc->SetDpi(96.0f, 96.0f); + cn1Win.d2dFactory = factory.Detach(); + + DXGI_SWAP_CHAIN_DESC1 desc = {}; + desc.Width = (UINT) w; + desc.Height = (UINT) h; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + desc.BufferCount = 2; + desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; + desc.Scaling = DXGI_SCALING_STRETCH; + desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + ComPtr windowUnknown; + g_window.As(&windowUnknown); + if (FAILED(dxgiFactory->CreateSwapChainForCoreWindow(g_d3d.Get(), windowUnknown.Get(), &desc, NULL, + &g_swapChain))) { + cn1WindowsLog("initDisplay(uwp): CreateSwapChainForCoreWindow failed"); + return 0; + } + if (FAILED(cn1UwpBindBackBuffer()) || FAILED(cn1UwpCreateCanvas(w, h))) { + cn1WindowsLog("initDisplay(uwp): could not bind the swap chain"); + return 0; + } + return 1; +} + +/* Copies the canvas to the back buffer and presents. EDT, outside a frame. */ +static void cn1UwpPresent(void) { + if (!g_swapChain || !g_canvas || !g_backBuffer) { + return; + } + g_dc->SetTarget(g_backBuffer.Get()); + g_dc->BeginDraw(); + g_dc->SetTransform(D2D1::Matrix3x2F::Identity()); + g_dc->DrawImage(g_canvas.Get(), D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_COMPOSITE_MODE_SOURCE_COPY); + g_dc->EndDraw(); + g_dc->SetTarget(g_canvas.Get()); + g_swapChain->Present(1, 0); +} + +/* ------------------------------------------------------------- input */ + +static int cn1UwpButtons(ABI::Windows::UI::Input::IPointerPoint* point, int* touchFlag, boolean* inContact) { + using namespace ABI::Windows::Devices::Input; + int mask = 0; + *touchFlag = 0; + *inContact = 0; + point->get_IsInContact(inContact); + ComPtr device; + PointerDeviceType type = PointerDeviceType_Mouse; + if (SUCCEEDED(point->get_PointerDevice(&device)) && device) { + device->get_PointerDeviceType(&type); + } + if (type == PointerDeviceType_Touch) { + *touchFlag = CN1_PE_TOUCH_FLAG; + } else if (type == PointerDeviceType_Pen) { + *touchFlag = CN1_PE_PEN_FLAG; + } + ComPtr props; + if (SUCCEEDED(point->get_Properties(&props)) && props) { + boolean b = 0; + if (SUCCEEDED(props->get_IsLeftButtonPressed(&b)) && b) mask |= CN1_PE_MASK_PRIMARY; + if (SUCCEEDED(props->get_IsRightButtonPressed(&b)) && b) mask |= CN1_PE_MASK_SECONDARY; + if (SUCCEEDED(props->get_IsMiddleButtonPressed(&b)) && b) mask |= CN1_PE_MASK_MIDDLE; + } + if (mask == 0 && *inContact) { + mask = CN1_PE_MASK_PRIMARY; /* a finger or a pen tip */ + } + return mask; +} + +typedef ITypedEventHandler PointerHandler; + +static HRESULT cn1UwpOnPointer(IPointerEventArgs* args, CN1EventType kind) { + ComPtr point; + if (FAILED(args->get_CurrentPoint(&point)) || !point) { + return S_OK; + } + Point p = {}; + point->get_Position(&p); + int x = cn1UwpToPixels(p.X); + int y = cn1UwpToPixels(p.Y); + int touch = 0; + boolean inContact = 0; + int mask = cn1UwpButtons(point.Get(), &touch, &inContact); + if (kind == CN1_EVENT_POINTER_RELEASED) { + /* The released button is no longer reported as pressed. */ + cn1WinPushEvent(CN1_EVENT_POINTER_RELEASED, x, y, CN1_PE_MASK_PRIMARY | touch); + } else if (kind == CN1_EVENT_POINTER_PRESSED) { + cn1WinPushEvent(CN1_EVENT_POINTER_PRESSED, x, y, (mask != 0 ? mask : CN1_PE_MASK_PRIMARY) | touch); + } else if (mask != 0) { + cn1WinPushEvent(CN1_EVENT_POINTER_DRAGGED, x, y, mask | touch); + } else { + cn1WinPushEvent(CN1_EVENT_POINTER_HOVER, x, y, touch); + } + return S_OK; +} + +static HRESULT cn1UwpOnWheel(IPointerEventArgs* args) { + ComPtr point; + ComPtr props; + if (FAILED(args->get_CurrentPoint(&point)) || !point || FAILED(point->get_Properties(&props)) || !props) { + return S_OK; + } + Point p = {}; + point->get_Position(&p); + INT32 delta = 0; + boolean horizontal = 0; + props->get_MouseWheelDelta(&delta); + props->get_IsHorizontalMouseWheel(&horizontal); + cn1WinPushEvent(horizontal ? CN1_EVENT_MOUSE_HWHEEL : CN1_EVENT_MOUSE_WHEEL, + cn1UwpToPixels(p.X), cn1UwpToPixels(p.Y), (int) delta); + return S_OK; +} + +/* The TV-safe insets in pixels (left, top, right, bottom). ApplicationView and + * the window bounds answer only on the view thread, while the EDT asks during + * layout, so the view thread computes them (initDisplay, SizeChanged) and the + * native only copies these out. */ +static volatile LONG g_safeInsets[4]; +static volatile LONG g_safeKnown; + +static void cn1UwpUpdateSafeArea(void) { + using namespace ABI::Windows::UI::ViewManagement; + ComPtr statics; + ComPtr view; + ComPtr view2; + Rect visible = {}; + Rect bounds = {}; + if (!g_window || FAILED(GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_ViewManagement_ApplicationView).Get(), &statics)) + || FAILED(statics->GetForCurrentView(&view)) || FAILED(view.As(&view2)) + || FAILED(view2->get_VisibleBounds(&visible)) || FAILED(g_window->get_Bounds(&bounds))) { + return; + } + int edge[4]; + edge[0] = cn1UwpToPixels(visible.X - bounds.X); + edge[1] = cn1UwpToPixels(visible.Y - bounds.Y); + edge[2] = cn1UwpToPixels((bounds.X + bounds.Width) - (visible.X + visible.Width)); + edge[3] = cn1UwpToPixels((bounds.Y + bounds.Height) - (visible.Y + visible.Height)); + for (int i = 0; i < 4; i++) { + InterlockedExchange(&g_safeInsets[i], edge[i] > 0 ? edge[i] : 0); + } + InterlockedExchange(&g_safeKnown, 1); +} + +/* Handled lives on ICoreWindowEventArgs, which every key event also implements. */ +static void cn1UwpMarkHandled(IKeyEventArgs* a) { + ComPtr base; + if (SUCCEEDED(a->QueryInterface(IID_PPV_ARGS(&base)))) { + base->put_Handled(TRUE); + } +} + +static void cn1UwpRegisterInput(void) { + EventRegistrationToken token; + g_window->add_PointerPressed(Callback([](ICoreWindow*, IPointerEventArgs* a) { + return cn1UwpOnPointer(a, CN1_EVENT_POINTER_PRESSED); + }).Get(), &token); + g_window->add_PointerReleased(Callback([](ICoreWindow*, IPointerEventArgs* a) { + return cn1UwpOnPointer(a, CN1_EVENT_POINTER_RELEASED); + }).Get(), &token); + g_window->add_PointerMoved(Callback([](ICoreWindow*, IPointerEventArgs* a) { + return cn1UwpOnPointer(a, CN1_EVENT_POINTER_DRAGGED); + }).Get(), &token); + g_window->add_PointerWheelChanged(Callback([](ICoreWindow*, IPointerEventArgs* a) { + return cn1UwpOnWheel(a); + }).Get(), &token); + + typedef ITypedEventHandler KeyHandler; + /* Handled = true: otherwise the shell treats Gamepad B as "go back" and the + * D-pad as its own focus navigation, on top of the application's. */ + g_window->add_KeyDown(Callback([](ICoreWindow*, IKeyEventArgs* a) { + ABI::Windows::System::VirtualKey key = ABI::Windows::System::VirtualKey_None; + a->get_VirtualKey(&key); + cn1UwpMarkHandled(a); + cn1WinPushEvent(CN1_EVENT_KEY_PRESSED, 0, 0, (int) key); + return S_OK; + }).Get(), &token); + g_window->add_KeyUp(Callback([](ICoreWindow*, IKeyEventArgs* a) { + ABI::Windows::System::VirtualKey key = ABI::Windows::System::VirtualKey_None; + a->get_VirtualKey(&key); + cn1UwpMarkHandled(a); + cn1WinPushEvent(CN1_EVENT_KEY_RELEASED, 0, 0, (int) key); + return S_OK; + }).Get(), &token); + + typedef ITypedEventHandler SizeHandler; + g_window->add_SizeChanged(Callback([](ICoreWindow*, IWindowSizeChangedEventArgs*) { + int w, h; + g_scale = cn1UwpScale(); + cn1UwpWindowPixels(&w, &h); + cn1UwpUpdateSafeArea(); + if (w == cn1Win.width && h == cn1Win.height) { + return S_OK; + } + cn1Win.width = w; + cn1Win.height = h; + /* Applied by the EDT before its next frame (cn1WinApplyPendingResize), + * never here: the EDT may be mid-frame on the canvas. */ + cn1Win.pendingW = w; + cn1Win.pendingH = h; + InterlockedExchange(&cn1Win.pendingResize, 1); + cn1WinPushEvent(CN1_EVENT_SIZE_CHANGED, w, h, 0); + return S_OK; + }).Get(), &token); + + typedef ITypedEventHandler ClosedHandler; + g_window->add_Closed(Callback([](ICoreWindow*, ICoreWindowEventArgs*) { + InterlockedExchange(&g_closed, 1); + cn1WinPushEvent(CN1_EVENT_CLOSE, 0, 0, 0); + return S_OK; + }).Get(), &token); +} + +/* ------------------------------------------------- CoreApplication view */ + +class Cn1View : public RuntimeClass { +public: + IFACEMETHODIMP Initialize(ICoreApplicationView* view) override { + EventRegistrationToken token; + typedef ITypedEventHandler ActivatedHandler; + return view->add_Activated(Callback([](ICoreApplicationView*, IActivatedEventArgs*) { + /* Until the window is activated the console keeps showing the splash. */ + if (g_window) { + g_window->Activate(); + } + return S_OK; + }).Get(), &token); + } + + IFACEMETHODIMP SetWindow(ICoreWindow* window) override { + g_window = window; + g_window->get_Dispatcher(&g_dispatcher); + return S_OK; + } + + IFACEMETHODIMP Load(HSTRING) override { + return S_OK; + } + + IFACEMETHODIMP Run() override { + /* The Java program, on the window's own thread; it returns when the + * application exits. */ + cn1JavaMain(g_argc, g_argv); + return S_OK; + } + + IFACEMETHODIMP Uninitialize() override { + return S_OK; + } +}; + +class Cn1ViewSource : public RuntimeClass { +public: + IFACEMETHODIMP CreateView(IFrameworkView** view) override { + return Make().CopyTo(view); + } +}; + +int main(int argc, char* argv[]) { + g_argc = argc; + g_argv = argv; + RoInitializeWrapper init(RO_INIT_MULTITHREADED); + ComPtr app; + if (FAILED(GetActivationFactory(HStringReference( + RuntimeClass_Windows_ApplicationModel_Core_CoreApplication).Get(), &app))) { + return 1; + } + ComPtr source = Make(); + return FAILED(app->Run(source.Get())) ? 1 : 0; +} + +/* --------------------------------------------------------------- bridge */ + +extern "C" { + +int cn1UwpLocalFolder(WCHAR* buf, int capacity) { + using namespace ABI::Windows::Storage; + ComPtr statics; + ComPtr data; + ComPtr folder; + ComPtr item; + HString path; + if (buf == NULL || capacity <= 0 + || FAILED(GetActivationFactory(HStringReference(RuntimeClass_Windows_Storage_ApplicationData).Get(), + &statics)) + || FAILED(statics->get_Current(&data)) || FAILED(data->get_LocalFolder(&folder)) + || FAILED(folder.As(&item)) || FAILED(item->get_Path(path.GetAddressOf()))) { + return 0; + } + UINT32 len = 0; + const wchar_t* raw = path.GetRawBuffer(&len); + if (raw == NULL || (int) len >= capacity) { + return 0; + } + memcpy(buf, raw, len * sizeof(WCHAR)); + buf[len] = L'\0'; + return 1; +} + +void cn1WinApplyPendingResize(void) { + if (!InterlockedExchange(&cn1Win.pendingResize, 0) || !g_swapChain) { + return; + } + int w = (int) cn1Win.pendingW; + int h = (int) cn1Win.pendingH; + g_dc->SetTarget(NULL); + g_backBuffer.Reset(); + if (FAILED(g_swapChain->ResizeBuffers(2, (UINT) w, (UINT) h, DXGI_FORMAT_B8G8R8A8_UNORM, 0)) + || FAILED(cn1UwpBindBackBuffer()) || FAILED(cn1UwpCreateCanvas(w, h))) { + cn1WindowsLog("uwp: resize failed"); + } +} + +extern "C" JAVA_VOID com_codename1_impl_windows_WindowsNative_mainWindowSetTitle___java_lang_String( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1); + +JAVA_VOID com_codename1_impl_windows_WindowsNative_initDisplay___java_lang_String_int_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1, JAVA_INT __cn1Arg2, JAVA_INT __cn1Arg3) { + (void) __cn1Arg2; + (void) __cn1Arg3; /* the window is the whole screen; its size is not ours to pick */ + if (!cn1WinInitRuntime()) { + return; + } + if (!g_window) { + cn1WindowsLog("initDisplay(uwp): no CoreWindow -- not started through main?"); + return; + } + g_scale = cn1UwpScale(); + int w, h; + cn1UwpWindowPixels(&w, &h); + cn1Win.width = w; + cn1Win.height = h; + if (!cn1UwpCreateDevice(w, h)) { + return; + } + cn1Win.windowGraphics = cn1WinCreateGraphics((ID2D1RenderTarget*) g_dc.Get()); + if (cn1Win.windowGraphics != NULL) { + cn1Win.windowGraphics->isWindowTarget = JAVA_TRUE; + } + CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, (void**) &cn1Win.wicFactory); + cn1UwpRegisterInput(); + if (__cn1Arg1 != JAVA_NULL) { + com_codename1_impl_windows_WindowsNative_mainWindowSetTitle___java_lang_String(threadStateData, __cn1Arg1); + } + g_window->Activate(); + cn1UwpUpdateSafeArea(); + cn1WindowsLog("initDisplay(uwp): CoreWindow swap chain created"); +} + +/* Sets the title on the view thread, the only one ApplicationView answers on. */ +static void cn1UwpApplyTitle(const WCHAR* title) { + using namespace ABI::Windows::UI::ViewManagement; + ComPtr statics; + ComPtr view; + if (SUCCEEDED(GetActivationFactory(HStringReference(RuntimeClass_Windows_UI_ViewManagement_ApplicationView).Get(), + &statics)) && SUCCEEDED(statics->GetForCurrentView(&view))) { + view->put_Title(HStringReference(title != NULL ? title : L"").Get()); + } +} + +JAVA_VOID com_codename1_impl_windows_WindowsNative_mainWindowSetTitle___java_lang_String( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { + if (!g_dispatcher) { + return; + } + UINT32 len = 0; + WCHAR* title = __cn1Arg1 != JAVA_NULL ? cn1WinJavaStringToWide(threadStateData, __cn1Arg1, &len) : NULL; + boolean onViewThread = 0; + if (SUCCEEDED(g_dispatcher->get_HasThreadAccess(&onViewThread)) && onViewThread) { + cn1UwpApplyTitle(title); + free(title); + return; + } + /* From the EDT: RunAsync is callable from any thread, runs the handler on + * the view thread and wakes the pump that is blocked in ProcessEvents. The + * handler owns the string. */ + typedef ABI::Windows::UI::Core::IDispatchedHandler DispatchedHandler; + ComPtr action; + HRESULT hr = g_dispatcher->RunAsync(ABI::Windows::UI::Core::CoreDispatcherPriority_Normal, + Callback([title]() { + cn1UwpApplyTitle(title); + free(title); + return S_OK; + }).Get(), &action); + if (FAILED(hr)) { + free(title); + } +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_getDisplayWidth___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1Win.width; +} + +/* The logical DPI: 96 times the view scale. On a television that is what makes + * millimetre-sized theme metrics readable from the couch; the raw DPI of a 55" + * panel would size everything for a phone held at arm's length. */ +JAVA_INT com_codename1_impl_windows_WindowsNative_screenDpi___R_int(CODENAME_ONE_THREAD_STATE) { + return (JAVA_INT) floorf(96.0f * g_scale + 0.5f); +} + +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isTouchDevice___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isHighContrastEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isReduceMotionEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isScreenReaderEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + +JAVA_VOID com_codename1_impl_windows_WindowsNative_flushGraphics___long_int_int_int_int( + CODENAME_ONE_THREAD_STATE, JAVA_LONG __cn1Arg1, JAVA_INT __cn1Arg2, JAVA_INT __cn1Arg3, + JAVA_INT __cn1Arg4, JAVA_INT __cn1Arg5) { + CN1Graphics* g = (CN1Graphics*) (intptr_t) __cn1Arg1; + if (g != NULL) { + cn1WinEndFrame(g); + if (g == cn1Win.windowGraphics) { + cn1UwpPresent(); + } + } + InterlockedIncrement(&cn1Win.flushGen); +} + +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_pumpMessages___R_boolean(CODENAME_ONE_THREAD_STATE) { + /* Same contract as the Win32 pump: block for input, dispatch the burst, + * return so the caller drains the ring; false once the window is gone. The + * handlers only push into the ring and never touch the Java heap, so the + * thread stays yielded through the whole dispatch. */ + if (!g_dispatcher || g_closed) { + return JAVA_FALSE; + } + CN1_YIELD_THREAD; + HRESULT hr = g_dispatcher->ProcessEvents(CoreProcessEventsOption_ProcessOneAndAllPending); + CN1_RESUME_THREAD; + return (FAILED(hr) || g_closed) ? JAVA_FALSE : JAVA_TRUE; +} + +/* The TV-safe area, in pixels from each edge: what ApplicationView reports as + * visible inside the CoreWindow. A television may crop the outer few percent of + * the picture, so an Xbox reports an inset there; anything else reports none. */ +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_safeAreaInsets___int_1ARRAY_R_boolean( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { + if (__cn1Arg1 == JAVA_NULL || (*(JAVA_ARRAY) __cn1Arg1).length < 4 || !g_safeKnown) { + return JAVA_FALSE; + } + JAVA_ARRAY_INT* out = (JAVA_ARRAY_INT*) CN1_ARRAY_DATA(__cn1Arg1); + for (int i = 0; i < 4; i++) { + out[i] = (JAVA_ARRAY_INT) g_safeInsets[i]; + } + return JAVA_TRUE; +} + +} /* extern "C" */ + +#endif /* _WIN32 && CN1_WINDOWS_UWP */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_crash_protection.c b/Ports/WindowsPort/nativeSources/cn1_windows_crash_protection.c index caf94d1e1ec..4aab8ce2d5b 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_crash_protection.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_crash_protection.c @@ -1,12 +1,23 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Codename One designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ @@ -100,6 +111,11 @@ static unsigned __stdcall cn1_cp_log_reader_thread(void *arg) { } static void cn1_cp_log_install(void) { +#if defined(CN1_WINDOWS_UWP) + /* The UWP CRT has no _pipe, and an app container has no stderr anyone + * reads; the crash record still works, only the log tail is empty. */ + return; +#else int pipefds[2]; if (_pipe(pipefds, 4096, _O_BINARY) != 0) return; cn1_cp_stderr_dup_fd = _dup(_fileno(stderr)); @@ -115,6 +131,7 @@ static void cn1_cp_log_install(void) { HANDLE h = (HANDLE)_beginthreadex(NULL, 0, cn1_cp_log_reader_thread, (void *)(intptr_t)pipefds[0], 0, NULL); if (h != NULL) CloseHandle(h); +#endif /* CN1_WINDOWS_UWP */ } char * cn1_crash_protection_log_snapshot(void) { @@ -139,7 +156,19 @@ char * cn1_crash_protection_log_snapshot(void) { static void cn1_cp_resolve_pending_path(void) { wchar_t *wlocal = NULL; +#if defined(CN1_WINDOWS_UWP) + /* The package's own local folder: the only place the container may write. */ + static wchar_t uwpLocal[MAX_PATH]; + if (cn1UwpLocalFolder(uwpLocal, MAX_PATH)) { + wlocal = (wchar_t *)CoTaskMemAlloc(sizeof(uwpLocal)); + if (wlocal != NULL) { + memcpy(wlocal, uwpLocal, sizeof(uwpLocal)); + } + } + if (wlocal == NULL) { +#else if (SHGetKnownFolderPath(&FOLDERID_LocalAppData, 0, NULL, &wlocal) != S_OK) { +#endif snprintf(cn1_cp_pending_path, sizeof(cn1_cp_pending_path), "C:\\Temp\\.cn1_pending_native_crash"); cn1_cp_pending_path_set = 1; diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_crypto.c b/Ports/WindowsPort/nativeSources/cn1_windows_crypto.c index 2fe16c3f3c4..7ab6ccef58e 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_crypto.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_crypto.c @@ -1313,6 +1313,12 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_generateRsaKeyPair___int_R_ } /* Public half: BCrypt handle -> CERT_PUBLIC_KEY_INFO -> X.509 SPKI DER. */ +#if defined(CN1_WINDOWS_UWP) + /* crypt32's SPKI export is outside the UWP app partition. Generating an + * RSA pair on the Xbox target reports failure rather than half a pair. */ + cn1CryptoFail("RSA key pair export is not available in a UWP build", 0); + goto done; +#else if (!CryptExportPublicKeyInfoFromBCryptKeyHandle(key, X509_ASN_ENCODING, NULL, 0, NULL, NULL, &publicInfoLength)) { cn1CryptoFailLast("public key export size"); @@ -1330,6 +1336,7 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_generateRsaKeyPair___int_R_ cn1CryptoFailLast("public key encode"); goto done; } +#endif /* CN1_WINDOWS_UWP */ /* Private half: BCrypt blob -> PKCS#1 DER -> PKCS#8 PrivateKeyInfo DER. */ status = BCryptExportKey(key, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, 0, &privateBlobLength, 0); diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_desktopwindow.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_desktopwindow.cpp index 42247fb6db6..7f3a5bafe2d 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_desktopwindow.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_desktopwindow.cpp @@ -43,6 +43,8 @@ */ #include "cn1_windows.h" + +#if !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include #include #include @@ -1117,3 +1119,5 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_monitorForMainWindow___R_int( } } /* extern "C" */ + +#endif /* !CN1_WINDOWS_UWP */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_edit.c b/Ports/WindowsPort/nativeSources/cn1_windows_edit.c index 120bdab3802..7e8ef02ec8a 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_edit.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_edit.c @@ -38,7 +38,7 @@ #include "cn1_windows.h" -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #define CN1_EDIT_OP_CREATE 1 #define CN1_EDIT_OP_DESTROY 2 diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_filedialog.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_filedialog.cpp index ba17234d844..771d11954e9 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_filedialog.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_filedialog.cpp @@ -32,7 +32,7 @@ * the Java String[] only after it has resumed. Nothing on the pump thread touches * a Java object. */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include "cn1_windows.h" diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_io.c b/Ports/WindowsPort/nativeSources/cn1_windows_io.c index f03270804a3..2f67cf0ef02 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_io.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_io.c @@ -580,9 +580,16 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_fileList___java_lang_String JAVA_OBJECT com_codename1_impl_windows_WindowsNative_storageDir___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { WCHAR base[MAX_PATH]; WCHAR dir[MAX_PATH]; +#if defined(CN1_WINDOWS_UWP) + /* The app container may write only under its own package folder. */ + if (!cn1UwpLocalFolder(base, MAX_PATH)) { + return JAVA_NULL; + } +#else if (SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, base) != S_OK) { return JAVA_NULL; } +#endif _snwprintf(dir, MAX_PATH, L"%s\\CodenameOne", base); dir[MAX_PATH - 1] = L'\0'; /* ensure the directory exists; ignore "already exists" */ @@ -608,6 +615,22 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_executableDir___R_java_lang } JAVA_OBJECT com_codename1_impl_windows_WindowsNative_fileRoots___R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE) { +#if defined(CN1_WINDOWS_UWP) + /* The app container sees no drives, only its package folder: that is the + * one root. */ + WCHAR local[MAX_PATH]; + JAVA_OBJECT root; + JAVA_OBJECT arr; + if (!cn1UwpLocalFolder(local, MAX_PATH)) { + return JAVA_NULL; + } + root = cn1WinWideToJavaString(threadStateData, local); + arr = allocArray(threadStateData, 1, &class_array1__java_lang_String, sizeof(JAVA_OBJECT), 1); + if (arr != JAVA_NULL) { + ((JAVA_OBJECT*) CN1_ARRAY_DATA(arr))[0] = root; + } + return arr; +#else /* GetLogicalDriveStringsW fills a NUL-separated, double-NUL-terminated list */ DWORD needed = GetLogicalDriveStringsW(0, NULL); WCHAR* buffer; @@ -661,6 +684,7 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_fileRoots___R_java_lang_Str } free(names); return arr; +#endif /* CN1_WINDOWS_UWP */ } JAVA_LONG com_codename1_impl_windows_WindowsNative_fileRootSize___java_lang_String_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { @@ -730,6 +754,10 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_fileSetHidden___java_lang_Str free(path); } +#if !defined(CN1_WINDOWS_UWP) /* uwp: desktop only */ +/* The Win32 clipboard, ShellExecute and the common file dialogs do not exist in + * the UWP app container; scripts/gen-windows-uwp-stubs.py stubs these natives. */ + /* --------------------------------------------------------------- clipboard */ JAVA_VOID com_codename1_impl_windows_WindowsNative_clipboardSetText___java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { @@ -1138,6 +1166,8 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_fileDialog___boolean_int_ja return cn1WinWideToJavaString(threadStateData, req.result); } +#endif /* uwp: end desktop only */ + /* ------------------------------------------------- DPAPI secure storage */ /* diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_menu.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_menu.cpp index ac3f197fbc1..39433993cea 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_menu.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_menu.cpp @@ -52,6 +52,8 @@ */ #include "cn1_windows.h" + +#if !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include #include @@ -485,3 +487,5 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_currentModifiers___R_int( } } + +#endif /* !CN1_WINDOWS_UWP */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_notify.c b/Ports/WindowsPort/nativeSources/cn1_windows_notify.c index 94cdf2e2b2a..0332ebf911e 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_notify.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_notify.c @@ -37,7 +37,7 @@ #include "cn1_windows.h" -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include #include diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_peer.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_peer.cpp index d9e54c7395d..767ccd78b4d 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_peer.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_peer.cpp @@ -40,7 +40,7 @@ * these run directly on the EDT-facing native calls. */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #define WIN32_LEAN_AND_MEAN #include diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_print.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_print.cpp index 505fe2f5397..1f0cfd69378 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_print.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_print.cpp @@ -49,7 +49,7 @@ * window there is no dialog, so the request fails. */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include "cn1_windows.h" diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_screenshot.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_screenshot.cpp index 342c423e63f..b76e2b0b6ca 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_screenshot.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_screenshot.cpp @@ -267,6 +267,8 @@ static JAVA_OBJECT cn1WinWicSourceToPngBytes(CODENAME_ONE_THREAD_STATE, IWICBitm * window from one whose raster and hierarchy disagree, and it never contains a * peer or a native editor at all. */ +#if !defined(CN1_WINDOWS_UWP) /* uwp: desktop only */ +/* GDI PrintWindow of a secondary HWND. */ #ifndef PW_RENDERFULLCONTENT /* Windows 8.1 and later. Defined here rather than assumed, because an older SDK * header omits it and the flag is the difference between a captured Direct2D @@ -343,6 +345,7 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_captureDesktopWindowToPngBy } return result; } +#endif /* uwp: end desktop only */ /* * Encodes the current window's render target to PNG bytes. In headless / diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_socket.c b/Ports/WindowsPort/nativeSources/cn1_windows_socket.c index 28d1f47375d..dd7a87f7691 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_socket.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_socket.c @@ -76,7 +76,8 @@ JAVA_LONG com_codename1_impl_windows_WindowsNative_socketConnect___java_lang_Str } cn1SockEnsureStartup(); host = stringToUTF8(threadStateData, __cn1Arg1); - wsprintfA(portStr, "%d", (int) __cn1Arg2); + /* snprintf, not wsprintfA: user32 is not in the UWP app partition. */ + snprintf(portStr, sizeof(portStr), "%d", (int) __cn1Arg2); ZeroMemory(&hints, sizeof(hints)); hints.ai_family = AF_UNSPEC; diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_uwp_stubs.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_uwp_stubs.cpp new file mode 100644 index 00000000000..9a6d4bcdac0 --- /dev/null +++ b/Ports/WindowsPort/nativeSources/cn1_windows_uwp_stubs.cpp @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * GENERATED by scripts/gen-windows-uwp-stubs.py -- do not edit by hand. + * + * The desktop-only natives, answering "unsupported" in a UWP (Xbox Developer + * Mode) build, where their real definitions are compiled out. See the script + * for why each one has to exist. + */ +#include "cn1_windows.h" + +#if defined(_WIN32) && defined(CN1_WINDOWS_UWP) + +extern "C" { + +/* cn1_windows_accessibility.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityAction___long_java_lang_String_int_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG nodeId, JAVA_OBJECT actionId, JAVA_INT actionHash, JAVA_OBJECT label) { +} + +/* cn1_windows_accessibility.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityBegin__(CODENAME_ONE_THREAD_STATE) { +} + +/* cn1_windows_accessibility.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityEnd___int(CODENAME_ONE_THREAD_STATE, JAVA_INT changeType) { +} + +/* cn1_windows_accessibility.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityNode___long_long_java_lang_String_java_lang_String_java_lang_String_java_lang_String_int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG id, JAVA_LONG parent, JAVA_OBJECT role, JAVA_OBJECT label, JAVA_OBJECT description, JAVA_OBJECT value, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height, JAVA_INT flags) { +} + +/* cn1_windows_audiorec.c */ +JAVA_LONG com_codename1_impl_windows_WindowsNative_audioRecStart___java_lang_String_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT pathObj, JAVA_INT sampleRate, JAVA_INT channels) { + return 0; +} + +/* cn1_windows_audiorec.c */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_audioRecStop___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { +} + +/* cn1_windows_camera.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_cameraCaptureFrame___int_1ARRAY_R_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT outDims) { + return 0; +} + +/* cn1_windows_camera.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_cameraEnumerate___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_camera.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_cameraSessionLatestFrame___long_int_1ARRAY_R_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_OBJECT outDims) { + return 0; +} + +/* cn1_windows_camera.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_cameraSessionSetPaused___long_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_BOOLEAN paused) { +} + +/* cn1_windows_camera.cpp */ +JAVA_LONG com_codename1_impl_windows_WindowsNative_cameraSessionStart___int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT deviceIndex, JAVA_INT reqW, JAVA_INT reqH) { + return 0; +} + +/* cn1_windows_camera.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_cameraSessionStop___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { +} + +/* cn1_windows_screenshot.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_captureDesktopWindowToPngBytes___int_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_INT __cn1Arg1) { + return 0; +} + +/* cn1_windows_io.c */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_clipboardGetFiles___R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_io.c */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_clipboardGetImage___R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_io.c */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_clipboardGetText___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_io.c */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_clipboardSetFiles___java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { +} + +/* cn1_windows_io.c */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_clipboardSetImage___byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { +} + +/* cn1_windows_io.c */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_clipboardSetText___java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { +} + +/* cn1_windows_menu.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_currentModifiers___R_int(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_desktopWindowCreate___int_java_lang_String_int_int_int_int_boolean_boolean_int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT windowId, JAVA_OBJECT title, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height, JAVA_BOOLEAN decorated, JAVA_BOOLEAN resizable, JAVA_INT ownerSlot, JAVA_BOOLEAN positionSet) { + return -1; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowDestroy___int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowFocus___int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowGetBounds___int_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_OBJECT out) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_desktopWindowGetHeight___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_desktopWindowGetWidth___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_LONG com_codename1_impl_windows_WindowsNative_desktopWindowGraphics___int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetAlwaysOnTop___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_BOOLEAN onTop) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetBounds___int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetDecorated___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_BOOLEAN decorated) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetEnabled___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_BOOLEAN enabled) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetMinimumSize___int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT width, JAVA_INT height) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetResizable___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_BOOLEAN resizable) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetState___int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT state) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetTitle___int_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_OBJECT title) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowSetUtility___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_BOOLEAN utility) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_desktopWindowShow___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_BOOLEAN visible) { +} + +/* cn1_windows_edit.c */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_editClose___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { +} + +/* cn1_windows_edit.c */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_editGetText___long_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + return 0; +} + +/* cn1_windows_edit.c */ +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_editIsDone___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + return 0; +} + +/* cn1_windows_edit.c */ +JAVA_LONG com_codename1_impl_windows_WindowsNative_editStringAt___int_int_int_int_java_lang_String_boolean_int_long_int_int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h, JAVA_OBJECT text, JAVA_BOOLEAN singleLine, JAVA_INT maxSize, JAVA_LONG fontPeer, JAVA_INT fgColor, JAVA_INT bgColor, JAVA_INT align, JAVA_INT slot) { + return 0; +} + +/* cn1_windows_filedialog.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_fileDialogEx___boolean_java_lang_String_java_lang_String_java_lang_String_java_lang_String_java_lang_String_1ARRAY_int_boolean_boolean_boolean_boolean_java_lang_String_1ARRAY_java_lang_String_java_lang_String_R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_BOOLEAN save, JAVA_OBJECT title, JAVA_OBJECT acceptLabel, JAVA_OBJECT initialDir, JAVA_OBJECT suggestedName, JAVA_OBJECT filters, JAVA_INT selectedFilter, JAVA_BOOLEAN multi, JAVA_BOOLEAN dirs, JAVA_BOOLEAN confirmOverwrite, JAVA_BOOLEAN showHidden, JAVA_OBJECT options, JAVA_OBJECT clientGuid, JAVA_OBJECT defaultFolder) { + return 0; +} + +/* cn1_windows_io.c */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_fileDialog___boolean_int_java_lang_String_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_BOOLEAN save, JAVA_INT type, JAVA_OBJECT title) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_mainWindowGetBounds___int_1ARRAY_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_mainWindowSetEnabled___boolean(CODENAME_ONE_THREAD_STATE, JAVA_BOOLEAN enabled) { +} + +/* cn1_windows_chrome.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_mainWindowTitleBarInsets___int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT array) { +} + +/* cn1_windows_chrome.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_mainWindowTitleBarRegions___int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT array) { +} + +/* cn1_windows_menu.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_menuSetCommands___java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT spec) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_monitorBounds___int_boolean_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_INT monitor, JAVA_BOOLEAN workArea, JAVA_OBJECT out) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_monitorCount___R_int(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_monitorDpi___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT monitor) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_monitorForMainWindow___R_int(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_monitorForWindow___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + return 0; +} + +/* cn1_windows_notify.c */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_notificationPollClicked___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_peer.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_peerCalcPreferredSize___long_int_int_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT dispW, JAVA_INT dispH, JAVA_OBJECT out) { +} + +/* cn1_windows_peer.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_peerCaptureArgb___long_int_1ARRAY_R_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT outDims) { + return 0; +} + +/* cn1_windows_peer.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_peerDeinitialized___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { +} + +/* cn1_windows_peer.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_peerInitialized___long_int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT slot, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h) { +} + +/* cn1_windows_peer.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_peerSetBounds___long_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h) { +} + +/* cn1_windows_peer.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_peerSetVisible___long_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_BOOLEAN visible) { +} + +/* cn1_windows_desktopwindow.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_primaryMonitor___R_int(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_print.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_printDocument___java_lang_String_java_lang_String_java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT pathObj, JAVA_OBJECT mimeObj, JAVA_OBJECT jobNameObj) { + return -1; +} + +/* cn1_windows_print.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_printLastError___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_winrt.cpp */ +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_shareText___java_lang_String_java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT textObj, JAVA_OBJECT titleObj) { + return 0; +} + +/* cn1_windows_io.c */ +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_shellOpen___java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { + return 0; +} + +/* cn1_windows_notify.c */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_showNotification___java_lang_String_java_lang_String_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT id, JAVA_OBJECT title, JAVA_OBJECT body) { +} + +/* cn1_windows_widgets.cpp */ +JAVA_LONG com_codename1_impl_windows_WindowsNative_widgetCreate___int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT w, JAVA_INT h) { + return 0; +} + +/* cn1_windows_widgets.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_widgetDestroy___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { +} + +/* cn1_windows_widgets.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_widgetFocusApp__(CODENAME_ONE_THREAD_STATE) { +} + +/* cn1_windows_widgets.cpp */ +JAVA_FLOAT com_codename1_impl_windows_WindowsNative_widgetGetDpiScale___long_R_float(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + return 0; +} + +/* cn1_windows_widgets.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_widgetGetX___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + return 0; +} + +/* cn1_windows_widgets.cpp */ +JAVA_INT com_codename1_impl_windows_WindowsNative_widgetGetY___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + return 0; +} + +/* cn1_windows_widgets.cpp */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_widgetPollEvent___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return 0; +} + +/* cn1_windows_widgets.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_widgetSetHitRects___long_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT rects) { +} + +/* cn1_windows_widgets.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_widgetSetPosition___long_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT x, JAVA_INT y) { +} + +/* cn1_windows_widgets.cpp */ +JAVA_VOID com_codename1_impl_windows_WindowsNative_widgetUpdatePixels___long_int_1ARRAY_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT argb, JAVA_INT w, JAVA_INT h) { +} + +} /* extern "C" */ + +#endif /* _WIN32 && CN1_WINDOWS_UWP */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_widgets.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_widgets.cpp index 9b31e7b3d2b..79658941bba 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_widgets.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_widgets.cpp @@ -70,7 +70,7 @@ * Deliberately no MSVC STL here -- the xwin cross-compile CI's clang-cl * predates the Clang version the MSVC STL demands (STL1000), so this file * sticks to plain C containers (see g_widgetEvents / CN1Widget.hitRects). */ -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ #include #include #include @@ -82,7 +82,7 @@ #include "cn1_windows.h" -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CN1_WINDOWS_UWP) /* desktop only: stubbed for UWP in cn1_windows_uwp_stubs.c */ /* --------------------------------------------------------------- op codes */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp index cd79b91baf8..53cc731b0a5 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp @@ -299,7 +299,9 @@ static LONG WINAPI cn1WinFaultToException(EXCEPTION_POINTERS* info) { /* Re-arm the guard page the overflow consumed so the (small) work below * has stack; the context restore then jumps to a frame high up the stack * with room to spare. */ - _resetstkoflw(); +#if !defined(CN1_WINDOWS_UWP) + _resetstkoflw(); /* not in the UWP CRT: a second overflow then terminates */ +#endif t = getThreadLocalData(); if (t == NULL || t->tryBlockOffset <= 0) { return EXCEPTION_CONTINUE_SEARCH; @@ -557,6 +559,7 @@ int cn1WinPollEvent(CN1Event* out) { } /* ------------------------------------------------------------- input helpers */ +#if !defined(CN1_WINDOWS_UWP) /* Win32 window; cn1_windows_corewindow.cpp is the UWP host */ /* The virtual key whose WM_KEYDOWN a menu accelerator consumed, so the matching WM_KEYUP can * be consumed as well. Zero when no such press is outstanding. Touched only from the window @@ -976,8 +979,42 @@ int cn1WinCreateWindow(const char* utf8Title, int width, int height) { return cn1Win.hwnd != NULL ? 1 : 0; } +#endif /* !CN1_WINDOWS_UWP */ + /* ------------------------------------------------- WindowsNative bridge */ +/* + * The part of initDisplay every host shares -- the Win32 window here and the UWP + * CoreWindow host (cn1_windows_corewindow.cpp): COM, the fault handlers that turn + * a null dereference into a NullPointerException, and the input ring. Returns 0 + * when the runtime was already initialised. + */ +int cn1WinInitRuntime(void) { + if (InterlockedCompareExchange(&cn1Win.initialized, 1, 0) != 0) { + return 0; /* already initialised */ + } + + /* Under UWP the view thread is already initialised by CoreApplication and + * this answers RPC_E_CHANGED_MODE, which is harmless: the apartment the + * view thread has is the one the WinRT calls need. */ + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + cn1PreviousUnhandled = SetUnhandledExceptionFilter(cn1WinUnhandled); + /* First-chance: turn null-deref / stack-overflow faults into catchable Java + * exceptions (see cn1WinFaultToException). Installed first (FirstHandler=1) + * so it runs before the OS default, but it only redirects genuine null + * derefs / overflows with a CN1 catch frame and lets everything else pass + * through to the unhandled-exception logger above. */ + AddVectoredExceptionHandler(1, cn1WinFaultToException); + + InitializeCriticalSection(&cn1Win.eventLock); + cn1Win.eventSignal = CreateEventW(NULL, FALSE, FALSE, NULL); + cn1Win.eventHead = 0; + cn1Win.eventTail = 0; + cn1Win.dpiScale = 1.0f; + return 1; +} + + JAVA_VOID com_codename1_impl_windows_WindowsNative_nativeLog___java_lang_String( CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { if (__cn1Arg1 == JAVA_NULL) { @@ -991,6 +1028,7 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_nativeLog___java_lang_String( * (from cn1WinBeginFrame) so the Resize happens between frames on the same * thread that draws -- never while a frame is open on another thread. */ +#if !defined(CN1_WINDOWS_UWP) void cn1WinApplyPendingResize(void) { if (cn1Win.pendingResize && g_hwndTarget != NULL) { D2D1_SIZE_U size; @@ -1000,6 +1038,7 @@ void cn1WinApplyPendingResize(void) { cn1Win.pendingResize = 0; } } +#endif /* True when CN1_FAULT_SELFTEST is set in the environment. The clean target does * not translate System.getenv, so the launcher's fault-handler self-test gate @@ -1010,6 +1049,7 @@ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_faultSelfTestEnabled___R_b return (n > 0 && n < sizeof(buf)) ? JAVA_TRUE : JAVA_FALSE; } +#if !defined(CN1_WINDOWS_UWP) /* Win32 window; cn1_windows_corewindow.cpp is the UWP host */ /* * The main window's title, after initDisplay has already set it once. * @@ -1053,19 +1093,10 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_initDisplay___java_lang_Strin * never returns in server mode -- it serves widgets and exits the process. */ cn1WidgetBoardMaybeRunComServer(); #endif - if (InterlockedCompareExchange(&cn1Win.initialized, 1, 0) != 0) { + if (!cn1WinInitRuntime()) { return; /* already initialised */ } - CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); - cn1PreviousUnhandled = SetUnhandledExceptionFilter(cn1WinUnhandled); - /* First-chance: turn null-deref / stack-overflow faults into catchable Java - * exceptions (see cn1WinFaultToException). Installed first (FirstHandler=1) - * so it runs before the OS default, but it only redirects genuine null - * derefs / overflows with a CN1 catch frame and lets everything else pass - * through to the unhandled-exception logger above. */ - AddVectoredExceptionHandler(1, cn1WinFaultToException); - /* The exe is linked as a GUI-subsystem app (see writeCmakeProject), so * double-clicking it never allocates a console -- no stray window, no stdout. * When launched from a console (cmd), GUI-subsystem processes do NOT get their @@ -1088,11 +1119,6 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_initDisplay___java_lang_Strin } } - InitializeCriticalSection(&cn1Win.eventLock); - cn1Win.eventSignal = CreateEventW(NULL, FALSE, FALSE, NULL); - cn1Win.eventHead = 0; - cn1Win.eventTail = 0; - cn1Win.dpiScale = 1.0f; /* Headless screenshot mode: no window; render into an offscreen WIC bitmap * that headlessTick later encodes to PNG. The EDT paints through the same @@ -1255,15 +1281,27 @@ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isScreenReaderEnabled___R_ if (!SystemParametersInfoW(SPI_GETSCREENREADER, 0, &screenReader, 0)) return JAVA_FALSE; return screenReader ? JAVA_TRUE : JAVA_FALSE; } +#endif /* !CN1_WINDOWS_UWP */ JAVA_INT com_codename1_impl_windows_WindowsNative_getDisplayHeight___R_int(CODENAME_ONE_THREAD_STATE) { return cn1Win.height; } +#if !defined(CN1_WINDOWS_UWP) +/* A desktop window has no overscan to keep clear of; only the UWP host (on a + * television) reports a safe area. */ +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_safeAreaInsets___int_1ARRAY_R_boolean( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { + (void) __cn1Arg1; + return JAVA_FALSE; +} +#endif + JAVA_LONG com_codename1_impl_windows_WindowsNative_getWindowGraphics___R_long(CODENAME_ONE_THREAD_STATE) { return (JAVA_LONG) (intptr_t) cn1Win.windowGraphics; } +#if !defined(CN1_WINDOWS_UWP) /* Win32 window; cn1_windows_corewindow.cpp is the UWP host */ JAVA_VOID com_codename1_impl_windows_WindowsNative_flushGraphics___long_int_int_int_int( CODENAME_ONE_THREAD_STATE, JAVA_LONG __cn1Arg1, JAVA_INT __cn1Arg2, JAVA_INT __cn1Arg3, JAVA_INT __cn1Arg4, JAVA_INT __cn1Arg5) { @@ -1277,6 +1315,7 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_flushGraphics___long_int_int_ * stopped painting (see runHeadlessLoop). */ InterlockedIncrement(&cn1Win.flushGen); } +#endif /* !CN1_WINDOWS_UWP */ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_pollEvent___int_1ARRAY_R_boolean( CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1Arg1) { @@ -1297,6 +1336,7 @@ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_pollEvent___int_1ARRAY_R_b return JAVA_TRUE; } +#if !defined(CN1_WINDOWS_UWP) /* Win32 window; cn1_windows_corewindow.cpp is the UWP host */ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_pumpMessages___R_boolean(CODENAME_ONE_THREAD_STATE) { /* Runs on the app's main thread (the window's owner) after Display.init. * Blocks for the next window message, dispatches it plus any already-queued @@ -1333,6 +1373,7 @@ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_pumpMessages___R_boolean(C } return JAVA_TRUE; } +#endif /* !CN1_WINDOWS_UWP */ JAVA_VOID com_codename1_impl_windows_WindowsNative_runHeadlessLoop__(CODENAME_ONE_THREAD_STATE) { /* Headless capture has no window / message loop. The main thread parks here diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp index 858808b8cf0..cd389b17b2b 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp @@ -54,7 +54,9 @@ #include #include #include +#if !defined(CN1_WINDOWS_UWP) #include /* IDataTransferManagerInterop */ +#endif using namespace ABI::Windows::Foundation; using namespace ABI::Windows::Foundation::Collections; @@ -382,6 +384,9 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_contactsGetAll___R_java_lan } } +#if !defined(CN1_WINDOWS_UWP) /* uwp: desktop only */ +/* Share goes through the HWND interop bridge, which a UWP app has no use for; + * scripts/gen-windows-uwp-stubs.py stubs shareText there. */ /* ----------------------------------------------------------------- share * * System share via the WinRT DataTransferManager. ShowShareUIForWindow needs the @@ -460,6 +465,7 @@ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_shareText___java_lang_Stri return JAVA_TRUE; } +#endif /* uwp: end desktop only */ /* UISettings provides the user's actual accent palette. Keep initialization balanced: this query runs repeatedly on the long-lived EDT, unlike short-lived worker bridges. */ @@ -468,6 +474,7 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_nativeThemeSettings___R_jav using namespace ABI::Windows::UI::ViewManagement; CN1Buf buffer; cn1BufInit(&buffer); +#if !defined(CN1_WINDOWS_UWP) /* no system metrics or classic palette in the app container */ NONCLIENTMETRICSW metrics = {}; metrics.cbSize = sizeof(metrics); if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0)) { @@ -484,6 +491,7 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_nativeThemeSettings___R_jav cn1BufAppendBytes(&buffer, line, strlen(line)); } } +#endif HRESULT initialized = RoInitialize(RO_INIT_MULTITHREADED); { ComPtr instance; @@ -505,6 +513,7 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_nativeThemeSettings___R_jav if (SUCCEEDED(initialized)) RoUninitialize(); /* GetSysColor describes the classic/high-contrast palette, not Fluent dark surfaces. Use these semantic colors only for high contrast; normal Fluent keeps its surfaces. */ +#if !defined(CN1_WINDOWS_UWP) HIGHCONTRASTW contrast = {}; contrast.cbSize = sizeof(contrast); if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(contrast), &contrast, 0) @@ -523,6 +532,7 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_nativeThemeSettings___R_jav } } } +#endif JAVA_OBJECT result = newStringFromCString(threadStateData, buffer.data ? buffer.data : ""); free(buffer.data); return result; diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java index 73ab1291934..9c08f5c0d13 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java @@ -2721,19 +2721,96 @@ public int getBackspaceKeyCode() { return 8; } + /* Windows virtual-key codes, which is what reaches Codename One unchanged + * from both hosts. The gamepad ones arrive only in the UWP (Xbox) build: a + * CoreWindow app gets the controller as keys, a Win32 window never does. */ + private static final int VK_RETURN = 0x0D; + private static final int VK_LEFT = 0x25; + private static final int VK_UP = 0x26; + private static final int VK_RIGHT = 0x27; + private static final int VK_DOWN = 0x28; + private static final int VK_GAMEPAD_A = 0xC3; + private static final int VK_GAMEPAD_B = 0xC4; + private static final int VK_GAMEPAD_DPAD_UP = 0xCB; + private static final int VK_GAMEPAD_DPAD_DOWN = 0xCC; + private static final int VK_GAMEPAD_DPAD_LEFT = 0xCD; + private static final int VK_GAMEPAD_DPAD_RIGHT = 0xCE; + private static final int VK_GAMEPAD_LEFT_THUMBSTICK_UP = 0xD3; + private static final int VK_GAMEPAD_LEFT_THUMBSTICK_DOWN = 0xD4; + private static final int VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT = 0xD5; + private static final int VK_GAMEPAD_LEFT_THUMBSTICK_LEFT = 0xD6; + + /// B on a controller, the console's universal "back". A Win32 window never + /// receives it, so on the desktop this changes nothing. @Override public int getBackKeyCode() { - return -1; + return VK_GAMEPAD_B; } + /// The arrows and Return, plus the controller's D-pad, left stick and A. + /// + /// This used to answer 0 for everything, so no GAME_* comparison in Form, List + /// or any focus traversal ever matched on this port: the arrows did not move + /// focus and Return pressed no button. On an Xbox, where the controller is the + /// only input, an application could not be used at all. @Override public int getGameAction(int keyCode) { - return 0; + switch (keyCode) { + case VK_UP: + case VK_GAMEPAD_DPAD_UP: + case VK_GAMEPAD_LEFT_THUMBSTICK_UP: + return Display.GAME_UP; + case VK_DOWN: + case VK_GAMEPAD_DPAD_DOWN: + case VK_GAMEPAD_LEFT_THUMBSTICK_DOWN: + return Display.GAME_DOWN; + case VK_LEFT: + case VK_GAMEPAD_DPAD_LEFT: + case VK_GAMEPAD_LEFT_THUMBSTICK_LEFT: + return Display.GAME_LEFT; + case VK_RIGHT: + case VK_GAMEPAD_DPAD_RIGHT: + case VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT: + return Display.GAME_RIGHT; + case VK_RETURN: + case VK_GAMEPAD_A: + return Display.GAME_FIRE; + default: + return 0; + } } + /// The reverse of `#getGameAction(int)`, answering with the keyboard key. @Override public int getKeyCode(int gameAction) { - return 0; + switch (gameAction) { + case Display.GAME_UP: + return VK_UP; + case Display.GAME_DOWN: + return VK_DOWN; + case Display.GAME_LEFT: + return VK_LEFT; + case Display.GAME_RIGHT: + return VK_RIGHT; + case Display.GAME_FIRE: + return VK_RETURN; + default: + return 0; + } + } + + /// The main window minus the edges a television may crop (UWP/Xbox build + /// only); the whole window everywhere else. + @Override + public com.codename1.ui.geom.Rectangle getDisplaySafeArea(com.codename1.ui.geom.Rectangle rect) { + rect = super.getDisplaySafeArea(rect); + int[] insets = new int[4]; + if (WindowsNative.safeAreaInsets(insets)) { + rect.setBounds(rect.getX() + insets[0], rect.getY() + insets[1], + Math.max(0, rect.getWidth() - insets[0] - insets[2]), + Math.max(0, rect.getHeight() - insets[1] - insets[3])); + } + return rect; } /* ------------------------------------------------------------ network */ diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java index 95bbfe819b5..8bda5d7f460 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java @@ -140,6 +140,13 @@ public static native long videoWriterOpen(String outPath, boolean hevc, int widt public static native int getDisplayHeight(); + /** + * Fills {left, top, right, bottom} with the pixels to keep clear at each edge + * of the main window and returns true, or returns false when there is no such + * area. Only the UWP (Xbox) build has one: a television may crop its edges. + */ + public static native boolean safeAreaInsets(int[] out); + /** Real horizontal screen DPI (96 == 100% scale). */ public static native int screenDpi(); diff --git a/docs/demos/common/src/main/snippets/developer-guide/working-with-windows.properties b/docs/demos/common/src/main/snippets/developer-guide/working-with-windows.properties index feb2ade11e4..67575c0a171 100644 --- a/docs/demos/common/src/main/snippets/developer-guide/working-with-windows.properties +++ b/docs/demos/common/src/main/snippets/developer-guide/working-with-windows.properties @@ -10,3 +10,7 @@ codename1.arg.windows.signing.url=https://acme.example codename1.arg.windows.signing.timestampUrl=http://timestamp.digicert.com codename1.arg.windows.signing.digest=sha256 // end::working-with-windows-properties-001[] + +// tag::working-with-windows-properties-002[] +codename1.arg.windows.xbox=true +// end::working-with-windows-properties-002[] diff --git a/docs/developer-guide/Gaming-Consoles.asciidoc b/docs/developer-guide/Gaming-Consoles.asciidoc new file mode 100644 index 00000000000..c3646a30c48 --- /dev/null +++ b/docs/developer-guide/Gaming-Consoles.asciidoc @@ -0,0 +1,55 @@ +[appendix] +[#gaming-consoles] +== Gaming consoles + +Codename One apps and games run on phones, desktops, the browser and TVs. Gaming +consoles are a longer-term goal, and this appendix tracks where that work stands. +Everything here is *experimental*: build hints, behaviour and packaging can change +without notice, and nothing here is supported for production use. + +=== Why consoles are different + +A console doesn't run arbitrary binaries. Each platform holder controls which +packages install, which SDK they're built with, and which APIs they may call. A +retail console title also needs a publishing agreement with that platform holder, +which gives access to its SDK, development hardware and certification process. +None of that can be shipped as open source, so a console port has two parts: + +* The *portable* part, which belongs in Codename One itself: rendering through the +platform's graphics stack, controller input mapped to Codename One's game actions, +TV overscan reported as the display safe area, and an app model without a desktop +window. +* The *platform* part, which depends on each platform holder's SDK and terms and +can't be published. + +=== Xbox in Developer Mode + +The first step is the one console that can be reached without a publishing +agreement. Any retail Xbox can switch to Developer Mode and sideload UWP apps, and +`windows.xbox=true` builds the native Windows port as such an app. See +<> for how to build and install it. + +What this target exercises: + +* The port running inside the Windows app container on a `CoreWindow`, with +Direct2D rendering into a Direct3D swap chain instead of a desktop window. +* Linking against `WindowsApp.lib` alone, which proves at build time that every +system call the port makes exists inside the container. +* The controller arriving as key events: the D-pad and left stick map to the four +directions, *A* to fire and *B* to back, through `Display.getGameAction()`. +* The visible bounds the console reports, exposed through +`Display.getDisplaySafeArea()`. + +Continuous integration cross-compiles this target on Linux, then installs and +launches the package on a Windows machine and checks that it reaches its first +frame. Controller input and TV overscan can only be observed on a console, and +haven't been verified on one. + +=== Beyond Developer Mode + +Developer Mode is a sandbox for testing, not a distribution channel: a game sold on +Xbox is built with Microsoft's Game Development Kit under a publishing agreement, +and PlayStation and Nintendo consoles likewise require their platform holder's +SDK. The portable parts above (the windowless host, controller mapping, safe area, +and game code from <> or `com.codename1.gaming`) are +what such a port would reuse. diff --git a/docs/developer-guide/Working-With-Windows.asciidoc b/docs/developer-guide/Working-With-Windows.asciidoc index b08113e44a9..10dcc8e3436 100644 --- a/docs/developer-guide/Working-With-Windows.asciidoc +++ b/docs/developer-guide/Working-With-Windows.asciidoc @@ -112,6 +112,11 @@ architecture from either host. and folding identical functions (`/OPT:ICF`). Set `true` to keep debug symbols (a `.pdb` next to the exe) so a native crash address can be symbolized while you are developing. Optimizations stay on in either case. + +|windows.xbox +|Experimental. `true`/`false` (default `false`). Builds a UWP app for an Xbox in Developer Mode +instead of a desktop `.exe`, packaged as an `.msix`. See +<>. Can't be combined with `windows.msix`. |=== The full cross-platform build-hints reference is in the @@ -177,6 +182,57 @@ NOTE: `osslsigncode` must be on the build host's `PATH` (or set `CN1_OSSLSIGNCOD to its location). If signing is requested but the certificate or tool is missing, the build fails with a clear message rather than shipping an unsigned exe by surprise. +[[xbox-developer-mode]] +=== Xbox in Developer Mode + +WARNING: This target is *experimental*. It's a first step toward gaming consoles +(see <>), and its build hint, behaviour and packaging can change. +Continuous integration checks that the package installs and starts on Windows; +it hasn't been verified on a console. + +An Xbox console switched to Developer Mode runs apps you sideload onto it, with no +store submission and no publisher program. The console accepts UWP apps only, so +`windows.xbox=true` builds the same native port as a UWP app: + +* The app runs inside the Windows app container on a `CoreWindow`, rendering +through Direct2D into a Direct3D swap chain. It still runs on a Windows desktop, +which is the quickest place to try a build. +* The controller drives the UI. The D-pad and left stick move focus, *A* fires, +and *B* is the back key, so `Display.getGameAction()` and the back command work +without code for the controller. +* `Display.getDisplaySafeArea()` reports the region the console keeps clear of TV +overscan; lay out important content inside it. +* The result is `.msix`, signed. Without a signing certificate the build +creates a self-signed development certificate whose subject is the package +publisher, and writes it beside the package as `-dev.cer`. Set +`windows.msix.publisher` and `windows.msix.pfx` to sign with your own. + +[source,properties] +---- +include::../demos/common/src/main/snippets/developer-guide/working-with-windows.properties[tag=working-with-windows-properties-002,indent=0] +---- + +Features that need a desktop report unsupported in this build: secondary windows, +menus, the tray, native widgets and text editors, file dialogs, the clipboard, +printing, the camera, audio recording and the share sheet. Files live in the app's +local folder, which is the only file system root. + +==== Installing on the console + +. On the console, install the *Dev Home* app and switch to Developer Mode as +Microsoft describes, then turn on *Remote Access* (Device Portal) in Dev Home. +. Open the Device Portal address Dev Home shows (`https://:11443`) +in a browser on the same network. +. Under *Home*, select *Add* and pick the `.msix`. When asked for a certificate, +pick the `.cer` that came with it. +. The app appears in Dev Home's list and among the console's games and apps; launch it from +either. + +To try the package on a Windows desktop first, trust the certificate (import the +`.cer` into *Local Machine* > *Trusted People*), then install the package with +`Add-AppxPackage .msix` in PowerShell, with Developer Mode enabled in +Windows settings. + === Native Windows vs. the desktop (JVM) app vs. the jar Codename One can produce three different Windows artifacts from the same project. diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 1dd0adee5cc..34ddb485cc5 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -334,6 +334,8 @@ include::Maven-Appendix-Control-Center.adoc[] include::Maven-Appendix-Rich-Properties.adoc[] +include::Gaming-Consoles.asciidoc[] + = Historical reference include::Working-with-UWP.asciidoc[] diff --git a/docs/developer-guide/languagetool-accept.txt b/docs/developer-guide/languagetool-accept.txt index 2140fac64de..9000de22449 100644 --- a/docs/developer-guide/languagetool-accept.txt +++ b/docs/developer-guide/languagetool-accept.txt @@ -881,6 +881,7 @@ Vulkan Karatsuba enqueues backoff +overscan fieldbus mutexes preallocated diff --git a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java index 81c9df0c3bb..eb463935cfc 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java @@ -439,6 +439,17 @@ public void usesClassMethod(String cls, String method) { boolean msix = "true".equalsIgnoreCase(request.getArg("windows.msix", "false")); File winAppSdk = msix ? resolveWinAppSdkDir() : null; String widgetBoardCompileFlags = msix ? widgetBoardCompileFlags(winAppSdk) : ""; + // windows.xbox=true builds the UWP flavour instead (Xbox Developer Mode + // sideloads UWP packages only): the CoreWindow host, compiled against the + // app partition and linked to WindowsApp.lib, then packed into an MSIX + // with no full-trust entry point -- see buildXboxPackage. The Widgets + // Board provider is a full-trust COM server, so the two cannot combine. + boolean xbox = "true".equalsIgnoreCase(request.getArg("windows.xbox", "false")); + if (xbox && msix) { + throw new BuildException("windows.xbox=true and windows.msix=true cannot be combined: " + + "the Xbox package is a UWP app container, the windows.msix one a full-trust " + + "desktop package with a Widgets Board provider. Build them separately."); + } List configure = new ArrayList(); configure.add("cmake"); @@ -451,6 +462,7 @@ public void usesClassMethod(String cls, String method) { configure.add("Ninja"); configure.add("-DCMAKE_C_COMPILER=" + clangClExecutable()); configure.add("-DCMAKE_CXX_COMPILER=" + clangClExecutable()); + configure.add("-DCN1_WINDOWS_UWP=" + (xbox ? "ON" : "OFF")); if (cross) { addCrossCompileConfigure(configure, triple, arch, xwinSysroot, widgetBoardCompileFlags, msix ? widgetBoardLinkFlags(winAppSdk, arch) : ""); @@ -522,6 +534,9 @@ public void usesClassMethod(String cls, String method) { // windows.msix=true: wrap the signed exe in an MSIX package that also // declares the Windows 11 Widgets Board provider (surfaces.json kinds). buildMsixPackage(request, resDir, arch); + if (xbox) { + buildXboxPackage(request, arch); + } log("Native Windows executable: " + windowsExecutable.getAbsolutePath() + " (" + arch + ")"); return true; } @@ -1036,6 +1051,10 @@ private void signMsixPackage(File msix, BuildRequest request) { } String password = request.getArg("windows.msix.password", request.getArg("windows.signing.password", request.getCertificatePassword())); + signMsixWith(msix, pkcs12, password, request.getArg("windows.signing.digest", "sha256")); + } + + private void signMsixWith(File msix, File pkcs12, String password, String digest) { if (password == null) { password = ""; } @@ -1052,7 +1071,7 @@ private void signMsixPackage(File msix, BuildRequest request) { cmd.add("-pass"); cmd.add(password); cmd.add("-h"); - cmd.add(request.getArg("windows.signing.digest", "sha256")); + cmd.add(digest); cmd.add("-in"); cmd.add(msix.getAbsolutePath()); cmd.add("-out"); @@ -1076,6 +1095,190 @@ private void signMsixPackage(File msix, BuildRequest request) { } } + /** + * windows.xbox=true: packs the UWP exe into an MSIX that Xbox Developer + * Mode can sideload through Device Portal (or a desktop through + * Add-AppxPackage). The package is an app-container UWP app -- no + * runFullTrust, a Windows.Universal target -- because that is the only kind + * the console runs outside the GDK. + * + * Signing follows windows.msix.pfx / the exe signing configuration. With + * no certificate at all a self-signed development certificate is created + * whose subject is the manifest Publisher, the package is signed with it + * and its public half is written beside the package as + * {@code
-dev.cer}: Device Portal asks for exactly that file + * when installing, and a desktop must trust it before Add-AppxPackage. + */ + private void buildXboxPackage(BuildRequest request, String arch) { + log("Packaging UWP MSIX for Xbox Developer Mode (windows.xbox=true)"); + // A package installs only when its manifest Publisher equals the signing + // certificate's subject. The generated dev certificate is made to match; + // a supplied one (often the exe's Authenticode certificate) is not, and + // guessing would ship a package every install refuses. + boolean hasCert = request.getArg("windows.msix.pfx", + request.getArg("windows.signing.pkcs12", null)) != null + || (request.getCertificate() != null && request.getCertificate().length > 0); + String publisherHint = request.getArg("windows.msix.publisher", null); + if (hasCert && (publisherHint == null || publisherHint.trim().length() == 0)) { + throw new BuildException("windows.xbox=true is signing with your certificate, so set " + + "windows.msix.publisher to that certificate's subject exactly as Windows shows " + + "it (for example CN=Example, O=Example Ltd); the console refuses a package whose " + + "Publisher differs. Leave out the certificate to have a development one created."); + } + try { + File layout = new File(getBuildDirectory(), "uwp-layout"); + File assets = new File(layout, "Assets"); + assets.mkdirs(); + copy(windowsExecutable, new File(layout, "app.exe")); + byte[] icon = request.getIcon(); + if (icon == null || icon.length == 0) { + throw new BuildException("windows.xbox=true needs the app icon to generate the " + + "package logos, but the build request carries no icon."); + } + createFile(new File(assets, "StoreLogo.png"), icon); + createFile(new File(assets, "Square150x150Logo.png"), icon); + createFile(new File(assets, "Square44x44Logo.png"), icon); + createFile(new File(assets, "SplashScreen.png"), icon); + String publisher = xboxPublisher(request); + createFile(new File(layout, "AppxManifest.xml"), + buildXboxAppxManifest(request, arch, publisher).getBytes(StandardCharsets.UTF_8)); + + File msix = new File(resultDir, request.getMainClass() + ".msix"); + String makemsix = System.getenv("CN1_MAKEMSIX"); + if (makemsix == null || makemsix.isEmpty()) { + makemsix = "makemsix"; + } + if (!exec(getBuildDirectory(), 600000, makemsix, "pack", + "-d", layout.getAbsolutePath(), "-p", msix.getAbsolutePath()) || !msix.isFile()) { + throw new BuildException("MSIX packaging failed. Ensure the `makemsix` tool " + + "(from the msix-packaging project) is on PATH, or point CN1_MAKEMSIX at it."); + } + if (hasCert) { + signMsixPackage(msix, request); + } else { + File pfx = createXboxDevCertificate(new File(getBuildDirectory(), "uwp-devcert"), publisher, + new File(resultDir, request.getMainClass() + "-dev.cer")); + signMsixWith(msix, pfx, "", "sha256"); + } + log("Xbox package: " + msix.getAbsolutePath()); + } catch (BuildException ex) { + throw ex; + } catch (Exception ex) { + throw new BuildException("Failed to build the Xbox MSIX package", ex); + } + } + + /** + * The manifest Publisher: windows.msix.publisher, else a CN made of the + * display name reduced to characters that need no escaping in a + * distinguished name (a comma or quote there would make it a different + * name from the certificate subject, and the install would be refused). + */ + static String xboxPublisher(BuildRequest request) { + String hint = request.getArg("windows.msix.publisher", null); + if (hint != null && hint.trim().length() > 0) { + return hint.trim(); + } + String cn = request.getDisplayName() == null ? "" : request.getDisplayName().replaceAll("[^A-Za-z0-9 ._-]", "").trim(); + return "CN=" + (cn.length() == 0 ? "CodenameOneDev" : cn); + } + + /** + * A self-signed code-signing certificate for sideloading, made with + * openssl, in {@code dir}. Returns the PKCS#12 (empty password) and writes + * the DER certificate to {@code cerOut}. Package visible because CI creates + * one through this method and installs a package signed with it on Windows. + */ + File createXboxDevCertificate(File dir, String publisher, File cerOut) throws Exception { + dir.mkdirs(); + File key = new File(dir, "key.pem"); + File cert = new File(dir, "cert.pem"); + File pfx = new File(dir, "dev.pfx"); + // "CN=a, O=b" -> "/O=b/CN=a": openssl -subj encodes in the order given, + // while Windows writes a subject most specific first (the reverse of the + // encoding) when it compares it with the manifest Publisher. + StringBuilder subj = new StringBuilder(); + String[] rdns = publisher.split(","); + for (int i = rdns.length - 1; i >= 0; i--) { + subj.append('/').append(rdns[i].trim()); + } + String openssl = System.getenv("CN1_OPENSSL"); + if (openssl == null || openssl.isEmpty()) { + openssl = "openssl"; + } + boolean ok = exec(dir, 120000, openssl, "req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", key.getAbsolutePath(), "-out", cert.getAbsolutePath(), "-days", "365", + "-subj", subj.toString(), + "-addext", "basicConstraints=critical,CA:FALSE", + "-addext", "keyUsage=critical,digitalSignature", + "-addext", "extendedKeyUsage=codeSigning") + && exec(dir, 120000, openssl, "pkcs12", "-export", "-inkey", key.getAbsolutePath(), + "-in", cert.getAbsolutePath(), "-out", pfx.getAbsolutePath(), "-passout", "pass:") + && exec(dir, 120000, openssl, "x509", "-in", cert.getAbsolutePath(), + "-outform", "DER", "-out", cerOut.getAbsolutePath()); + if (!ok || !pfx.isFile() || !cerOut.isFile()) { + throw new BuildException("Could not create the Xbox development certificate with openssl " + + "(1.1.1 or newer; CN1_OPENSSL overrides). Alternatively supply windows.msix.pfx " + + "and windows.msix.password with a certificate whose subject is " + publisher + "."); + } + log("Created a self-signed development certificate " + cerOut.getName() + + "; install it with the package (Device Portal asks for it)."); + return pfx; + } + + /** + * The UWP AppxManifest for windows.xbox=true. Windows.Universal covers the + * console and a desktop alike (Windows.Xbox alone would refuse to install + * on the desktop the package is tested on); 10.0.17763 is the oldest + * console OS Developer Mode still runs. The entry point is the exe itself: + * a CoreApplication app needs no activatable class. + */ + static String buildXboxAppxManifest(BuildRequest request, String arch, String publisher) { + String identityName = request.getArg("windows.msix.identityName", request.getPackageName()); + String version = normalizeMsixVersion(request.getArg("windows.msix.version", + request.getVersion() != null ? request.getVersion() : "1.0.0.0")); + String displayName = escapeXml(request.getDisplayName()); + String procArch = ARCH_ARM64.equals(normalizeArch(arch)) ? "arm64" : "x64"; + StringBuilder sb = new StringBuilder(); + sb.append("\n") + .append("\n") + .append(" \n") + .append(" \n") + .append(" ").append(displayName).append("\n") + .append(" ").append(escapeXml(request.getVendor() != null + ? request.getVendor() : request.getDisplayName())).append("\n") + .append(" Assets\\StoreLogo.png\n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append(" \n") + .append("\n"); + return sb.toString(); + } + /** Pads/truncates a version string to the four numeric parts MSIX requires. */ static String normalizeMsixVersion(String version) { String[] parts = (version == null ? "1.0.0.0" : version).split("\\."); diff --git a/maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java b/maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java index a6b14ab280d..111a1f76a7a 100644 --- a/maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java +++ b/maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java @@ -87,4 +87,74 @@ void restrictedCalendarCapabilityRequiresExplicitOptIn() throws Exception { assertTrue(withOptIn.contains("")); assertFalse(withOptIn.contains("")); } + + @Test + void xboxManifestIsAnAppContainerUwpPackage() { + BuildRequest request = new BuildRequest(); + request.setPackageName("com.example.xbox"); + request.setDisplayName("Pad, \"Test\""); + request.setVendor("Example"); + request.setVersion("2.1"); + String publisher = WindowsNativeBuilder.xboxPublisher(request); + // the display name's comma and quotes would make a different DN than the cert's + assertEquals("CN=Pad Test", publisher); + String manifest = WindowsNativeBuilder.buildXboxAppxManifest(request, "x64", publisher); + assertTrue(manifest.contains("Publisher=\"CN=Pad Test\"")); + assertTrue(manifest.contains("")); + // Xbox runs nothing full trust outside the GDK + assertFalse(manifest.contains("runFullTrust")); + assertFalse(manifest.contains("FullTrustApplication")); + + request.putArgument("windows.msix.publisher", "CN=Example, O=Example Ltd"); + assertEquals("CN=Example, O=Example Ltd", WindowsNativeBuilder.xboxPublisher(request)); + } + + /// windows-cross-compile.yml installs and launches a UWP package on a Windows + /// runner, which has no makemsix and no build-engine build, so it packs with + /// this committed manifest. Pinning it to the generator keeps that run a test + /// of the manifest the builder actually ships. + @Test + void xboxSmokeManifestMatchesTheGenerator() throws Exception { + BuildRequest request = new BuildRequest(); + request.setPackageName("com.codename1.uwpsmoke"); + request.setDisplayName("UwpSmoke"); + request.setVendor("Codename One"); + request.setVersion("1.0"); + String generated = WindowsNativeBuilder.buildXboxAppxManifest(request, "x64", + WindowsNativeBuilder.xboxPublisher(request)); + java.io.InputStream in = getClass().getResourceAsStream("xbox-smoke-AppxManifest.xml"); + java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + for (int n = in.read(buf); n > 0; n = in.read(buf)) { + bytes.write(buf, 0, n); + } + in.close(); + assertEquals(generated, new String(bytes.toByteArray(), "UTF-8"), + "regenerate src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml"); + } + + /// Creates the development certificate through the builder's own openssl + /// commands. windows-cross-compile.yml sets CN1_XBOX_DEVCERT_OUT and signs + /// the package it installs on Windows with the result, so a certificate + /// Windows would refuse fails there; elsewhere it runs when openssl is on + /// the PATH and checks the files appear. + @Test + void xboxDevCertificateIsCreatedForTheSmokePublisher() throws Exception { + String out = System.getenv("CN1_XBOX_DEVCERT_OUT"); + java.io.File dir = out != null && out.length() > 0 ? new java.io.File(out) + : java.nio.file.Files.createTempDirectory("xbox-devcert").toFile(); + java.io.File cer = new java.io.File(dir, "dev.cer"); + java.io.File pfx; + try { + pfx = new WindowsNativeBuilder().createXboxDevCertificate(dir, "CN=UwpSmoke", cer); + } catch (BuildException ex) { + org.junit.jupiter.api.Assumptions.assumeTrue(out != null && out.length() > 0, + "openssl is not available: " + ex.getMessage()); + throw ex; + } + assertTrue(pfx.isFile() && pfx.length() > 0); + assertTrue(cer.isFile() && cer.length() > 0); + } } diff --git a/maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml b/maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml new file mode 100644 index 00000000000..93bcbdc747f --- /dev/null +++ b/maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml @@ -0,0 +1,27 @@ + + + + + UwpSmoke + Codename One + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java index 3ff9998de98..fcd6b4bc0f3 100644 --- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java +++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java @@ -437,6 +437,19 @@ static void register(List h) { .type(HintType.STRING) .platform("windows")); + h.add(new Hint("windows.xbox") + .group(HintGroup.WINDOWS) + .type(HintType.BOOLEAN) + .def("false") + .platform("windows") + .doc("Native Windows port only, experimental. true/false (defaults to false). Builds a UWP app for Xbox " + + "Developer Mode instead of a desktop `.exe`: the app runs in the app container on a " + + "CoreWindow, the gamepad drives navigation, and the result is an `.msix` to install " + + "through the console's Device Portal. Without a signing certificate a self-signed " + + "development one is generated and shipped beside the package as `
-dev.cer`. " + + "Desktop-only features (secondary windows, menus, native widgets, file dialogs, " + + "printing, the camera) report unsupported. Cannot be combined with `windows.msix`.")); + h.add(new Hint("windows.sdkRoot") .group(HintGroup.WINDOWS) .type(HintType.STRING) diff --git a/scripts/gen-windows-uwp-stubs.py b/scripts/gen-windows-uwp-stubs.py new file mode 100755 index 00000000000..fdf486562a0 --- /dev/null +++ b/scripts/gen-windows-uwp-stubs.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Codename One designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Codename One through http://www.codenameone.com/ if you +# need additional information or have any questions. +"""Generates Ports/WindowsPort/nativeSources/cn1_windows_uwp_stubs.cpp. + +A UWP (Xbox Developer Mode) build compiles the desktop-only Windows natives out +-- secondary windows, menus, the tray, native widgets, file dialogs, printing, +UI Automation, the camera -- because they are written against Win32 APIs the +app container does not have. Every Java native they define is still referenced +by the translated WindowsNative class, so the build needs a definition of each +one. This script writes them, each answering 0 / null / nothing, which is what +the Java side already reads as "not supported here". + +Generated rather than hand-written so a native added to one of those files +cannot be forgotten: `--check` (run in CI) fails when the committed file is not +what this script would write. + + scripts/gen-windows-uwp-stubs.py # rewrite the stubs + scripts/gen-windows-uwp-stubs.py --check # fail if they are stale +""" +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +NATIVE_DIR = os.path.join(ROOT, "Ports", "WindowsPort", "nativeSources") +OUT = os.path.join(NATIVE_DIR, "cn1_windows_uwp_stubs.cpp") + +# The files a UWP build compiles out. Each carries the matching +# `!defined(CN1_WINDOWS_UWP)` guard; keep the two lists together. +DESKTOP_ONLY = [ + "cn1_windows_accessibility.cpp", + "cn1_windows_audiorec.c", + "cn1_windows_camera.cpp", + "cn1_windows_chrome.cpp", + "cn1_windows_desktopwindow.cpp", + "cn1_windows_edit.c", + "cn1_windows_filedialog.cpp", + "cn1_windows_menu.cpp", + "cn1_windows_notify.c", + "cn1_windows_peer.cpp", + "cn1_windows_print.cpp", + "cn1_windows_widgets.cpp", +] + +# Natives the UWP host (cn1_windows_corewindow.cpp) implements for real even +# though their desktop version lives in one of the files above. +IMPLEMENTED_BY_UWP_HOST = set() + +# Natives where 0 is a success the Java side acts on, so the stub must answer +# the failure value instead. Keyed by the Java method name. Every other stub's +# 0 / null / false was checked to read as "unsupported" at its call sites; a new +# int-returning native needs the same check. +RETURN_OVERRIDES = { + "desktopWindowCreate": "-1", # a slot; 0 is a real window (WindowsWindowManager) + "printDocument": "-1", # 0 is "completed", 1 "cancelled" +} + +DEF_RE = re.compile( + r"^(?:extern \"C\"\s+)?(JAVA_[A-Z]+)\s+(com_codename1_\w+)\s*\(([^)]*)\)\s*\{", + re.M) +GUARD = "!defined(CN1_WINDOWS_UWP)" + +# A file the UWP build keeps can still have desktop-only stretches (io.c's +# clipboard and file dialog). Those are fenced by exactly these two lines, and +# the natives between them are stubbed as well. +REGION_START = "#if !defined(CN1_WINDOWS_UWP) /* uwp: desktop only */" +REGION_END = "#endif /* uwp: end desktop only */" + + +def strip_comments(src): + src = re.sub(r"/\*.*?\*/", lambda m: "\n" * m.group(0).count("\n"), src, flags=re.S) + return re.sub(r"//[^\n]*", "", src) + + +def desktop_only_sources(): + """(file name, source text) for every stretch a UWP build compiles out.""" + for name in sorted(os.listdir(NATIVE_DIR)): + if not name.startswith("cn1_windows") or name == os.path.basename(OUT): + continue + with open(os.path.join(NATIVE_DIR, name), encoding="ascii") as f: + src = f.read() + if name in DESKTOP_ONLY: + if GUARD not in src: + sys.exit("%s is listed as desktop-only but has no %s guard" % (name, GUARD)) + yield name, src + continue + # A near miss on the marker would silently leave a region's natives + # unstubbed, which only a UWP link would then report. + for line in src.splitlines(): + if "uwp: desktop only" in line and line.strip() != REGION_START: + sys.exit("%s: malformed desktop-only marker %r; use exactly %r" + % (name, line.strip(), REGION_START)) + pos = 0 + while True: + start = src.find(REGION_START, pos) + if start < 0: + break + end = src.find(REGION_END, start) + if end < 0: + sys.exit("%s opens a desktop-only region it never closes" % name) + yield name, src[start:end] + pos = end + + +def collect(): + natives = {} + for name, src in desktop_only_sources(): + for m in DEF_RE.finditer(strip_comments(src)): + ret, fn, params = m.group(1), m.group(2), " ".join(m.group(3).split()) + if fn in IMPLEMENTED_BY_UWP_HOST: + continue + if fn in natives: + sys.exit("%s is defined twice (%s and %s)" % (fn, natives[fn][2], name)) + natives[fn] = (ret, params, name) + return natives + + +def java_name(fn): + return fn[len("com_codename1_impl_windows_WindowsNative_"):].split("__")[0] + + +def render(natives): + names = set(java_name(fn) for fn in natives) + for name in RETURN_OVERRIDES: + if name not in names: + sys.exit("RETURN_OVERRIDES names %s, which is not a stubbed native" % name) + out = [] + out.append("""/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * GENERATED by scripts/gen-windows-uwp-stubs.py -- do not edit by hand. + * + * The desktop-only natives, answering "unsupported" in a UWP (Xbox Developer + * Mode) build, where their real definitions are compiled out. See the script + * for why each one has to exist. + */ +#include "cn1_windows.h" + +#if defined(_WIN32) && defined(CN1_WINDOWS_UWP) + +extern "C" { +""") + for fn in sorted(natives): + ret, params, src = natives[fn] + out.append("\n/* %s */\n" % src) + out.append("%s %s(%s) {\n" % (ret, fn, params)) + if ret != "JAVA_VOID": + out.append(" return %s;\n" % RETURN_OVERRIDES.get(java_name(fn), "0")) + out.append("}\n") + out.append(""" +} /* extern "C" */ + +#endif /* _WIN32 && CN1_WINDOWS_UWP */ +""") + return "".join(out) + + +def main(): + text = render(collect()) + if "--check" in sys.argv[1:]: + try: + with open(OUT, encoding="ascii") as f: + current = f.read() + except IOError: + current = None + if current != text: + sys.exit("%s is stale: run scripts/gen-windows-uwp-stubs.py" % os.path.relpath(OUT, ROOT)) + print("UWP stubs are current") + return + with open(OUT, "w", encoding="ascii", newline="\n") as f: + f.write(text) + print("wrote %s" % os.path.relpath(OUT, ROOT)) + + +if __name__ == "__main__": + main() diff --git a/vm/ByteCodeTranslator/src/cn1_sqlite3.c b/vm/ByteCodeTranslator/src/cn1_sqlite3.c index 036f3f1b83b..dfbcb65d7c8 100644 --- a/vm/ByteCodeTranslator/src/cn1_sqlite3.c +++ b/vm/ByteCodeTranslator/src/cn1_sqlite3.c @@ -145,6 +145,59 @@ */ #define AEGIS_OMIT_AES_HARDWARE_SUPPORT 1 +/* + * A UWP build (Xbox Developer Mode) compiles against the app partition, where + * five Win32 calls the amalgamation's os_win syscall table names do not exist. + * Upstream removed its SQLITE_OS_WINRT switch, so they are supplied here from + * what the app container does have. AreFileApisANSI answering FALSE keeps + * SQLite on its UTF-16 paths, so the ANSI CreateFileA is never reached. + */ +#if defined(_WIN32) && defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_APP +/* before any CRT header, as the amalgamation itself does, or rand_s is undeclared */ +#ifndef _CRT_RAND_S +#define _CRT_RAND_S +#endif +#include +static BOOL WINAPI cn1SqliteAreFileApisANSI(void) { return FALSE; } +static HANDLE WINAPI cn1SqliteCreateFileW(LPCWSTR name, DWORD access, DWORD share, + LPSECURITY_ATTRIBUTES sa, DWORD disposition, DWORD flags, HANDLE templ) { + CREATEFILE2_EXTENDED_PARAMETERS ex; + (void)templ; + ZeroMemory(&ex, sizeof(ex)); + ex.dwSize = sizeof(ex); + ex.dwFileAttributes = flags & 0xFFFF; + ex.dwFileFlags = flags & 0xFFF00000; + ex.dwSecurityQosFlags = flags & 0x000F0000; + ex.lpSecurityAttributes = sa; + return CreateFile2(name, access, share, disposition, &ex); +} +static HANDLE WINAPI cn1SqliteCreateFileA(LPCSTR name, DWORD access, DWORD share, + LPSECURITY_ATTRIBUTES sa, DWORD disposition, DWORD flags, HANDLE templ) { + (void)name; (void)access; (void)share; (void)sa; (void)disposition; (void)flags; (void)templ; + SetLastError(ERROR_NOT_SUPPORTED); + return INVALID_HANDLE_VALUE; +} +static DWORD WINAPI cn1SqliteGetFileSize(HANDLE h, LPDWORD high) { + LARGE_INTEGER size; + if (!GetFileSizeEx(h, &size)) { + return INVALID_FILE_SIZE; + } + if (high) { + *high = (DWORD)size.HighPart; + } + return size.LowPart; +} +static BOOL WINAPI cn1SqliteHeapValidate(HANDLE heap, DWORD flags, LPCVOID mem) { + (void)heap; (void)flags; (void)mem; + return TRUE; +} +#define AreFileApisANSI cn1SqliteAreFileApisANSI +#define CreateFileW cn1SqliteCreateFileW +#define CreateFileA cn1SqliteCreateFileA +#define GetFileSize cn1SqliteGetFileSize +#define HeapValidate cn1SqliteHeapValidate +#endif + #include "cn1_sqlite3_amalgamation.h" #endif /* CN1_INCLUDE_SQLITE */ diff --git a/vm/ByteCodeTranslator/src/cn1_win_compat.c b/vm/ByteCodeTranslator/src/cn1_win_compat.c index 088f0736bcf..2bd8440982a 100644 --- a/vm/ByteCodeTranslator/src/cn1_win_compat.c +++ b/vm/ByteCodeTranslator/src/cn1_win_compat.c @@ -401,7 +401,16 @@ long long cn1_win_available_memory(void) { } int cn1_win_cpu_count(void) { +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) + /* GetActiveProcessorCount is desktop only. A UWP app (the Xbox target) is + * never spread over processor groups, so the system info count is the same + * number there. */ + SYSTEM_INFO info; + GetNativeSystemInfo(&info); + return (int) info.dwNumberOfProcessors; +#else return (int) GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); +#endif } void cn1_win_current_stack_limits(void** low, void** high) { diff --git a/vm/ByteCodeTranslator/src/cn1_win_compat.h b/vm/ByteCodeTranslator/src/cn1_win_compat.h index 7ad7d1dc49e..9025a502011 100644 --- a/vm/ByteCodeTranslator/src/cn1_win_compat.h +++ b/vm/ByteCodeTranslator/src/cn1_win_compat.h @@ -47,6 +47,22 @@ #include /* _putenv_s */ #include /* FILE, for cn1_win_fopen_utf8 */ +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +/* A UWP app has no process environment and the app-partition CRT declares no + * getenv. The runtime only reads diagnostic switches from it, and "not set" is + * the true answer inside the app container. */ +static __inline char* cn1_uwp_getenv(const char* name) { + (void)name; + return NULL; +} +#define getenv cn1_uwp_getenv +/* No TZ to re-read either (setenv is a no-op here), so the zone switch in + * nativeMethods resolves against the system zone; tzset has nothing to do. */ +static __inline void cn1_uwp_tzset(void) { +} +#define tzset cn1_uwp_tzset +#endif + #ifdef __cplusplus extern "C" { #endif @@ -195,6 +211,20 @@ FILE* cn1_win_fopen_utf8(const char* path, const char* mode); /* --- environment / time.h POSIX helpers absent from MSVC --- Thin static-inline wrappers over the MSVC equivalents; used by the date / timezone runtime in nativeMethods. */ +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +/* A UWP app (the Xbox Developer Mode target) has no process environment: the + * app-partition CRT declares neither _putenv_s nor getenv. Reporting failure + * leaves TZ unset, and the Windows named-zone path never reads it anyway. */ +static __inline int setenv(const char* name, const char* value, int overwrite) { + (void)name; (void)value; (void)overwrite; + return -1; +} + +static __inline int unsetenv(const char* name) { + (void)name; + return -1; +} +#else static __inline int setenv(const char* name, const char* value, int overwrite) { (void)overwrite; /* the runtime always overwrites, which _putenv_s does */ return _putenv_s(name, value); @@ -203,6 +233,7 @@ static __inline int setenv(const char* name, const char* value, int overwrite) { static __inline int unsetenv(const char* name) { return _putenv_s(name, ""); } +#endif /* Civil fields to a UTC epoch second, by arithmetic rather than through the * CRT. _mkgmtime is documented to support only dates from 1970-01-01 and diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java index fa8a3e8f2c5..90bf7b789c8 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java @@ -1808,6 +1808,13 @@ public String generateCCode(List allClasses) { clInitMethod = clsName + "_" + m.getMethodName() + "__"; } if(m.isMain()) { + // A UWP build (the Xbox Developer Mode target) cannot start in + // main: the process has to hand its first thread to + // CoreApplication::Run, which calls back on that same thread once + // the window exists. cn1_windows_corewindow.cpp owns the real main + // and calls this one, renamed, from there. Every other target leaves + // CN1_WINDOWS_UWP undefined and gets main unchanged. + b.append("\n#if defined(CN1_WINDOWS_UWP)\n#define main cn1JavaMain\n#endif\n"); b.append("\nint main(int argc, char *argv[]) {\n"); // Line-buffer stdout/stderr. C streams block-buffer when they are // not a tty, so everything an app logs into a pipe -- which is diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index bba61e5dd4f..038788d960b 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -1453,7 +1453,31 @@ private static void writeCmakeProject(File projectRoot, File srcRoot, String app // saw it because the cross-compile leg sets WEBVIEW2_SDK_DIR and got the // library by accident, which is the same way the iOS port once lost // UniformTypeIdentifiers. + // CN1_WINDOWS_UWP (WindowsNativeBuilder passes it for windows.xbox=true) + // builds for the UWP app container that Xbox Developer Mode runs: + // the app API partition, CoreWindow host (cn1_windows_corewindow.cpp), + // desktop-only natives stubbed (cn1_windows_uwp_stubs.cpp). It links + // WindowsApp.lib and nothing else that imports a DLL, so the link + // itself is the proof that every import exists in the container -- a + // desktop-only call fails here, not on the console's loader. The CRT + // is static (/MT): the app-CRT import libs are not part of the SDK + // layout we build against, and a static CRT needs no VCLibs framework + // package next to the app. SQLite has no switch for this partition any more; + // cn1_sqlite3.c supplies the few file calls it lacks. + writer.append(" option(CN1_WINDOWS_UWP \"Build for the UWP app container (Xbox Developer Mode)\" OFF)\n"); + writer.append(" if(CN1_WINDOWS_UWP)\n"); + writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_WINDOWS_UWP=1 WINAPI_FAMILY=WINAPI_FAMILY_APP)\n"); + writer.append(" target_link_libraries(${PROJECT_NAME} WindowsApp uuid mfuuid dxguid)\n"); + writer.append(" target_link_options(${PROJECT_NAME} PRIVATE /APPCONTAINER /NODEFAULTLIB:kernel32.lib)\n"); + writer.append(" foreach(CN1_FLAG_VAR CMAKE_C_FLAGS_RELEASE CMAKE_CXX_FLAGS_RELEASE CMAKE_C_FLAGS_RELWITHDEBINFO CMAKE_CXX_FLAGS_RELWITHDEBINFO CMAKE_C_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_MINSIZEREL)\n"); + writer.append(" string(REPLACE \"/MD\" \"/MT\" ${CN1_FLAG_VAR} \"${${CN1_FLAG_VAR}}\")\n"); + writer.append(" endforeach()\n"); + writer.append(" foreach(CN1_FLAG_VAR CMAKE_C_FLAGS_DEBUG CMAKE_CXX_FLAGS_DEBUG)\n"); + writer.append(" string(REPLACE \"/MDd\" \"/MTd\" ${CN1_FLAG_VAR} \"${${CN1_FLAG_VAR}}\")\n"); + writer.append(" endforeach()\n"); + writer.append(" else()\n"); writer.append(" target_link_libraries(${PROJECT_NAME} d2d1 dwrite dxgi windowscodecs winhttp ws2_32 user32 gdi32 ole32 oleaut32 uuid mf mfplat mfreadwrite mfuuid shell32 comdlg32 crypt32 bcrypt ncrypt winmm runtimeobject dbghelp advapi32 dwmapi)\n"); + writer.append(" endif()\n"); // BrowserComponent is backed by WebView2 (cn1_windows_browser.cpp), // gated on the SDK being present: when WEBVIEW2_SDK_DIR points at a // Microsoft.Web.WebView2 build/native folder we link the static @@ -1461,7 +1485,8 @@ private static void writeCmakeProject(File projectRoot, File srcRoot, String app // browser natives compile as stubs and the port reports the browser // as unsupported. version/shell32/advapi32/shlwapi back the loader; advapi32 // is in the unconditional list above as well, because the port itself needs it. - writer.append(" if(DEFINED ENV{WEBVIEW2_SDK_DIR} AND EXISTS \"$ENV{WEBVIEW2_SDK_DIR}/include/WebView2.h\")\n"); + // WebView2 hosts in an HWND, which a UWP build has none of. + writer.append(" if(NOT CN1_WINDOWS_UWP AND DEFINED ENV{WEBVIEW2_SDK_DIR} AND EXISTS \"$ENV{WEBVIEW2_SDK_DIR}/include/WebView2.h\")\n"); writer.append(" target_include_directories(${PROJECT_NAME} PRIVATE \"$ENV{WEBVIEW2_SDK_DIR}/include\")\n"); writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_HAVE_WEBVIEW2=1)\n"); // VsDevCmd sets VSCMD_ARG_TGT_ARCH to the *target* arch (arm64/x64), diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index c1fa2210c75..b9bfc151dd6 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -756,17 +756,52 @@ void crossCompilesWindowsExeWithXwin() throws Exception { // --- translate (host-agnostic): compile a real Form app against core+port, // stage the native layer, run the translator with the "windows" app type --- + Path cmakeRoot = translateWindowsApp("WinFormApp", winFormAppSource(), coreClasses, portClasses, config); + + // --- cross-compile that dist to a Windows x64 PE with clang-cl + xwin --- + Path exe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp"); + System.out.println("CN1_XWIN_EXE=" + exe.toAbsolutePath() + " (" + (Files.size(exe) / 1024) + "KB)"); + + // --- and the UWP (Xbox Developer Mode) build of the same dist. It links + // WindowsApp.lib and nothing else that imports a DLL, so a native that + // calls outside the app container fails this link rather than the + // console's loader. --- + Path uwpExe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp", true); + System.out.println("CN1_XWIN_UWP_EXE=" + uwpExe.toAbsolutePath() + " (" + (Files.size(uwpExe) / 1024) + "KB)"); + + // --- the UWP smoke app the Windows CI job installs and launches as a real + // UWP activation (windows-cross-compile.yml). It writes a marker into its + // package LocalState once its first form has painted, which is the only + // place outside the container the job can read it from. --- + String uwpOut = System.getenv("CN1_UWP_EXE_OUT"); + if (uwpOut != null && !uwpOut.trim().isEmpty()) { + Path smokeRoot = translateWindowsApp("UwpSmokeApp", uwpSmokeAppSource(), coreClasses, portClasses, config); + Path smokeExe = crossBuildDist(smokeRoot, sys, clangCl, "UwpSmokeApp", true); + Path dest = Paths.get(uwpOut.trim()); + if (dest.getParent() != null) { + Files.createDirectories(dest.getParent()); + } + Files.copy(smokeExe, dest, StandardCopyOption.REPLACE_EXISTING); + System.out.println("UWP smoke exe copied to " + dest.toAbsolutePath()); + } + } + + /// Compiles one app class against core + the Windows port, stages the native + /// sources and runs the translator with the "windows" app type. Answers the + /// emitted CMake root. + static Path translateWindowsApp(String className, String source, Path coreClasses, Path portClasses, + CompilerHelper.CompilerConfig config) throws Exception { Parser.cleanup(); Path sourceDir = Files.createTempDirectory("xwin-src"); Path classesDir = Files.createTempDirectory("xwin-cls"); Path javaApiDir = Files.createTempDirectory("xwin-japi"); - Files.write(sourceDir.resolve("WinFormApp.java"), winFormAppSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve(className + ".java"), source.getBytes(StandardCharsets.UTF_8)); CompilerHelper.compileJavaAPI(javaApiDir, config); assertEquals(0, CompilerHelper.compile(config.jdkHome, Arrays.asList( "-source", config.targetVersion, "-target", config.targetVersion, "-classpath", coreClasses + java.io.File.pathSeparator + portClasses, - "-d", classesDir.toString(), sourceDir.resolve("WinFormApp.java").toString())), - "WinFormApp should compile:\n" + CompilerHelper.getLastErrorLog()); + "-d", classesDir.toString(), sourceDir.resolve(className + ".java").toString())), + className + " should compile:\n" + CompilerHelper.getLastErrorLog()); Path nativeDir = Paths.get("..", "..", "Ports", "WindowsPort", "nativeSources").normalize().toAbsolutePath(); Path nativeStage = Files.createTempDirectory("xwin-native"); try (java.util.stream.Stream s = Files.list(nativeDir)) { @@ -778,13 +813,53 @@ void crossCompilesWindowsExeWithXwin() throws Exception { } Path outputDir = Files.createTempDirectory("xwin-out"); String sources = classesDir + ";" + coreClasses + ";" + portClasses + ";" + javaApiDir + ";" + nativeStage; - runTranslatorMultiSource(sources, outputDir, "WinFormApp", "windows"); + runTranslatorMultiSource(sources, outputDir, className, "windows"); Path cmakeRoot = outputDir.resolve("dist"); assertTrue(Files.exists(cmakeRoot.resolve("CMakeLists.txt")), "translator should emit a CMake project"); - // --- cross-compile that dist to a Windows x64 PE with clang-cl + xwin --- - Path exe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp"); - System.out.println("CN1_XWIN_EXE=" + exe.toAbsolutePath() + " (" + (Files.size(exe) / 1024) + "KB)"); + return cmakeRoot; + } + + /// The app the UWP smoke run launches: one form, then a marker file with the + /// display size in the app home (the package LocalState under UWP), written a + /// cycle after show() so it means the first frame went through. + static String uwpSmokeAppSource() { + return "import com.codename1.ui.Display;\n" + + "import com.codename1.ui.Form;\n" + + "import com.codename1.ui.Label;\n" + + "import com.codename1.ui.Button;\n" + + "import com.codename1.ui.layouts.BoxLayout;\n" + + "import com.codename1.io.FileSystemStorage;\n" + + "import java.io.OutputStream;\n" + + "public class UwpSmokeApp {\n" + + " public static void main(String[] args) {\n" + + " Display.init(null);\n" + + " Display.getInstance().callSerially(new Runnable() {\n" + + " public void run() {\n" + + " Form f = new Form(\"UWP smoke\", BoxLayout.y());\n" + + " f.add(new Label(\"Codename One in the UWP app container\"));\n" + + " f.add(new Button(\"Press A\"));\n" + + " f.show();\n" + + " Display.getInstance().callSerially(new Runnable() {\n" + + " public void run() { writeMarker(); }\n" + + " });\n" + + " }\n" + + " });\n" + + " com.codename1.impl.windows.WindowsImplementation.runMainEventLoop();\n" + + " }\n" + + " static void writeMarker() {\n" + + " Display d = Display.getInstance();\n" + + " String text = \"CN1-UWP-STARTED \" + d.getDisplayWidth() + \"x\" + d.getDisplayHeight() + \"\\n\";\n" + + " try {\n" + + " FileSystemStorage fs = FileSystemStorage.getInstance();\n" + + " OutputStream out = fs.openOutputStream(fs.getAppHomePath() + \"uwp-smoke.txt\");\n" + + " out.write(text.getBytes(\"UTF-8\"));\n" + + " out.close();\n" + + " } catch (Exception e) {\n" + + " e.printStackTrace();\n" + + " }\n" + + " }\n" + + "}\n"; } /** @@ -798,6 +873,13 @@ void crossCompilesWindowsExeWithXwin() throws Exception { * BrowserComponent peer; otherwise the browser natives compile as stubs. */ static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeName) throws Exception { + return crossBuildDist(cmakeRoot, sys, clangCl, exeName, false); + } + + /// `uwp` builds the UWP (Xbox Developer Mode) variant: `-DCN1_WINDOWS_UWP=ON`, + /// which compiles the app API partition and links WindowsApp.lib alone. + static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeName, boolean uwp) + throws Exception { String target = "x86_64-pc-windows-msvc"; String libArch = "x86_64"; String inc = String.join(" ", @@ -821,9 +903,10 @@ static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeN "-I", sys.resolve("crt/include").toString(), "-I", sys.resolve("sdk/include/ucrt").toString()); String llvmRc = System.getenv().getOrDefault("CN1_LLVM_RC", "llvm-rc"); - Path buildDir = cmakeRoot.resolve("xbuild"); + Path buildDir = cmakeRoot.resolve(uwp ? "xbuild-uwp" : "xbuild"); Files.createDirectories(buildDir); runCommand(Arrays.asList("cmake", "-S", cmakeRoot.toString(), "-B", buildDir.toString(), + "-DCN1_WINDOWS_UWP=" + (uwp ? "ON" : "OFF"), "-G", "Ninja", "-DCMAKE_SYSTEM_NAME=Windows", "-DCMAKE_SYSTEM_PROCESSOR=AMD64",