Skip to content
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

Multiple problems when using with Unity/MacOS #8

Closed
staszain opened this issue Apr 9, 2020 · 44 comments · Fixed by ValveSoftware/GameNetworkingSockets#109
Closed
Labels
bug Something isn't working native unity

Comments

@staszain
Copy link

staszain commented Apr 9, 2020

Hi, I'm evaluating this library for some time now in Unity. I managed to import it into the project with some issues (for some reason it doesn't load the first time you run the game), however it doesn't work out of the box for me. I've compiled the library from 1.1.0 and took the same version of the bindings. Here are the problems I've encountered so far:

  1. client.DispatchCallback(status) is getting called with info structure containing garbage. It looks like there is a problem with marshalling. I was able to fix this with adding "ref" to the StatusCallback declaration:
    public delegate void StatusCallback(ref StatusInfo info, IntPtr context);
    I'm not an expert in c# marshalling and not sure this is a right fix and the system is now working as it supposed to, or is it going to break other platforms.

  2. Once StatusCallback is changed, the structure still seems to be off. It appears that some of the field values are shifted to neighbour fields. I was able to fix this by adding StructLayoutAttribute.Pack = 4 to StatusInfo structure declaration:
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct StatusInfo {
    ...
    as it looks like "connection" variable takes whole 8 bytes instead of 4. Not sure why Unity/Mono has a different default Pack setting, but I think this should fix the problem for Mono and not break other platforms.

  3. Just a suggestion to mention deinitialization procedure, especially if you set a debug callback like described in the "Usage":

Library.Initialize();
NetworkingUtils utils = new NetworkingUtils();
utils.SetDebugCallback(DebugType.Everything, debug);

you absolutely MUST set it to None when you're done:

utils.SetDebugCallback(DebugType.None, null);
Library.Deinitilize();

otherwise Unity will freeze on the next launch (probably because debug callback is now invalid and crashes silently somewhere inside).

@nxrighthere
Copy link
Owner

client.DispatchCallback(status) is getting called with info structure containing garbage. It looks like there is a problem with marshalling. I was able to fix this with adding "ref" to the StatusCallback declaration:

You are right, the signature was incorrect, it should be passed by reference.

Going to investigate other problems too.

@nxrighthere nxrighthere added bug Something isn't working unity labels Apr 10, 2020
@nxrighthere
Copy link
Owner

nxrighthere commented Apr 10, 2020

Just a suggestion to mention deinitialization procedure, especially if you set a debug callback like described in the "Usage":

Library.Initialize();
NetworkingUtils utils = new NetworkingUtils();
utils.SetDebugCallback(DebugType.Everything, debug);

you absolutely MUST set it to None when you're done:

utils.SetDebugCallback(DebugType.None, null);
Library.Deinitilize();

otherwise Unity will freeze on the next launch (probably because debug callback is now invalid and crashes silently somewhere inside).

I think this should be resolved in the native library, we can simply zero out/reset all global data at deinitialization with GameNetworkingSockets_Kill() since we done with it, like: SteamNetworkingSocketsLib::s_pfnDebugOutput, SteamNetworkingSocketsLib::g_eSteamDatagramDebugOutputDetailLevel.

Other game engines might be affected by this as well, since usually libraries there are loaded once, and global data/variables will persist in memory while the application remains alive.

@fletcherdvalve What do you think about this?

@nxrighthere
Copy link
Owner

Once StatusCallback is changed, the structure still seems to be off. It appears that some of the field values are shifted to neighbour fields. I was able to fix this by adding StructLayoutAttribute.Pack = 4 to StatusInfo structure declaration:
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct StatusInfo {
...
as it looks like "connection" variable takes whole 8 bytes instead of 4. Not sure why Unity/Mono has a different default Pack setting, but I think this should fix the problem for Mono and not break other platforms.

That one is strange. I'm unable to reproduce this on my end.

Native size of structures on Ubuntu:
Native

Managed size of structures on macOS Catalina 10.15.4 (Unity 2020.1.0b2):
macOS

Managed size of structures on Windows 10 (Unity 2019.3.2f1):
Unity

Managed size of structures on Windows 10 (.NET Core 3.1.200):
netcore

Can you please run the following script and confirm that on your end?

using System;
using System.Runtime.InteropServices;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;

namespace Valve.Sockets {
	public class Test : JobComponentSystem {
		protected override void OnCreate() {
			Debug.Log("Address size: " + Marshal.SizeOf(typeof(Address)) + " bytes");
			Debug.Log("Configuration size: " + Marshal.SizeOf(typeof(Configuration)) + " bytes");
			Debug.Log("StatusInfo size: " + Marshal.SizeOf(typeof(StatusInfo)) + " bytes");
			Debug.Log("ConnectionInfo size: " + Marshal.SizeOf(typeof(ConnectionInfo)) + " bytes");
			Debug.Log("ConnectionStatus size: " + Marshal.SizeOf(typeof(ConnectionStatus)) + " bytes");
			Debug.Log("NetworkingIdentity size: " + Marshal.SizeOf(typeof(NetworkingIdentity)) + " bytes");
			Debug.Log("NetworkingMessage size: " + Marshal.SizeOf(typeof(NetworkingMessage)) + " bytes");
		}

		protected override JobHandle OnUpdate(JobHandle inputDependencies) => inputDependencies;
	}
}

@staszain
Copy link
Author

staszain commented Apr 10, 2020

Can you please run the following script and confirm that on your end?

It appears the problem is on C++ end. I have 704 bytes for the StatusInfo instead of 712, which probably means that some C++ defaults on Mac are to blame. I obviously didn't make any changes to the library.

@nxrighthere
Copy link
Owner

What compiler are you using?

@staszain
Copy link
Author

AppleClang 11.0.3.11030032

@nxrighthere
Copy link
Owner

I have a feeling that this is because not all integer types are strictly set to exact-width, and some of those are compiler-specific, but I'm not sure.

I'll move this issue to the native repository if @fletcherdvalve doesn't appear here next Monday-Tuesday.

@zpostfacto
Copy link

Can you figure out where the difference is coming in?

@nxrighthere
Copy link
Owner

I might be wrong, but, probably what is affecting the layout is: int m_eEndReason; in SteamNetConnectionInfo_t and int m_cbSize; in SteamNetworkingIdentity which encapsulated into SteamNetConnectionInfo_t. It seems that only those guys are not strictly set to exact-width.

@zpostfacto
Copy link

But his size is 8 bytes smaller, right? Given that those int fields are 4 bytes on all platforms I know of, this would imply that they are 0 bytes on Apple clang. :)

I understand that it is legal for the C spec for int to be a size other than 4 bytes. I am just not aware of any ABI we care about that decided to make what would be an absolutely disastrous decision.

It's got to be some alignment thing or some enum or something.

@nxrighthere
Copy link
Owner

Yea, hard to diagnose this without the compiler and OS (I don't have it). So @staszain it would be great if you can help us to determine what exactly is affecting the layout.

@staszain
Copy link
Author

staszain commented Apr 14, 2020

I will be happy to provide any assistance you need from my side.

As I mentioned I believe the first shift comes from StatusInfo.connection field, which is UInt32 as declared in Valve.Sockets. It takes either 8 or 4 bytes depending on Pack attribute parameter and the entire content of the ConnectionInfo structure is shifted 4 bytes depending on that. I will double check.

@staszain
Copy link
Author

Yes, it looks like it is exactly the case. If I set StatusInfo StructLayout.Pack attribute to 8, then the content of StatusInfo.connectionInfo.identity.type, which comes right after StatusInfo.connection, is already shifted by four bytes. Thus I have the content of SteamNetworkingIdentity.m_cbSize in SteamNetworkingIdentity.m_eType field instead of its proper value.

@nxrighthere
Copy link
Owner

It appears the problem is on C++ end. I have 704 bytes for the StatusInfo instead of 712, which probably means that some C++ defaults on Mac are to blame. I obviously didn't make any changes to the library.

We are trying to determine why this happens? What you are explaining is about the managed side, but we have the problem with the native one since the layout is screwed there?

@staszain
Copy link
Author

staszain commented Apr 14, 2020

Well, my idea was that this happens because with this particular compiler the structure SteamNetConnectionStatusChangedCallback_t is packed in such a way that SteamNetConnectionInfo_t m_info is aligned to 4 bytes instead of 8 in case of other compilers

Sorry, I'm not 100% sure that this is what you asking. I'll try to research it more on my side.

@nxrighthere
Copy link
Owner

Can you please check side by side which fields are causing this? It seems that the managed side is correct in your case (it should be 712 bytes), but the native one is 704 bytes in size.

@staszain
Copy link
Author

staszain commented Apr 14, 2020

Well, the idea was that if Pack attribute fixes this on the managed side then it is SteamNetConnectionStatusChangedCallback_t.m_hConn that causes the problem. It is packed to 4 bytes instead of 8. I will try to find where the other 4 bytes diff comes from. But my guess it is m_eOldState.

@staszain
Copy link
Author

staszain commented Apr 14, 2020

SteamNetConnectionStatusChangedCallback_t *p = (SteamNetConnectionStatusChangedCallback_t*)nullptr;
    long offsetInfo = reinterpret_cast<long>(&p->m_info);
    long offsetOldState = reinterpret_cast<long>(&p->m_eOldState);
    std::cout << "offsetInfo: " << offsetInfo << ", offsetOldState: " << offsetOldState << std::endl;
    std::cout << "SteamNetConnectionStatusChangedCallback_t size: " << sizeof(SteamNetConnectionStatusChangedCallback_t) << std::endl;

Result:
offsetInfo: 4, offsetOldState: 700
SteamNetConnectionStatusChangedCallback_t size: 704

@staszain
Copy link
Author

staszain commented Apr 14, 2020

What confuses me is that SteamNetworkingIdentity is clearly aligned to 4 bytes. I think the cause of this might be that some compilers try to align inner structures to 8 bytes within a parent structure, while other compilers do not. In this case adding some padding manually should solve the problem and not break other platforms. Something like:

struct SteamNetConnectionStatusChangedCallback_t
{ 
	enum { k_iCallback = k_iSteamNetworkingSocketsCallbacks + 1 };
	HSteamNetConnection m_hConn;
        int32_t padding1;
	SteamNetConnectionInfo_t m_info;
	ESteamNetworkingConnectionState m_eOldState;
        int32_t padding2;
};

Unfortunately I cannot verify this on other platforms.

@nxrighthere
Copy link
Owner

Yea, I think this should work. In my case without any manual padding SteamNetConnectionStatusChangedCallback_t is correctly aligned. @fletcherdvalve

@staszain
Copy link
Author

Surprisingly struct alignas(8) SteamNetConnectionStatusChangedCallback_t doesn't help.

@zpostfacto
Copy link

zpostfacto commented Apr 14, 2020

I am very afraid of changing this struct, since it has been shipped in a Steamworks SDK. That means I am bound to support that ABI essentially indefinitely. If I change this struct I will have to introduce a backward compatibility layer or else break the games that are using the current version of the struct.

Another complication is that we have to use the same struct packing on both 32-bit and 64-bit within the same platform, because these are serialized in some cases between 32-bit and 64-bit processes. So where things have ended up is that structs are packed the same on the same platform even if bitness is different. But structs are NOT necessarily packed the same on different platforms. That is sort of anti the whole concept of .NET, where you have some virtual machine bytecode that can run anywhere. But that is the unfortunate state of affairs on Steamworks. We have to have platform specific binaries, even .net binaries.


#if defined(__linux__) || defined(__APPLE__) 
// The 32-bit version of gcc has the alignment requirement for uint64 and double set to
// 4 meaning that even with #pragma pack(8) these types will only be four-byte aligned.
// The 64-bit version of gcc has the alignment requirement for these types set to
// 8 meaning that unless we use #pragma pack(4) our structures will get bigger.
// The 64-bit structure packing has to match the 32-bit structure packing for each platform.
#define VALVE_CALLBACK_PACK_SMALL
#else
#define VALVE_CALLBACK_PACK_LARGE
#endif

#if defined( VALVE_CALLBACK_PACK_SMALL )
#pragma pack( push, 4 )
#elif defined( VALVE_CALLBACK_PACK_LARGE )
#pragma pack( push, 8 )
#else
#error ???
#endif 

typedef struct ValvePackingSentinel_t
{
    uint32 m_u32;
    uint64 m_u64;
    uint16 m_u16;
    double m_d;
} ValvePackingSentinel_t;

#pragma pack( pop )


#if defined(VALVE_CALLBACK_PACK_SMALL)
VALVE_COMPILE_TIME_ASSERT( sizeof(ValvePackingSentinel_t) == 24 )
#elif defined(VALVE_CALLBACK_PACK_LARGE)
VALVE_COMPILE_TIME_ASSERT( sizeof(ValvePackingSentinel_t) == 32 )
#else
#error ???
#endif

@nxrighthere
Copy link
Owner

nxrighthere commented Apr 14, 2020

That makes me sad... Should we close this issue and open/plan it as an enhancement in the native repository?

@zpostfacto
Copy link

Yeah, it is pretty crappy.

But now that I think more about it, though, it's probably pretty common for structs to be packed differently on different platforms, right? Also having an ABI that you are stuck with and that you cannot change doesn't seem that unusual -- outside of opensource projects that is. Perhaps the only thing that is unusual about this code is that we change the packing to ensure that 32-bit and 64-bit behave the same, but I don't think that really changes anything relevant to this problem. So surely there are standard workarounds for this, right?

Is it possible to have the managed code use different packing on different platforms? Seems like the the C# Steamworks wrappers must have hit this issue before. Again, I can't imagine that what we're doing here is really unusual. This has to be a common problem.

@zpostfacto
Copy link

@fletcherdvalve Also, what about this one? #8 (comment)

I could clear that pointer on shutdown, seems fine. You can set the debug output callback before initialization, and actually this is really useful. So I don't want to clear it on init.

@nxrighthere
Copy link
Owner

To be honest, this is the first time when I see something like this. My customers are using a lot of native libraries across platforms (C and C#) on Windows/Linux/macOS, and this was never an issue with structures. Their sizes were always consistent across compilers with MSVC/GCC/Clang.

In .NET, we can't determine a platform at compilation time, only during the runtime, which doesn't help here, unfortunately. Managed assemblies are unified before JIT. Except asking a user to manually define symbols in accordance with a platform if it's not Unity where they are automatically defined.

@zpostfacto
Copy link

I'm gonna throw in some padding fields to force the alignment. We can fix this, at least in the opensource code.

@staszain , can you confirm the values you currently have for:

sizeof(SteamNetConnectionInfo_t)
sizeof(SteamNetworkingIdentity)

@staszain
Copy link
Author

staszain commented Apr 15, 2020

std::cout << "sizeof(SteamNetConnectionStatusChangedCallback_t): " << sizeof(SteamNetConnectionStatusChangedCallback_t) << std::endl;
std::cout << "sizeof(SteamNetConnectionInfo_t): " << sizeof(SteamNetConnectionInfo_t) << std::endl;
std::cout << "sizeof(SteamNetworkingIdentity): " << sizeof(SteamNetworkingIdentity) << std::endl;

output:

sizeof(SteamNetConnectionStatusChangedCallback_t): 704
sizeof(SteamNetConnectionInfo_t): 696
sizeof(SteamNetworkingIdentity): 136

@zpostfacto
Copy link

I was working on inserting the padding fields, and then it hit me. Why not just use #pragma pack (4)? This is honestly what I wish we could do in Steamworks, but that ship probably sailed in 2010 or so.

@nxrighthere, if I set #pragma pack(4), that will actually cause the Windows packing to change, because it is currently 8. But it'd easy to make the C# match this, right? In fact, I think for all the structures we care about, 4 results in no padding anywhere, and so it is equivalent to 1.

@zpostfacto
Copy link

Looks like no matter what I do, the structs on 32-bit and 64-bit cannot be the same size, because e.g. SteamNetworkingMessage_t has a pointer member. I don't know how that works with C#.

@nxrighthere
Copy link
Owner

@fletcherdvalve Yes, it's not a problem to match it in C#, the size of the pointer is depends on the architecture of the system.

@zpostfacto
Copy link

So C# assemblies will dynamically adjust the offsets of members in structs when they are loaded, depending on the architecture? Whoa.

@nxrighthere
Copy link
Owner

Yes, this is one of the advantages of the virtual machine with just-in-time compiler.

zpostfacto added a commit to ValveSoftware/GameNetworkingSockets that referenced this issue Apr 17, 2020
Steamworks SDK has found itself in a really bad place having to support ABIs
that differ on different platforms.  That didn't matter in the days when
everybody compiled all of their binaries per platform, but it breaks the
"write once, run anywhere" ideal.  We don't have to support old ABIs so let's
just pick a packing and use it everywhere.  (But note that we do have stucts
with pointers in them, so our structs will vary between 32-bit and 64-bit.)

nxrighthere/ValveSockets-CSharp#8
@nxrighthere
Copy link
Owner

@fletcherdvalve So we are always #pragma pack(push, 8) now?

@nxrighthere
Copy link
Owner

@staszain Can you confirm that the latest version from the master branch works for you?

@zpostfacto
Copy link

Yeah, always 8 now. Since I am basically just arbitrarily picking, I decided to arbitrarily pick compatibility with Windows.

@ArnaudValensi
Copy link

Hi @nxrighthere, what is the proper way to use this library?
Should we use release 1.1.0-F1 with GameNetworkingSockets 1.1.0 or master?

@staszain
Copy link
Author

I've built the lib from the latest commit - it looks like the problem is gone. Thank you very much everyone!

@nxrighthere
Copy link
Owner

@ArnaudValensi From the release section, you should always use the version of the native library that specified there (see below changes). The source file from master might or not be compatible with the latest changes in the master of the native library, right now it's compatible.

@nxrighthere
Copy link
Owner

@ArnaudValensi
Release (Managed) -> Release (Native) with the specified version.
Master (Managed) -> Master (Native) but not guaranteed.

@ArnaudValensi
Copy link

Thank you very much for your answer and for your work.

@nxrighthere
Copy link
Owner

nxrighthere commented Apr 20, 2020

@ArnaudValensi It's Fletcher doing the job, I'm just matching stuff to changes that he's making. 😸

@zpostfacto
Copy link

@ArnaudValensi From the release section, you should always use the version of the native library that specified there (see below changes). The source file from master might or not be compatible with the latest changes in the master of the native library, right now it's compatible.

I could probably cherry pick and make a release. Or, just release what it is at master HEAD right now. There's some P2P work in master right now that isn't really complete, but I think everything still works. Let me know if it would help to make an official release.

@ArnaudValensi
Copy link

@ArnaudValensi It's Fletcher doing the job, I'm just matching stuff to changes that he's making. 😸

So thanks to you both @fletcherdvalve and @nxrighthere 🙏🙂

jeroemcano2124 added a commit to jeroemcano2124/GameNetworkingSockets that referenced this issue Jul 18, 2024
Steamworks SDK has found itself in a really bad place having to support ABIs
that differ on different platforms.  That didn't matter in the days when
everybody compiled all of their binaries per platform, but it breaks the
"write once, run anywhere" ideal.  We don't have to support old ABIs so let's
just pick a packing and use it everywhere.  (But note that we do have stucts
with pointers in them, so our structs will vary between 32-bit and 64-bit.)

nxrighthere/ValveSockets-CSharp#8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working native unity
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants