Recent

Author Topic: Minor Q: HowTo use 'DebugLn' in a DLL?  (Read 863 times)

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Minor Q: HowTo use 'DebugLn' in a DLL?
« on: January 20, 2025, 08:39:18 pm »
I'd oftenly used "OutputDebugString" for to trace some stuff, but sometimes there are situations where i couldn't use it (as it requires the "Windows" uses, and that's oftenly not appliable).
So i switched more and more to use "DebugLn", and for a stand-alone exe i do need to:
1. Main form: include "LazLogger" in the use clause
2. Project Options > Compiler Options > 'Config and Target':  'Win32 gui application (-WG)' => no
3. Run Parameters > Command line parameters :  --debug-log=<logfile_path_and_name>

The latter isn't possible when developing a plugin DLL. As - related to 3. - the "Host Application" exe of course will not understand this command line parameter resp. interpretes it in an unpredictable way.

Is there an equivalent for this Run parameter for a DLL?

PS: background is: in a plugin DLL, i had a problem with my docking form's toolbar that sometimes lost the trigger to be drawn and appeared empty. Occured when the host application opened a docked subdialog below my docking form, or, same, when i closed this docked subdialog. Toolbar buttons were not drawn anymore.
I found a workaround for this. But i'm still interested to see which layer maybe not passed a paint message to the toolbar (the OnPaintButtons callback won't be called in this case).
« Last Edit: January 20, 2025, 08:41:13 pm by d7_2_laz »
Lazarus 3.6  FPC 3.2.2 Win10 64bit

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 10681
  • Debugger - SynEdit - and more
    • wiki
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #1 on: January 20, 2025, 09:05:37 pm »
Well you need to use the unit LazLagger in the dll => that will initialize the logger instance.
Actually => assuming initialization sections are run. Not sure that happens in a dll.

So if initialization sections are not run, then you must at some point trigger the setup yourself
Code: Pascal  [Select][+][-]
  1.   uses LazLoger;
  2. function CreateDebugLogger: TRefCountedObject;
  3. begin
  4.   Result := TLazLoggerFile.Create;
  5. //  TLazLoggerFile(Result).Assign(GetExistingDebugLogger);  // optional / if you call only once there should be none to copy any setting from
  6. end;
  7.  
  8. begin
  9.   LazDebugLoggerCreator := @CreateDebugLogger;
  10.  

Then the first call to  Debugln will call you creator, and the logger will be setup.


I don't know if the logger will be able to see the argc/argv in the dll. And even if, it may not be possible to give the relevant switches.

In that case your creator method can setup hardcoded settings.

Code: Pascal  [Select][+][-]
  1. YourLazLoggerFile.LogName := 'log.txt';


Feel free to add it to the relevant wiki pages ;)

PascalDragon

  • Hero Member
  • *****
  • Posts: 5815
  • Compiler Developer
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #2 on: January 20, 2025, 10:06:58 pm »
I'd oftenly used "OutputDebugString" for to trace some stuff, but sometimes there are situations where i couldn't use it (as it requires the "Windows" uses, and that's oftenly not appliable).

You can always import that function yourself by copying the declaration. The kernel32.dll is always loaded anyway.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #3 on: January 20, 2025, 10:38:03 pm »
Hi Martin,
that sounds very good; i can try to elaborate tomorrow and come back with the result.

Some small remarks here:
- yes, the plugin's controlling unit has an initialization section as well as a finalization.
  Otherwise this shouldn't be a problem either by the method yoo describe. One could do it (LazDebugLoggerCreator := .. etc.) in FormCreate (or plugin's Create) too and that should be sufficient.
  As far as Lazarus internal units are concerned and it's about to trace back here in higher levels of the hierarchy why some callback might be _Not triggered_ (*).
- "if the logger will be able to see the argc/argv in the dll" -> in my case (plugin for NotePad++) the host application will eat up this argument (it tries to open it as file and claims, this cannot be opened).

(*) In this situation (Not triggered) the means of stacktrace, breakpoint & Co. are of limit help. Normally i don't have such situation and so i'm still a noob in the matters of DebugLn. That won't be sufficient for a wiki article.
But what I could contribute - if i should succeed - is a kind of summary 'from a noob perspective' as attached document, for any free usage.

