Browse Source

* Allow storage to work in worker environment as well

Michael Van Canneyt 2 months ago
parent
commit
f000e8eb00

File diff suppressed because it is too large
+ 0 - 0
demo/wasienv/storage-threaded/bulma.min.css


+ 41 - 0
demo/wasienv/storage-threaded/index.html

@@ -0,0 +1,41 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+  <title>Webassembly Storage Access demo</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link href="bulma.min.css" rel="stylesheet">
+  <script src="storagemain.js"></script>
+  <style>
+      #pasjsconsole {
+        border-style: solid;
+        border-width: 1px;
+        margin-left: 64px;
+        margin-right: 64px;
+        min-height: 75vh;
+      }
+    </style>
+</head>
+<body>
+  <div class="container">
+    <h3 class="title is-3">Webassembly program output</h3>
+    <p>
+    This program shows how to access the browser's local (or session) storage in a
+    webassembly program.
+    </p>
+    <div class="box">
+      <div id="pasjsconsole"></div>
+    </div>
+  <div>
+    <label for="cbLog">
+      <input id="cbLog" type="checkbox" checked autocomplete="off">
+      Show API log (needs reload)
+    </label>
+  </div>
+  </div>
+  <script>
+    rtl.showUncaughtExceptions=true;
+    window.addEventListener("load", rtl.run);
+  </script>
+</body>
+</html>

+ 91 - 0
demo/wasienv/storage-threaded/storagehost.lpi

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="Webassembly storage demo - host program"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="6">
+      <Item0 Name="BrowserConsole" Value="1"/>
+      <Item1 Name="MaintainHTML" Value="1"/>
+      <Item2 Name="Pas2JSProject" Value="1"/>
+      <Item3 Name="PasJSLocation" Value="$NameOnly($(ProjFile))"/>
+      <Item4 Name="PasJSWebBrowserProject" Value="1"/>
+      <Item5 Name="RunAtReady" Value="1"/>
+    </CustomData>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="storagehost.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="index.html"/>
+        <IsPartOfProject Value="True"/>
+        <CustomData Count="1">
+          <Item0 Name="PasJSIsProjectHTMLFile" Value="1"/>
+        </CustomData>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target FileExt=".js">
+      <Filename Value="storagehost"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="js"/>
+    </SearchPaths>
+    <Parsing>
+      <SyntaxOptions>
+        <AllowLabel Value="False"/>
+        <UseAnsiStrings Value="False"/>
+        <CPPInline Value="False"/>
+      </SyntaxOptions>
+    </Parsing>
+    <CodeGeneration>
+      <TargetOS Value="nodejs"/>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <GenerateDebugInfo Value="False"/>
+        <UseLineInfoUnit Value="False"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-Jeutf-8 -Jirtl.js -Jc -Jminclude"/>
+      <CompilerPath Value="$(pas2js)"/>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 71 - 0
demo/wasienv/storage-threaded/storagehost.lpr

@@ -0,0 +1,71 @@
+{
+    This file is part of the Free Component Library
+
+    Webassembly Storage API - Demo program
+    Copyright (c) 2025 by Michael Van Canneyt [email protected]
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program 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.
+
+ **********************************************************************}
+
+program storagehost;
+
+{$mode objfpc}
+{$modeswitch externalclass}
+
+uses
+  wasiworkerapp, JS, Classes, SysUtils, Web, wasm.pas2js.storage,
+  rtl.WorkerCommands, pas2js.storagebridge.worker;
+
+Type
+  { TMyApplication }
+
+  TMyApplication = class(TWorkerWASIHostApplication)
+    FStorage : TStorageAPI;
+  private
+    procedure HandleConsoleWrite(Sender: TObject; aOutput: string);
+  protected
+    procedure RegisterMessageHandlers; override;
+    procedure DoRun; override;
+  public
+    Constructor Create(aOwner : TComponent); override;
+  end;
+
+procedure TMyApplication.DoRun;
+
+begin
+  Terminate;
+  StartWebAssembly('demostorage.wasm');
+end;
+
+procedure TMyApplication.HandleConsoleWrite(Sender: TObject; aOutput: string);
+begin
+  TCommandDispatcher.Instance.SendConsoleCommand(TConsoleOutputCommand.create(aOutput));
+end;
+
+procedure TMyApplication.RegisterMessageHandlers;
+begin
+  inherited RegisterMessageHandlers;
+end;
+
+constructor TMyApplication.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  Host.OnConsoleWrite:=@HandleConsoleWrite;
+  FStorage:=TStorageAPI.Create(WasiEnvironment);
+  FStorage.LogAPI:=FStorage.LogAPI or (TWorkerStorageBridge._LocalStorage.getItem('showlog')='1');
+end;
+
+var
+  Application : TMyApplication;
+
+begin
+  Application:=TMyApplication.Create(nil);
+  Application.Initialize;
+  Application.Run;
+end.

