-
Notifications
You must be signed in to change notification settings - Fork 585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Board: Improve I²C device management to avoid null reference exceptions #2032
Conversation
Remove devices after creating them while scanning the I2C bus. Resolves dotnet#2031
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. thanks. I let @pgrawehr to have a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see @pgrawehr's comment
Thanks for your input everyone! I agree that the destructor should be cleaning things up, and it remains unclear to me why it is not. For reference here is the test code I am using to evaluate this issue. The code change in this PR causes this failing test to pass. I'll take a closer look to figure out exactly what is going wrong, and whether a more targeted code change could similarly resolve this issue. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Device.I2c;
using System.Linq;
using Iot.Device.Board;
using Iot.Device.Ft232H;
using Iot.Device.FtCommon;
using Xunit;
namespace System.Device.Gpio.Tests;
[Trait("feature", "ft232h")]
public class Ft232hTests
{
[Fact]
public void DevicesReleaseAfterScanning()
{
// I have one device connected (a FT232H)
using Ft232HDevice f232h = new(FtCommon.GetDevices().First());
I2cBus bus = f232h.CreateOrGetI2cBus(0);
// First scan succeeds
foreach (int address in bus.PerformBusScan())
{
Console.WriteLine($"Found address {address:X2} ({address})");
}
// Second scan throws a null reference exception
foreach (int address in bus.PerformBusScan())
{
Console.WriteLine($"Found address {address:X2} ({address})");
}
}
} |
I haven't figured everything out yet (it's taking me a long time to debug this) but I have a better understanding of the core issue now. Here's a failing test: [Fact]
public void PerformBusScan2()
{
using Ft232HDevice f232h = new(FtCommon.FtCommon.GetDevices()[0]);
I2cBus bus = f232h.CreateOrGetI2cBus(0);
// open a device then dispose of it
I2cDevice device1 = bus.CreateDevice(0x48);
device1.ReadByte();
device1.Dispose(); // this sets _i2cBus to null ⚠️
// open the same device a second time
I2cDevice device2 = bus.CreateDevice(0x48); // this succeeds
device2.ReadByte(); // this throws a null reference exception ☠️
} Disposing the I2C device sets the bus to |
@krwq, thanks for your useful comments above. @Ellerbach I'm aware you're familiar with FT232H (#2019) so I'll pose this to both of you. I think I arrived at a fix, but this has potential for unexpected behavior so I really value your input. Currently the FT232H's I2C device destructor sets the bus to iot/src/devices/Ft232H/Ft232HI2c.cs Lines 48 to 54 in 301f795
This code fixes the issue, but it's not clear to me what the intent was of setting those variables to protected override void Dispose(bool disposing)
{
try
{
_i2cBus.RemoveDevice(_deviceAddress);
}
catch (ArgumentException)
{
// dont worry if the device has already been removed
}
base.Dispose(disposing);
} |
This reverts commit 11b5d35.
see discussion in dotnet#2032 resolves dotnet#2031
This commit is also an opportunity to re-run tests in the CI pipeline
Btw might be worth sharing some code with https://github.com/dotnet/iot/blob/main/src/devices/Ft4222/Ft4222I2cBus.cs so that we don't have same mistakes in multiple places. Possibly some common base class or something |
I like this idea, but it seems out of scope for this PR and may cause merge issues with #2019 which appears to be actively worked on. Do you recommend I attempt these changes in this PR, or would it be a better idea to open an issue for it and work on it in a new PR after #2032 and #2019 are successfully merged?
I see the CI release build has failed multiple times with errors that appear unrelated to the content of this PR. Can someone clarify whether this indicates there's a problem with my code modification, or if it's just an issue with the CI system? Thanks! |
This commit demonstrates how to scan the I2C bus for addresses devices acknowledge. Before the fix discussed in dotnet#2032, this scan would leave the i2c bus in a bad state such that reading/writing to devices after the scan would throw null reference exceptions.
This reduces load on the garbage collector. Note that `_i2cBus` should not be set to null at this time, as per discussion in dotnet#2032
This reverts commit 7b4f08e.
adds CreateDeviceNoCheck() and RemoveDeviceNoCheck() to mimic behaviors found in UnixI2cBus.cs Also creates a new used address hash set when CreateDevice() is called (not when the bus is instantiated), consistent with how the Unix I2C bus does it https://github.com/dotnet/iot/blob/main/src/System.Device.Gpio/System/Device/I2c/UnixI2cBus.cs
consistent with how the Unix I2C device class does it https://github.com/dotnet/iot/blob/main/src/System.Device.Gpio/System/Device/I2c/Devices/UnixI2cDevice.cs
expected to fail with a null reference exception due to a bug described by dotnet#2031 and dotnet#2032
Testing on actual hardwareTesting on actual hardware
Wow, that's fantastic! Thanks for pointing me in this direction. In addition to just being a RasPI, it looks like there is expected to be a BME280 I2C pressure sensor at address iot/src/System.Device.Gpio.Tests/ProtocolTests.cs Lines 40 to 48 in 58c8b14
Note to self: the raspberry pi tests say that they should pass without requiring raspberry pi hardware, so don't put i2c tests in there Testing on mock hardwareI added this failing test to demonstrate the issue without requiring special gear 6cef0f2 iot/src/devices/Board/tests/BoardTests.cs Lines 202 to 214 in 6cef0f2
Solution: don't store devices in the bus manager
I'll fix this issue by EDIT: I feel like the dictionary should stay because without it Checking for null
This is the line that throws the null reference exception, but in theory every function in this class that interacts with the bus could throw for the same reason. iot/src/devices/Ft232H/Ft232HI2c.cs Lines 29 to 32 in 58c8b14
Performing a null check on every read or write seems a bit costly. I agree If you disagree let me know and I'll make it nullable and add null checks in all the functions of this class that reference the bus. I guess I2C is a pretty slow protocol anyway, so in context adding a bunch of null checks is probably a negligible hit to overall performance. Renaming
Sounds good! Will do. |
fails, demonstrating issue discussed in dotnet#2031 and dotnet#2032
This reverts commit 72e2ea0.
fixes issues described in dotnet#2031 and dotnet#2032
suggested by pgrawehr in dotnet#2032
so Microsoft.NET.ApiCompat.ValidatePackage.targets will pass after 3dfa427
@pgrawehr I believe I made all the recommended changes and this is ready to merge now. Let me know what you think! TLDR:
EDIT: The I2C bus manager's |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok to rename the FT232HI2c yo FT232HI2cDevice. So don't keep the old one, I think it's confusing. And then please also adjust the sample and the read me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks almost good, just a minor finding.
it has been replaced by Ft232HI2cDevice
Done!
Supporting the previous point, I think both are okay as they are because they use iot/src/devices/Ft232H/samples/Program.cs Line 121 in 58c8b14
iot/src/devices/Ft232H/README.md Line 76 in 58c8b14
Done! I think this PR is ready to merge now, but let me know if you have any additional suggestions. Thanks for all your input along the way! 🚀 |
We need to update the compatibilitysuppressions still. |
I2cDevice newDevice = _busInstance.CreateDevice(deviceAddress); | ||
_devices.Add(deviceAddress, newDevice); | ||
_devices[deviceAddress] = newDevice; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at this point we should consider making _devices
either HashSet<I2cDevice>
or Dictionary<int, HashSet<I2cDevice>>
- perhaps even change design so that RemoveDevice(int)
is replaced with RemoveDevice(I2cDevice)
. Alternatively we should throw on CreateDevice if it was created again (we should also check if it was already disposed though). My personal favor is we should change the design.
@pgrawehr thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would probably be better, but I'm not sure it's worth the effort. Would need to make the changes and then check whether they show any benefit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's do it separately if needed in that case - current state is still better than it was - that's why I approved in the first place
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make iterative changes from now on. My comment is still valid though
@swharden Thanks again for figuring this out. |
Remove devices after creating them while scanning the I2C bus.
Thanks to @Ellerbach for providing feedback in the Discord 🚀
Resolves #2031
Microsoft Reviewers: Open in CodeFlow