C#: marshall strings to utf8 char* [duplicate]

点点圈 提交于 2019-12-23 22:54:55

问题


Background

I am trying to write a high level libspotify wrapper based on a modified libspotify.net (http://libspotifydotnet.codeplex.com/). As libspotify.net is just a thin (and completely bugged ... ) pinvoke layer, it does not handle the utf8 encoding libspotify uses.

My idea was to convert the string to a byte[] and change the signatures appropriately.

I have this native struct:

typedef struct sp_session_config {
  int api_version;                       
  const char *cache_location;           
  const char *settings_location;                    
  const void *application_key;           
  size_t application_key_size;           
  const char *user_agent;                
  const sp_session_callbacks *callbacks; 
  void *userdata;          
  bool compress_playlists;
  bool dont_save_metadata_for_playlists;
  bool initially_unload_playlists;
  const char *device_id;
  const char *proxy;
  const char *proxy_username;
  const char *proxy_password;
  const char *ca_certs_filename;
  const char *tracefile;

} sp_session_config;

Working version:

    [DllImport("libspotify")]
    public static extern sp_error sp_session_create(ref sp_session_config config, out sp_session sessionPtr);

    public struct sp_session_config
    {
        public int api_version;
        public IntPtr cache_location;
        public IntPtr settings_location;
        public IntPtr application_key;
        public uint application_key_size;
        public IntPtr user_agent;
        public IntPtr callbacks;
        public IntPtr userdata;
        public bool compress_playlists;
        public bool dont_save_metadata_for_playlists;
        public bool initially_unload_playlists;
        public IntPtr device_id;
        public IntPtr proxy;
        public IntPtr proxy_username;
        public IntPtr proxy_password;
        public IntPtr ca_certs_filename;
        public IntPtr tracefile;
    }

This version puts the work on the developer using the library.

My version:

    public struct sp_session_config_internal
    {
        public int api_version;
        public byte[] cache_location;
        public byte[] settings_location;
        public byte[] application_key;
        public uint application_key_size;
        public byte[] user_agent;
        public IntPtr callbacks;
        public IntPtr userdata;
        public bool compress_playlists;
        public bool dont_save_metadata_for_playlists;
        public bool initially_unload_playlists;
        public byte[] device_id;
        public byte[] proxy;
        public byte[] proxy_username;
        public byte[] proxy_password;
        public byte[] ca_certs_filename;
        public byte[] tracefile;
    }


    [DllImport("libspotify", EntryPoint="sp_session_create")]
    private static extern sp_error sp_session_create_internal(ref sp_session_config_internal config, out sp_session sessionPtr);
    public static sp_error sp_session_create(ref sp_session_config config, out sp_session sessionPtr)
    {
        sp_session_config_internal config_internal = new sp_session_config_internal();
        config_internal.api_version = config.api_version;
        config_internal.application_key = config.application_key;
        config_internal.application_key_size = (uint)config.application_key.Length;
        config_internal.ca_certs_filename = SH.StringToUtf8Bytes( config.ca_certs_filename);
    ...
        var err = sp_session_create_internal(ref config_internal, out session);
    ...
    }

When running, this gives the following error inside libspotify: Access violation reading location 0x000001c0 I googled and read somewhere that sometimes only the first array element gets copied. I tried decorating all arrays with

        [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1)]

That gave the exception "Cannot marshal field 'cache_location' of type 'sp_session_config_internal': Invalid managed/unmanaged type combination (Array fields must be paired with ByValArray or SafeArray)."

tl;dr

The complicated solution with IntPtr and manual marshalling works, the easier solution with byte arrays for utf8 strings gives a access violation when the library reads. Is there an easier way than manual marshalling with intptr?


回答1:


I have tackled the exact same problem - wrapping libspotify for .NET. I went the IntPtr route, and wrote a helper class to do the marshalling. I found array marshalling to be utterly maddening, and not worth tearing your hair out over.

Here's the helper class:

https://github.com/openhome/ohLibSpotify/blob/master/src/ohLibSpotify/Utf8String.cs#L99

Simplified version:

internal class Utf8String : IDisposable
{
    IntPtr iPtr;
    public IntPtr IntPtr { get { return iPtr; } }
    public int BufferLength { get { return iBufferSize; } }
    int iBufferSize;
    public Utf8String(string aValue)
    {
        if (aValue == null)
        {
            iPtr = IntPtr.Zero;
        }
        else
        {
            byte[] bytes = Encoding.UTF8.GetBytes(aValue);
            iPtr = Marshal.AllocHGlobal(bytes.Length + 1);
            Marshal.Copy(bytes, 0, iPtr, bytes.Length);
            Marshal.WriteByte(iPtr, bytes.Length, 0);
            iBufferSize = bytes.Length + 1;
        }
    }
    public void Dispose()
    {
        if (iPtr != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(iPtr);
            iPtr = IntPtr.Zero;
        }
    }
}

And usage can be seen here:

https://github.com/openhome/ohLibSpotify/blob/master/src/ohLibSpotify/SpotifySession.cs#L104

public static SpotifySession Create(SpotifySessionConfig config)
{
    IntPtr sessionPtr = IntPtr.Zero;
    IntPtr listenerToken;
    using (var cacheLocation = SpotifyMarshalling.StringToUtf8(config.CacheLocation))
    using (var settingsLocation = SpotifyMarshalling.StringToUtf8(config.SettingsLocation))
    using (var userAgent = SpotifyMarshalling.StringToUtf8(config.UserAgent))
    using (var deviceId = SpotifyMarshalling.StringToUtf8(config.DeviceId))
    using (var proxy = SpotifyMarshalling.StringToUtf8(config.Proxy))
    using (var proxyUsername = SpotifyMarshalling.StringToUtf8(config.ProxyUsername))
    using (var proxyPassword = SpotifyMarshalling.StringToUtf8(config.ProxyPassword))
    using (var traceFile = SpotifyMarshalling.StringToUtf8(config.TraceFile))
    {
        IntPtr appKeyPtr = IntPtr.Zero;
        listenerToken = ListenerTable.PutUniqueObject(config.Listener, config.UserData);
        try
        {
            NativeCallbackAllocation.AddRef();
            byte[] appkey = config.ApplicationKey;
            appKeyPtr = Marshal.AllocHGlobal(appkey.Length);
            Marshal.Copy(config.ApplicationKey, 0, appKeyPtr, appkey.Length);
            sp_session_config nativeConfig = new sp_session_config {
                api_version = config.ApiVersion,
                cache_location = cacheLocation.IntPtr,
                settings_location = settingsLocation.IntPtr,
                application_key = appKeyPtr,
                application_key_size = (UIntPtr)appkey.Length,
                user_agent = userAgent.IntPtr,
                callbacks = SessionDelegates.CallbacksPtr,
                userdata = listenerToken,
                compress_playlists = config.CompressPlaylists,
                dont_save_metadata_for_playlists = config.DontSaveMetadataForPlaylists,
                initially_unload_playlists = config.InitiallyUnloadPlaylists,
                device_id = deviceId.IntPtr,
                proxy = proxy.IntPtr,
                proxy_username = proxyUsername.IntPtr,
                proxy_password = proxyPassword.IntPtr,
                tracefile = traceFile.IntPtr,
            };
            // Note: sp_session_create will invoke a callback, so it's important that
            // we have already done ListenerTable.PutUniqueObject before this point.
            var error = NativeMethods.sp_session_create(ref nativeConfig, ref sessionPtr);
            SpotifyMarshalling.CheckError(error);
        }
        catch
        {
            ListenerTable.ReleaseObject(listenerToken);
            NativeCallbackAllocation.ReleaseRef();
            throw;
        }
        finally
        {
            if (appKeyPtr != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(appKeyPtr);
            }
        }
    }

    SpotifySession session = SessionTable.GetUniqueObject(sessionPtr);
    session.Listener = config.Listener;
    session.UserData = config.UserData;
    session.ListenerToken = listenerToken;
    return session;
}

Because it quickly gets tedious to write this stuff by hand, the vast majority of the wrappers and the DllImport declarations are automatically generated from the api.h header file. (Thus, you won't find them in the github repo, you'd need to download the project and build it to see all the generated code.)

It's all liberally licensed (2-clause BSD), so feel free either to use it or to borrow bits of it.



来源:https://stackoverflow.com/questions/17438339/c-marshall-strings-to-utf8-char

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!