+ 85 - 0
demo/wasienv/storage-threaded/storagemain.lpi

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="storagemain"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="3">
+      <Item0 Name="Pas2JSProject" Value="1"/>
+      <Item1 Name="PasJSLocation" Value="$NameOnly($(ProjFile))"/>
+      <Item2 Name="PasJSWebBrowserProject" Value="1"/>
+    </CustomData>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="storagemain.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="index.html"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target FileExt=".js">
+      <Filename Value="storagemain"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="js"/>
+    </SearchPaths>
+    <Parsing>
+      <SyntaxOptions>
+        <AllowLabel Value="False"/>
+        <UseAnsiStrings Value="False"/>
+        <CPPInline Value="False"/>
+      </SyntaxOptions>
+    </Parsing>
+    <CodeGeneration>
+      <TargetOS Value="browser"/>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <GenerateDebugInfo Value="False"/>
+        <UseLineInfoUnit Value="False"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-Jeutf-8 -Jirtl.js -Jc -Jminclude"/>
+      <CompilerPath Value="$(pas2js)"/>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 42 - 0
demo/wasienv/storage-threaded/storagemain.lpr

@@ -0,0 +1,42 @@
+program storagemain;
+
+{$mode objfpc}
+
+uses
+  Classes, BrowserConsole, BrowserApp, web, Rtl.WorkerCommands, pas2js.storagebridge.main;
+
+Type
+
+  { TApplication }
+
+  TApplication = class(TBrowserApplication)
+  private
+    procedure HandleConsoleCommand(aCmd: TConsoleOutputCommand);
+  protected
+    FWorker: TJSWorker;
+  Public
+    procedure DoRun; override;
+  end;
+
+{ TApplication }
+
+procedure TApplication.HandleConsoleCommand(aCmd : TConsoleOutputCommand);
+begin
+  Writeln('[Worker] ',aCmd.ConsoleMessage);
+end;
+
+procedure TApplication.DoRun;
+begin
+  Terminate;
+  FWorker:=TJSWorker.New('storagehost.js');
+  TCommandDispatcher.instance.RegisterWorker(FWorker,'storage');
+  TCommandDispatcher.instance.specialize AddCommandHandler<TConsoleOutputCommand>(cmdConsole,@HandleConsoleCommand);
+end;
+
+begin
+  With TApplication.Create(Nil) do
+    begin
+    Initialize;
+    Run;
+    end;
+end.

+ 122 - 0
packages/wasm-utils/src/pas2js.storagebridge.main.pas

