C#: маршалловые строки в utf8 char*
Фон
Я пытаюсь написать высокоуровневую оболочку libspotify на основе модифицированного libspotify.net ( http://libspotifydotnet.codeplex.com/). Поскольку libspotify.net - это всего лишь тонкий (и полностью ошибочный...) слой pinvoke, он не обрабатывает кодировку utf8, используемую libspotify.
Моя идея состояла в том, чтобы преобразовать строку в байт [] и соответствующим образом изменить подписи.
У меня есть эта нативная структура:
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;
Рабочая версия:
[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;
}
Эта версия помещает работу на разработчика, используя библиотеку.
Моя версия:
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);
...
}
При запуске это выдает следующую ошибку внутри libspotify: Место чтения нарушения доступа 0x000001c0 Я гуглил и где-то читал, что иногда копируется только первый элемент массива. Я попытался украсить все массивы
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1)]
Это привело к исключению "Невозможно выполнить маршализацию поля" cache_location "типа" sp_session_config_internal ": недопустимая комбинация управляемого / неуправляемого типа (поля массива должны быть связаны с ByValArray или SafeArray)."
ТЛ; др
Сложное решение с IntPtr и ручной сортировкой работает, более простое решение с байтовыми массивами для строк utf8 дает нарушение доступа при чтении библиотеки. Есть ли более простой способ, чем ручная сортировка с помощью intptr?
1 ответ
Я решил точно такую же проблему - упаковка libspotify для.NET. Я пошел по маршруту IntPtr и написал вспомогательный класс для выполнения сортировки. Я обнаружил, что сортировка массива крайне сводит с ума и не стоит рвать на себе волосы.
Вот вспомогательный класс:
https://github.com/openhome/ohLibSpotify/blob/master/src/ohLibSpotify/Utf8String.cs#L99
Упрощенная версия:
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;
}
}
}
И использование можно увидеть здесь:
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;
}
Поскольку писать этот материал быстро становится утомительным, подавляющее большинство упаковщиков и объявлений DllImport автоматически генерируются из заголовочного файла api.h. (Таким образом, вы не найдете их в репозитории github, вам нужно скачать проект и собрать его, чтобы увидеть весь сгенерированный код.)
Это все свободно лицензировано (BSD с 2 пунктами), так что не стесняйтесь использовать его или позаимствовать его.