vcmkrtchyan
vcmkrtchyan

Reputation: 2626

How does passing field as argument differ from passing method call as argument?

I am writing a .Net CE application on a Smart Device that has a printer on it. I collect my data in a StringBuilder object, and then try to print it. Here's how I print it

var receipt = new StringBuilder();

// ...

Printer.getInstance().print(receipt.ToString(), (int) Printer.TextAlign.Left, 0, 24, false);

Printer class is imported from a DLL. The application throws an unmanaged exception on the print line and crashes. But when I change my code to this

var receipt = new StringBuilder();

// ...

var str = receipt.ToString();
Printer.getInstance().print(str, (int) Printer.TextAlign.Left, 0, 24, false);

everything works fine. How is it even possible for the evalution of the StringBuilder affect the flow?

Here's a the methods from my Printer.dll (decompiled)

public int print(string text, int textAlign, int fontWeight, int fontSize, bool endLineFeed)
{
    open();

    Int32 prnReturn;
    Printer.Prn_SetLang(1);
    Printer.PRN_SetFont((byte) fontSize, (byte) fontSize, 0);
    Printer.PRN_SetAlign(textAlign);
    Printer.PRN_SetBold(fontWeight);
    Prn_String(text.TrimEnd());
    prnReturn = PRN_PrintAndWaitComplete();

    if(endLineFeed)
        printEndingLineFeed();

    close();

    return prnReturn;
}

public void open()
{
    PRN_Open();
}

public void close()
{
    PRN_Close();
}

private void printEndingLineFeed()
{
    open();
    //lineFeed(ENDING_LINE_FEED);
    PRN_FeedLine(ENDING_LINE_FEED);
    close();
}

and here are the methods that it calls from another DLL. Unfortunately, DotPeek doesn't decompile this.

[DllImport(PrinterDllName, SetLastError = true, EntryPoint = "PRN_FeedLine")]
public static extern Int32 PRN_FeedLine(Int32 pszData);

[DllImport(PrinterDllName, SetLastError = true, EntryPoint = "PRN_PrintAndWaitComplete")]
public static extern Int32 PRN_PrintAndWaitComplete();

Edit: Thanks to Kevin Gosse, I found out that the problem is only there in Debug mode. So my question now is, how does the debug mode evaluation differ from normal execution. Although I do understand that this might be off-topic, I'd be glad if someone could share a relevant piece of documentation.

Upvotes: 2

Views: 105

Answers (1)

Kevin Gosse
Kevin Gosse

Reputation: 39047

In release mode, both versions of your code are identical.

In debug mode, there is a subtle difference as the lifetime of str will be extended until the end of the current method (for debugging purpose).

So this code in debug mode:

var receipt = new StringBuilder();

// ...

var str = receipt.ToString();
Printer.getInstance().print(str, (int) Printer.TextAlign.Left, 0, 24, false);

Is equivalent to this in release mode:

var receipt = new StringBuilder();

// ...

var str = receipt.ToString();
Printer.getInstance().print(str, (int) Printer.TextAlign.Left, 0, 24, false);

GC.KeepAlive(str);

When your string is given to the native code, it's not tracked by the GC anymore. Therefore, it could be collected which will cause errors in the native part.

In theory, the marshaller automatically protects you from such situations when using common types (such as string), as described here: https://learn.microsoft.com/en-us/dotnet/framework/interop/copying-and-pinning?redirectedfrom=MSDN. That's another puzzle, but it's possible that .net compact framework has a different marshaller and doesn't protect you automatically. Unfortunately it's hard to find specific documentation on the subject.

What surprised me when you decompiled the method is how the native call doesn't actually receive the original string but text.TrimEnd(). It means that the lifetime of the original value shouldn't have any impact (since the native code receives a different string). However, it turns out that .TrimEnd returns the original string when there's nothing to trim. When you added a space, it started crashing even with the version of the code that extends the lifetime of the string. That's because now you're extending the lifetime of the wrong string (since TrimEnd() will return a different instance, and that's the one that will be used by the native code).

I believe that the code working in Release is pure luck. Maybe it just changes the timing of the garbage collector and you don't run into that specific issue, but it could cause problems in the future. Out of caution, I would suggest you to:

  • Trim the string before calling Printer.getInstance().print
  • Call GC.KeepAlive on the trimmed string after the call to print
  • If you want to be extra safe, you can pin the string instead of calling GC.KeepAlive

I wish I could give more than theories, but I believe you're running into specificities of .net compact framework. If somebody with more experience on the subject reads this and could give more information, that would be much appreciated.

Upvotes: 3

Related Questions