@@ -0,0 +1,122 @@
+unit pas2js.storagebridge.main;
+
+{$mode ObjFPC}
+
+interface
+
+uses
+  JS, Rtl.WorkerCommands, pas2js.storagebridge.shared, web;
+
+Type
+  { TMainStorageBridge }
+
+  TMainStorageBridge = class (TObject)
+  Private
+    class var _instance : TMainStorageBridge;
+  private
+    function GetStorage(aKind: Integer): TJSStorage;
+  protected
+    procedure HandleLocalStorageCommand(aCmd: TLocalStorageCommand); virtual;
+    procedure RegisterMessageHandlers; virtual;
+
+  public
+    constructor create;
+    class procedure Init;
+    class property Instance : TMainStorageBridge Read _Instance;
+  end;
+
+implementation
+
+{ TMainStorageBridge }
+function TMainStorageBridge.GetStorage(aKind : Integer) : TJSStorage;
+begin
+  Case aKind of
+    Ord(skLocal) : Result:=Window.localStorage;
+    Ord(skSession) : Result:=Window.sessionStorage;
+  else
+    Result:=nil;
+  end;
+end;
+
+procedure TMainStorageBridge.HandleLocalStorageCommand(aCmd : TLocalStorageCommand);
+
+var
+  lStorage : TJSStorage;
+  i, lError, lResultLen : Integer;
+  lResult : string;
+  lArr : TJSUint16Array;
+
+begin
+  lResult:='';
+  lResultLen:=0;
+  lStorage:=GetStorage(aCmd.Kind);
+  lError:=ESTORAGE_SUCCESS;
+  if lStorage=Nil then
+    lError:=ESTORAGE_KIND
+  else
+    case aCmd.FuncName of
+      FNClear :
+        lStorage.Clear;
+      FNLength :
+        lResultLen:=lStorage.Length;
+      FNKey :
+        begin
+        lResult:=lStorage.Key(Integer(aCmd.Args[0]));
+        if isNull(lResult) then
+          begin
+          lResultLen:=0;
+          lResult:='';
+          end
+        else
+          lResultLen:=Length(lResult);
+        end;
+      FNRemoveItem :
+        lStorage.removeItem(String(aCmd.Args[0]));
+      FNGetItem :
+        begin
+        lResult:=lStorage.getItem(String(aCmd.Args[0]));
+        if isNull(lResult) then
+          begin
+          lResultLen:=0;
+          lResult:='';
+          end
+        else
+          lResultLen:=Length(lResult);
+        end;
+      FNSetItem :
+        begin
+        lStorage.setItem(String(aCmd.Args[0]),String(aCmd.Args[1]));
+        end;
+    end;
+  aCmd.ResultData[CallResult]:=lError;
+  aCmd.ResultData[CallResultLen]:=lResultLen;
+  if lResult<>'' then
+    begin
+    lArr:=TJSUint16Array.New(aCmd.ResultData.buffer,CallResultData*4);
+    for I:=0 to Length(lResult)-1 do
+      lArr[i]:=TJSString(lResult).charCodeAt(i);
+    end;
+  aCmd.ResultData[CallLock]:=1;
+  TJSAtomics.notify(aCmd.ResultData,CallLock);
+end;
+
+procedure TMainStorageBridge.RegisterMessageHandlers;
+begin
+  TCommandDispatcher.Instance.specialize AddCommandHandler<TLocalStorageCommand>(cmdLocalStorage,@HandleLocalStorageCommand);
+end;
+
+constructor TMainStorageBridge.create;
+begin
+  RegisterMessageHandlers;
+end;
+
+class procedure TMainStorageBridge.Init;
+begin
+  _Instance:=TMainStorageBridge.Create;
+end;
+
+initialization
+  TMainStorageBridge.Init;
+
+end.
+

+ 53 - 0
packages/wasm-utils/src/pas2js.storagebridge.shared.pas

@@ -0,0 +1,53 @@
+unit pas2js.storagebridge.shared;
+
+{$mode ObjFPC}
+{$modeswitch externalclass}
+
+interface
+
+uses
+  Types, Rtl.WorkerCommands, JS;
+
+const
+  cmdLocalStorage = 'localstorage';
+
+  FNClear   = 'clear';
+  FNLength  = 'length';
+  FNSetItem = 'set_item';
+  FNGetItem = 'get_item';
+  FNRemoveItem = 'remove_item';
+  FNKey     = 'key';
+
+  LocalStorageBufferSize = 1024*1024; // 1 mb
+
+  {
+    Result data:
+    Lock
+    Result
+    ResultLength
+    [Data]
+  }
+  CallLock       = 0;
+  CallResult     = 1;
+  CallResultLen  = 2;
+  CallResultData = 3;
+
+  ESTORAGE_SUCCESS = 0;
+  ESTORAGE_KIND    = -1;
+
+Type
+  TStorageKind = (skLocal,skSession);
+
+  TLocalStorageCommand = Class External name 'TObject' (TCustomWorkerCommand)
+    Kind : integer; // Ord TStorageKind
+    ID : Integer; external name 'id';
+    ResultData : TJSInt32Array; external name 'atomic';
+    FuncName : String; external name 'funcName';
+    Args : TJSValueDynArray; external name 'args';
+  end;
+
+
+implementation
+
+end.
+

