Navid Madani 1 Posted December 3, 2023 (edited) unit uMacOSSpawn; interface uses System.SysUtils , System.Classes , Macapi.Foundation , Macapi.ObjectiveC , Macapi.ObjCRuntime , Macapi.Helpers ; type TProcessSpawn = class(TOCLocal) private fTask: NSTask; fInputPipe: NSPipe; fOutputPipe: NSPipe; fErrorPipe: NSPipe; fNotify: TNotifyEvent; fOutput: string; function GetNotify: TNotifyEvent; procedure SetNotify(const Value: TNotifyEvent); function GetOutput: string; public constructor Create(FileName: PWideChar); destructor Destroy; override; procedure NotificationReceived(notification: NSNotification); cdecl; procedure Input(const aText: string); property Output: string read GetOutput; property Notify: TNotifyEvent read GetNotify write SetNotify; end; implementation { TProcessSpawn } constructor TProcessSpawn.Create(FileName: PWideChar); var FileHandleLocObj: ILocalObject; InputLocObj: ILocalObject; OutputLocObj: ILocalObject; ErrorLocObj: ILocalObject; begin inherited Create; fInputPipe := TNSPipe.Create; fOutputPipe := TNSPipe.Create; fErrorPipe := TNSPipe.Create; fTask := TNSTask.Wrap(TNSTask.Wrap(TNSTask.OCClass.alloc).init); fTask.setLaunchPath(StrToNSStr(FileName)); if Supports(fInputPipe, ILocalObject, InputLocObj) and Supports(fOutputPipe, ILocalObject, OutputLocObj) and Supports(fErrorPipe, ILocalObject, ErrorLocObj) and Supports(fOutputPipe.fileHandleForReading, ILocalObject, FileHandleLocObj) then begin fTask.setStandardInput(InputLocObj.GetObjectID); fTask.setStandardOutput(OutputLocObj.GetObjectID); fTask.setStandardError(ErrorLocObj.GetObjectID); fOutputPipe.fileHandleForReading.acceptConnectionInBackgroundAndNotify; fOutputPipe.fileHandleForReading.readInBackgroundAndNotify; TNSNotificationCenter.Wrap(TNSNotificationCenter.OCClass.defaultCenter).addObserver( Self.GetObjectID, sel_getUid('NotificationReceived:'), NSFileHandleReadCompletionNotification, FileHandleLocObj.GetObjectID ); fTask.launch; // fNSTask.waitUntilExit; end else raise Exception.Create('ILocalObject interface not supported.'); end; destructor TProcessSpawn.Destroy; begin fTask.release; fInputPipe.release; fOutputPipe.release; fErrorPipe.release; inherited; end; function TProcessSpawn.GetNotify: TNotifyEvent; begin Result := fNotify; end; function TProcessSpawn.GetOutput: string; begin Result := fOutput; end; procedure TProcessSpawn.NotificationReceived(notification: NSNotification); var data: NSData; str: NSString; notifHandle: NSFileHandle; begin notifHandle := TNSFileHandle.Wrap(notification.&object); if notifHandle = fOutputPipe.fileHandleForReading then begin data := fOutputPipe.fileHandleForReading.availableData; str := TNSString.Wrap(TNSString.Alloc.initWithData(data, NSUTF8StringEncoding)); try fOutput := NSStrToStr(str); if Assigned(fNotify) then fNotify(Self); finally str.release; end; end; end; procedure TProcessSpawn.Input(const aText: string); var data: NSData; str: NSString; begin str := StrToNSStr(aText + #10); data := str.dataUsingEncoding(NSUTF8StringEncoding); fInputPipe.fileHandleForWriting.writeData(data); end; procedure TProcessSpawn.SetNotify(const Value: TNotifyEvent); begin fNotify := Value; end; end. I am trying to create a unit that spawns a console process on macOS that receives text input through stdin, and returns the result through stdout. I am not an iOS programmer, but I think I managed to get things done correctly based on documentation I consulted, and the unit below compiles without errors or warnings. However, the .addObserver call in the constructor (line 67) causes a runtime exception: First chance exception at $00000001823C976C. Exception class 6. Process Project1 (2540). Is this my fault, or could this be a bug in Delphi's Objective-C bridge? Thanks to everyone in advance. P.S.: I should also mention that debugging the RTL using Delphi 12 Athens on my Apple Silicon device running macOS Sonoma and Windows 11 Pro for ARM virtualized with Parallels, hangs the IDE 😞 Edited December 3, 2023 by Navid Madani Share this post Link to post
Dave Nottage 538 Posted December 3, 2023 2 hours ago, Navid Madani said: However, the .addObserver call in the constructor (line 67) causes a runtime exception: Assuming NSFileHandleReadCompletionNotification is an NSString, it should be passed like this: NSObjectToID(NSFileHandleReadCompletionNotification) Having said that, I haven't come across the need to use such an observer. You might like to take a look at the code in Kastri that creates a process on macOS and reads from the output. 1 Share this post Link to post
Navid Madani 1 Posted December 3, 2023 Thanks, David. That was indeed what caused the exception. I noticed that pattern at least once in the docs I read and forgot. The pattern above was based on Objective-C code samples I studied. However, mine does not work as intended because the notification handler is not called back. I did not know about Kastri until now. I'll pursue the Kastri solution if tinkering with my own implementation remains fruitless. In any case, I just became a Kastri sponsor on GitHub. Thanks again. 1 Share this post Link to post
Navid Madani 1 Posted December 4, 2023 (edited) unit uMacOSSpawn; interface uses System.SysUtils , System.Classes , System.Threading , Macapi.Foundation , Macapi.ObjectiveC , Macapi.ObjCRuntime , Macapi.Helpers ; type TStringCallback = procedure(const aText: string) of object; IProcessSpawn = interface ['{606A5174-5921-431C-AD57-DD037472CEF5}'] function GetNotify: TStringCallback; procedure SetNotify(const Value: TStringCallback); function GetOutput: string; procedure Input(const aText: string); property Output: string read GetOutput; property OutputNotify: TStringCallback read GetNotify write SetNotify; end; TProcessSpawn = class(TInterfacedObject, IProcessSpawn) strict private fNSTask: NSTask; fInputPipe: NSPipe; fOutputPipe: NSPipe; fOutputNotify: TStringCallback; fOutput: string; fOutputTask: ITask; function GetNotify: TStringCallback; procedure SetNotify(const Value: TStringCallback); function GetOutput: string; procedure SendNotification(const aToNotify: TStringCallback; const aOutput: string); function WaitForOutput: ITask; procedure Input(const aText: string); public constructor Create(const FileName: string; const aCallback: TStringCallback); destructor Destroy; override; end; implementation { TProcessSpawn } constructor TProcessSpawn.Create(const FileName: string; const aCallback: TStringCallback); var InputLocObj: ILocalObject; OutputLocObj: ILocalObject; begin inherited Create; fOutputNotify := aCallback; fInputPipe := TNSPipe.Create; fOutputPipe := TNSPipe.Create; fNSTask := TNSTask.Wrap(TNSTask.Wrap(TNSTask.OCClass.alloc).init); fNSTask.setLaunchPath(StrToNSStr(FileName)); if Supports(fInputPipe, ILocalObject, InputLocObj) and Supports(fOutputPipe, ILocalObject, OutputLocObj) then begin fNSTask.setStandardInput(InputLocObj.GetObjectID); fNSTask.setStandardOutput(OutputLocObj.GetObjectID); fNSTask.setStandardError(OutputLocObj.GetObjectID); fNSTask.launch; end else raise Exception.Create('ILocalObject interface not supported.'); end; destructor TProcessSpawn.Destroy; begin if fNSTask.isRunning then fNSTask.terminate; fNSTask.release; fInputPipe.release; fOutputPipe.release; fOutputNotify := nil; fOutputTask.Cancel; fOutputTask := nil; inherited; end; function TProcessSpawn.GetNotify: TStringCallback; begin Result := fOutputNotify; end; function TProcessSpawn.GetOutput: string; begin Result := fOutput; end; procedure TProcessSpawn.Input(const aText: string); var data: NSData; str: NSString; begin if not fNSTask.isRunning then raise Exception.Create('No running process!'); str := StrToNSStr(aText + #10); data := str.dataUsingEncoding(NSUTF8StringEncoding); fInputPipe.fileHandleForWriting.writeData(data); if fOutputTask = nil then fOutputTask := WaitForOutput; end; procedure TProcessSpawn.SendNotification(const aToNotify: TStringCallback; const aOutput: string); begin fOutputTask := nil; if Assigned(aToNotify) then begin TThread.Synchronize(nil, procedure begin aToNotify(aOutput); end); end; fOutputTask := WaitForOutput; end; procedure TProcessSpawn.SetNotify(const Value: TStringCallback); begin fOutputNotify := Value; end; function TProcessSpawn.WaitForOutput: ITask; begin Result := TTask.Create( procedure var data: NSData; str: NSString; begin data := fOutputPipe.fileHandleForReading.availableData; str := TNSString.Wrap(TNSString.Alloc.initWithData(data, NSUTF8StringEncoding)); try fOutput := NSStrToStr(str); SendNotification(fOutputNotify, fOutput); finally str.release; end; end); Result.Start; end; end. In case anyone would run into this issue, I simplified things by calling .dataAvailable which is a blocking call in a separate thread. The unit below (at least for now) is working for my purposes. Edited December 4, 2023 by Navid Madani Added TStringCallback definition. Share this post Link to post