Hi PascalDragon:,
"import that function": that could be, but i'm oftenly within diverse .inc files (and then their includers, and up) and feel, to manipulate here wouldn't be a preferred strategy and it would be better to use the native DebugLn.
(Import in the main unit / main form / plugin's pas file wouldn't be a problem. But here's the OutputDebugString already available anyway).
Lazarus 3.6  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #4 on: January 21, 2025, 03:09:04 pm »
Would the following procedure from a first test basically be correct?
'The first call to Debugln will call your creator' .. but it appears to me it's never entered.
I guess it still relies somehow on the command line param (resp. an env. variable).

The "LogName" property: might it contain a path particle?

Code: Pascal  [Select][+][-]
  1. interface
  2. uses ....,
  3.     LazLogger.
  4.     LazLoggerBase,   // needed for GetExistingDebugLogger
  5.     LazClasses;      // needed for TRefCountedObject
  6.  
  7. function CreateDebugLogger: TRefCountedObject;
  8. begin
  9.   Result := TLazLoggerFile.Create;
  10.   //TLazLoggerFile(Result).Assign(GetExistingDebugLogger);  // optional / if you call only once there should be none to copy any setting from
  11.   //TLazLoggerFile(Result).LogName := '___aLogfile.txt';
  12.   TLazLoggerFile(Result).LogName := 'D:\test\___aLogfile.txt';  // With path, possible? Otherwise, what might be the default path?
  13.   outputdebugstring(pchar(' --- check: am i called? '));   // No output in the event log
  14.   // A breakpoint on any line in this function won't be entered.
  15. end;
  16.  
  17. procedure TXYZDockingForm.FormCreate(Sender: TObject);
  18. var .....,  LazDebugLoggerCreator: TRefCountedObject;
  19. begin
  20.    LazDebugLoggerCreator := @CreateDebugLogger;
  21.    DebugLn(' ==== testTXYZDockingForm.FormCreate');  // ... the first call to Debugln will call your creator
  22.  
« Last Edit: January 21, 2025, 03:37:33 pm by d7_2_laz »
Lazarus 3.6  FPC 3.2.2 Win10 64bit

Thaddy

  • Hero Member
  • *****
  • Posts: 16403
  • Censorship about opinions does not belong here.
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #5 on: January 21, 2025, 03:39:43 pm »
My solution is this:
Code: Pascal  [Select][+][-]
  1. {$ifdef mswindows}uses windows;{$endif}
  2. procedure DebugStr(const s:string);
  3. begin
  4. {$ifdef mswindows}
  5.   OutputDebugString(s);
  6. {$else}
  7.   writeln(ErrOutPut, s);// is in system
  8. {$endif}
  9. end;
Crude and only windows and nixes.
But it works....
« Last Edit: January 21, 2025, 03:51:01 pm by Thaddy »
There is nothing wrong with being blunt. At a minimum it is also honest.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #6 on: January 21, 2025, 04:11:35 pm »
Thank you for replying Thaddy  :))
The essential part in question is: where to place this 'uses Windows'.
Imagine you are in a treeview.inc and don't want to change it's includer (here: comctrls.pas)  (in some cases such may end up desastrous if this unit appears in the wrong order).
So placing the little helper interimswise directly within the .inc file, this might result in: see image

Means, one would be redirected to change the uses of comctrls.pas, or did i miss something?

Edit: more generally spoken: we cannot place some other 'uses' within such .inc files, even not directly on the top. (At least i think so)
« Last Edit: January 21, 2025, 04:28:48 pm by d7_2_laz »
Lazarus 3.6  FPC 3.2.2 Win10 64bit

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 10681
  • Debugger - SynEdit - and more
    • wiki
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #7 on: January 21, 2025, 04:31:42 pm »
The "LogName" property: might it contain a path particle?

Code: Pascal  [Select][+][-]
  1.   outputdebugstring(pchar(' --- check: am i called? '));   // No output in the event log


Path in Logname: I think, yes that is ok. But I would have to test myself

outputdebugstring: Where is that coming from? That has nothing to do with debugln and LazLogger.
I know you mentioned it before, but I thought you said it was an alternative that you did not pursue (at least not as part of the LazLogger issue).