+ 150 - 0
packages/wasm-utils/src/pas2js.storagebridge.worker.pas

@@ -0,0 +1,150 @@
+unit pas2js.storagebridge.worker;
+
+{$mode ObjFPC}
+{$modeswitch externalclass}
+
+interface
+
+uses
+  types, js, webworker, rtl.WorkerCommands, pas2js.storagebridge.shared;
+
+Type
+  TMainThreadCallFunc = reference to function: Integer;
+
+  { TStorageBridge }
+
+  { TWorkerStorageBridge }
+
+  TWorkerStorageBridge = class (TObject)
+  Private
+    FCallID : Integer;
+    FKind : TStorageKind;
+  Protected
+    function DoMainThreadBlockingCall(const aFuncName: String; aArgs: array of JSValue): Integer;
+  public
+    class var _LocalStorage : TWorkerStorageBridge;
+    class var _SessionStorage : TWorkerStorageBridge;
+    // there can be only one call at a time, so a single global buffer is sufficient.
+    class var _AtomicBuffer : TJSSharedArrayBuffer;
+    class var FResultData : TJSInt32Array;
+  public
+    constructor create(aKind : TStorageKind);
+    class procedure init;
+    function Key(aKey : Integer) : string;
+    function GetItem(aKey : String) : String;
+    procedure SetItem(aKey,aValue : String);
+    procedure RemoveItem(aKey : String);
+    function count : Integer;
+    procedure clear;
+  end;
+
+implementation
+
+function Array_prototype_slice(val : JSValue) : TJSValueDynArray; external name 'Array.prototype.slice.call';
+
+function TWorkerStorageBridge.DoMainThreadBlockingCall(const aFuncName: String; aArgs: array of JSValue): Integer;
+
+var
+  lCmd : TLocalStorageCommand;
+begin
+  Inc(FCallID);
+  lCmd:=TLocalStorageCommand(TCustomWorkerCommand.createCommand(cmdLocalStorage));
+  lCmd.Kind:=Ord(FKind);
+  lCmd.ID := FCallID;
+  lCmd.FuncName := aFuncName;
+  lCmd.Args := aArgs;
+  lCmd.ResultData := FResultData;
+  TJSAtomics.store(FResultData, CallLock, 0);
+  TCommandDispatcher.Instance.SendCommand(lCmd);
+  TJSAtomics.wait(FResultData, CallLock, 0);
+  Result := TJSAtomics.load(FResultData, CallResult);
+  if Result<0 then
+    Writeln(ClassName,': Could not execute function "',aFuncName,'"');
+end;
+
+constructor TWorkerStorageBridge.create(aKind: TStorageKind);
+begin
+  FKind:=aKind;
+  FResultData:=TJSInt32Array.New(_AtomicBuffer);
+end;
+
+procedure TWorkerStorageBridge.clear;
+begin
+  DoMainThreadBlockingCall(FNClear,[]);
+end;
+
+function TWorkerStorageBridge.count: Integer;
+begin
+  if DoMainThreadBlockingCall(FNlength,[])<>0 then
+    Result:=0
+  else
+    Result:=Self.FResultData[CallResultLen];
+end;
+
+procedure TWorkerStorageBridge.SetItem(aKey, aValue: String);
+begin
+  DoMainThreadBlockingCall(FNSetItem,[aKey,aValue]);
+end;
+
+procedure TWorkerStorageBridge.RemoveItem(aKey: String);
+begin
+  DoMainThreadBlockingCall(FNRemoveItem,[aKey]);
+end;
+
+function TWorkerStorageBridge.Key(aKey: Integer): string;
+var
+  lResultLen : integer;
+  lStringArray :TJSUint16Array;
+begin
+  if DoMainThreadBlockingCall(FNKey,[aKey])<>0 then
+    Exit('');
+  lResultLen:=Self.FResultData[CallResultLen];
+  if lResultLen<=0 then
+    Exit('');
+  lStringArray:=TJSUint16Array.new(_AtomicBuffer, CallResultData*4, lResultLen);
+  Result := String(TJSFunction(@TJSString.fromCharCode).apply(nil, TJSValueDynArray(lStringArray)));
+end;
+
+function TWorkerStorageBridge.GetItem(aKey: String): String;
+var
+  lResultLen : integer;
+  lStringArray :TJSUint16Array;
+begin
+  if DoMainThreadBlockingCall(FNGetItem,[aKey])<>0 then
+    Exit('');
+  lResultLen:=Self.FResultData[CallResultLen];
+  if lResultLen<=0 then
+    Exit('');
+  lStringArray:=TJSUint16Array.new(_AtomicBuffer, CallResultData*4, lResultLen);
+  Result := String(TJSFunction(@TJSString.fromCharCode).apply(nil, TJSValueDynArray(lStringArray)));
+end;
+
+class procedure TWorkerStorageBridge.init;
+
+  function createStorageImpl(aBridge : TWorkerStorageBridge) : TJSObject;
+  begin
+    Result:=New([
+      'getItem',@aBridge.GetItem,
+      'setItem',@aBridge.SetItem,
+      'key',@aBridge.Key,
+      'removeItem',@aBridge.RemoveItem,
+      'clear',@aBridge.Clear]);
+    // length is a property, so we must use defineProperty, as count is a function
+    TJSObject.defineProperty(Result,'length',new(['get',@ABridge.count]));
+  end;
+
+begin
+  _AtomicBuffer:=TJSSharedArrayBuffer.New(LocalStorageBufferSize);
+  _LocalStorage:=TWorkerStorageBridge.Create(skLocal);
+  _SessionStorage:=TWorkerStorageBridge.Create(skSession);
+  // Create stubs
+  self_['localStorage']:=CreateStorageImpl(_LocalStorage);
+  self_['sessionStorage']:=CreateStorageImpl(_LocalStorage);
+  // Keep ZenFS happy.
+  self_['Storage']:=TJSObject;
+end;
+
+initialization
+  TWorkerStorageBridge.Init;
+end.
+

+ 19 - 4
packages/wasm-utils/src/wasm.pas2js.storage.pas

@@ -1,16 +1,30 @@
 unit wasm.pas2js.storage;
 
 {$mode ObjFPC}
+{$modeswitch externalclass}
 
 // Uncomment this if you want to remove all logging calls
 { $DEFINE NOLOGAPICALLS}
 interface
 
 uses
-  js, web, wasienv, wasm.storage.shared;
+  js, weborworker, wasienv, wasm.storage.shared;
 
 Type
-  
+  TJSStorage = class external name 'Storage' (TJSEventTarget)
+  private
+    FLength: NativeInt; external name 'length';
+  public
+    function key(aIndex : Integer) : String;
+    function getItem(aKeyName : string) : string;
+    procedure setItem(aKeyName : string; aValue : string);
+    procedure removeItem(aKeyName : string);
+    procedure clear;
+    property Keys[AIndex : Integer] : String read key;
+    property Items[aKeyName: String] : String read getItem write setItem; default;
+    property length : NativeInt Read FLength;
+  end;
+
   { TStorageAPI }
 
   TStorageAPI = Class(TImportExtension)
@@ -45,10 +59,11 @@ end;
 
 function TStorageAPI.GetStorage(aKind: Longint): TJSStorage;
 
+
 begin
   Case aKind of
-    STORAGE_LOCAL : Result:=window.localStorage;
-    STORAGE_SESSION : Result:=window.sessionStorage;
+    STORAGE_LOCAL : Result:=TJSStorage(Self_['localStorage']);
+    STORAGE_SESSION : Result:=TJSStorage(Self_['sessionStorage']);
   else
     Result:=Nil;
   end;

Some files were not shown because too many files changed in this diff