LazLogger use debugln, so to test that log, call
Code: Pascal  [Select][+][-]
  1. debugln('check, am I called');

outputdebugstring goes to Windows API, and afaik will only show up via a debugger, if one is attached.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #8 on: January 21, 2025, 05:00:57 pm »
Martin,  misunderstanding: this "OutputDebugString" at this place mentioned has nothing to do with the DebugLn - it was  simply meant as an additional verification only, that the create-procedure is not called (as a breakpoint herein won't be reached).
For me, the need for the DebugLn arises where a "uses Windows" is either not available or not applicable, i.e. iin .inc files. So i thought: stop using this instrument here and use the correct instrument.

As the create-procedure obviously won't be called by the code. as next step a direct approach:

Code: Pascal  [Select][+][-]
  1. procedure TXYZDockingForm.FormCreate(Sender: TObject);
  2. var .....,  LazDebugLoggerCreator: TRefCountedObject;  
  3. begin
  4.    //LazDebugLoggerCreator := @CreateDebugLogger; // Then the first call to  Debugln will call your creator
  5.    LazDebugLoggerCreator := TLazLoggerFile.Create;
  6.    TLazLoggerFile(LazDebugLoggerCreator).LogName := 'D:\test\___aLogfile.txt';
  7.    DebugLn(' ==== test TXYZDockingForm.FormCreate');

Still no log file could be seen
(and i'm not really surprised -> why the DebugLn should know about this - here still purely local - logger instance. But it won't make a difference if i define this as a Form's instance variable instead). There's still some connector missing.

Lazarus 3.6  FPC 3.2.2 Win10 64bit

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 10681
  • Debugger - SynEdit - and more
    • wiki
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #9 on: January 21, 2025, 05:34:52 pm »
Quote
Code: Pascal  [Select][+][-]
  1.   //LazDebugLoggerCreator := @CreateDebugLogger; // Then the first call to  Debugln will call your creator
  2.    LazDebugLoggerCreator := TLazLoggerFile.Create;

Putting an object reference into the function pointer => that should crash.
So if it doesn't then for some reason your "debugln" does not create a logger => maybe some Logger already exists...

Check what "DebugLogger" contains / if it is not nil, then set the LogName to that.
Well, if you use LazLoggerDummy then that wont work, because it is a dummy logger, but otherwise it should be the correct class (if it is non nil).


If that doesn't help then make sure you use either
  LazLogger.debugln
  LazLoggerBase.debugln
There is a version in LCL, it should forward, but not sure. Or of course again, if LazLoggerDummy is in the way (but really it shouldn't / you would know if you put it there.... )

Otherwise, I am not sure / Sorry can't do a deep dive into that now...

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #10 on: January 21, 2025, 10:25:21 pm »
- Which units used: as mentioned earlier today:  LazLogger, LazLoggerBase (for: GetExistingDebugLogger), LazClasses  (for: TRefCountedObject).
  (i see, it would be better to keep the pieces together at one place, so below).
- "Putting an object reference into the function pointer => that should crash"
  But isn't it the same if one does :   "Result := TLazLoggerFile.Create"  and Result is type TRefCountedObject
  or one does :  var xyz: TRefCountedObject;  and then   xyz := TLazLoggerFile.Create ?
  The question is only: what can we do with such instance of a reference counter, can we operate on it (for me it sounds confusing, and i can only guess).
  At least: it doesn't crash (and the object hadn't been nil).
- I had also tried with "DebugLogger", but had not success.

But now: i tried to show all steps and interims results more detailed. And, doing so, i got a new situation.

Code: Pascal  [Select][+][-]
  1. interface
  2. uses ....,
  3.     LazLogger.
  4.     LazLoggerBase,   // needed for GetExistingDebugLogger
  5.     LazClasses;      // needed for TRefCountedObject
  6.  
  7. procedure TXYZDockingForm.FormCreate(Sender: TObject);
  8. var .....,  LazDebugLoggerCreator: TRefCountedObject;
  9. begin
  10. // -- Test 1
  11.    if DebugLogger = nil then
  12.          OutputDebugString(pchar('DebugLogger is nil'))
  13.       // Note: OutputDebugString here only as temporary display instrument:
  14.    else
  15.    begin
  16.          OutputDebugString(pchar('DebugLogger LogName = ' + TLazLoggerFile(DebugLogger).LogName )); // "DebugLogger.LogName" would be not compileable
  17.          TLazLoggerFile(DebugLogger).LogName := 'D:\__Temp\yy\___aLogfile.txt';
  18.          OutputDebugString(pchar('DebugLogger new assigned LogName = ' + TLazLoggerFile(DebugLogger).LogName )); // "DebugLogger.LogName" would be not compileable
  19.    end;
  20.    DebugLn(' -----  Welcome logfile !');
  21.  
  22. // -- Or Test 2
  23.    LazDebugLoggerCreator := TLazLoggerFile.Create;
  24.    if LazDebugLoggerCreator = nil then
  25.       OutputDebugString(pchar('LazDebugLoggerCreator is nil'))
  26.    else
  27.    if TLazLoggerFile(LazDebugLoggerCreator) = nil then
  28.       OutputDebugString(pchar('TLazLoggerFile(LazDebugLoggerCreator) is nil'))
  29.    else
  30.    begin
  31.          // This was only temporarely an additional test:
  32.           TLazLoggerFile(LazDebugLoggerCreator).Assign(GetExistingDebugLogger);  // optional / if you call only once there should be none to copy any setting from
  33.           OutputDebugString(pchar(' Assigned GetExistingDebugLogger;   log name =  ' + TLazLoggerFile(LazDebugLoggerCreator).LogName));
  34.  
  35.       TLazLoggerFile(LazDebugLoggerCreator).LogName := 'D:\__Temp\yy\___aLogfile.txt';
  36.       OutputDebugString(pchar(' Assigned new log name:  ' + TLazLoggerFile(LazDebugLoggerCreator).LogName));
  37.    end;
  38.    DebugLn(' -----  Welcome logfile !');

Attached as images the individual output results  (i did either Test1 or Test2, not both together).
One can see: both appear to behave very unsuspicious (no crash, no nil; assignment appeared to be good).

Test2 (that was the previous approach) did Not result in a logfile.
Test1 - using the existing "DebugLogger" - finally generated the desired log file, and it said:  -----  Welcome logfile !

That means, success so far: useable in the DLL's main unit / form at least seems to be:
Code: Pascal  [Select][+][-]
  1.    if DebugLogger <> nil then
  2.       TLazLoggerFile(DebugLogger).LogName := <desired_path_and_name>;

This can be applied to other forms within the dll app too (uses LauLogger here and doing the same assignment as above. DebugLn then will append to the same log file).

So far, so very good! We can defiine the logfile name (in a DLL ->) there, were a command line parameter is not applicable.

The second and last chaptor will be: how to make this operational for not self-written units, eg. toolbutton.inc, toolbar.inc etc.
(First i'll try to transfer it to the core pas file of the plugin / initialization section, and then see, how far this matters.
(Actually a DebugLn in eg. treeview.inc is compilable, but results in nothing. Probably LazLoggerDummy is at work. Tried to compile with custom option -dDEBUG, but that, actually, caused other issues; couldn't compile; range check errors. Need to investigate.)

Lazarus 3.6  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #11 on: January 21, 2025, 11:16:24 pm »
Oh wow, it works°
Quote
--- Hello logfile for FormCreate
 ------DebugLn in treeview.inc.  TCustomTreeView.UpdateHotTrack
 ------DebugLn in treeview.inc.  TCustomTreeView.UpdateHotTrack

Need to see why it works in the small demo and not in the original app, but those are details (i hope).
Basically it's operational as desired, perfect!

What did i need to to other than to assign the log file path/name for the DebugLogger as described (in FormCreate of the main form)? Nothing!
Thank you Martin for to have pointed to that!

If i succeed to transfer it to the original more complex app, i'll come back here with a small 'noob howto summary').
Lazarus 3.6  FPC 3.2.2 Win10 64bit

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 10681
  • Debugger - SynEdit - and more
    • wiki
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #12 on: January 21, 2025, 11:41:36 pm »
Quote
"Putting an object reference into the function pointer => that should crash"
  But isn't it the same if one does :   "Result := TLazLoggerFile.Create"  and Result is type TRefCountedObject
  or one does :  var xyz: TRefCountedObject;  and then   xyz := TLazLoggerFile.Create ?
  The question is only: what can we do with such instance of a reference counter, can we operate on it (for me it sounds confusing, and i can only guess).

Code: Pascal  [Select][+][-]
  1. type
  2.   TLazDebugLoggerCreator = function: TRefCountedObject;
  3. var
  4.   LazDebugLoggerCreator: TLazDebugLoggerCreator = nil;
  5.  

LazDebugLoggerCreator is a function reference. I don't even understand how you get the assignment to compile.
I get:
Code: Text  [Select][+][-]
  1. project1.lpr(8,43) Error: Incompatible types: got "TLazLoggerFile" expected "<procedure variable type of function:TRefCountedObject;Register>"

You can't even do
Code: Pascal  [Select][+][-]
  1. LazDebugLoggerCreator := @TLazLoggerFile.Create;
because the constructor has a different calling convention. (Well it compiles in mode delphi / but no idea / not tested)


With LazLogger there are very few cases where you may want to refcount it yourself (and its not refcounted automatically, this needs to be called by code explicitly).
If you do stuff that replaces the logger, then you could get a copy before, and could protect it from being destroyed.

I have some code that uses multiply loggers => basically a testcase, where the tested code use LazLogger, and the test does capture all output. But then the test has a 2nd logger for its own logging.




Quote
using the existing "DebugLogger" - finally generated the desired log file, and it said:
If you have an existing logger (that is, apparently initialization was called? Or don't know...) then the LazDebugLoggerCreator is not invoked => its only done once, when there is no logger yet.

Quote
This can be applied to other forms within the dll app too (uses LauLogger here and doing the same assignment as above. DebugLn then will append to the same log file).

They all share one instance of the logger. And if you set the same logfile everywhere then once it is set, the other assignments don't change anything.

LCL units use LazLogger too => so they use the same instance. Once any of your code has set the logfile name, then they should start logging.

You just need to see what the earliest hook is where you can do that.
In normal apps that is initialization sections.
In dll, I don't know. I didn't expect them to be run. But if the logger has already an instance then maybe the do?

d7_2_laz

  • Hero Member
  • *****
  • Posts: 601
Re: Minor Q: HowTo use 'DebugLn' in a DLL?
« Reply #13 on: Today at 02:18:00 pm »
Martin, yes, by your quote of the type definition for TLazDebugLoggerCreator i understand better. what you mean.
(I was wondering from the beginning anyhow how should have worked, to gain properties for a file object from a ref counter. even when doing a cast to TLazLoggerFile).
Why such even compiles? I'd guess too it's by mode delphi (yes. this had been set via compiler options > parsing, from an earlier delphi conversion).
I don't plan to do anything else with the ref counter. Nor to have multiple logs.

Quote
If you have an existing logger (that is, apparently initialization was called? ...)

I had not needed yet to step further to the initialization (nor in the main form, neither in the plugin's core code) what might be somehow related to the DebugLogger object. It was simply there and acessible, whoever it had created. I just checked: it's even available in a release build mode).

Quote
And if you set the same logfile everywhere then once it is set, the other assignments don't change anything.
Yes, that was the plan .. eg. main form and another form do set LogName to the same location and each's output will be appended there.
 
Quote
LCL units use LazLogger too => so they use the same instance.
Fine for me, and btw equals the behaviour of OutputDebugString / event log, where output from diverse sources might appear.
Btw, in my test log file i had also entries from other sources. Eg. from a  debugln('HandleListViewOwnerDataHint') in win32wscustomlistview.inc, being set in the LCL code.

Quote
You just need to see what the earliest hook is where you can do that.
In normal apps that is initialization sections.
In dll, I don't know. I didn't expect them to be run.

In the plugin DLL those initialization sections are there and used as well (for other purposes, not Debug yet).
My first thought earlier had been i'd need to hook it into initialization, but, until there might be further needs, doing it in FormCreate is sufficient.
Lazarus 3.6  FPC 3.2.2 Win10 64bit

 

TinyPortal © 2005-2018